Android -跨程序共享数据 ContentProvider

8、跨程序共享数据 ContentProvider

虽然文件和SharedPreferences存储听提供了MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE这两种操作模式,用于供给其他应用程序访问当前应用的数据,但这两种模式来Android4.2版本中都已经被废弃了。为什么呢?因为Android官方已经不再推荐使用这种方式来实现跨程序数据共享的功能,而是推荐使用更加安全可靠的ContentProvider技术。

为什要将我们程序的数据共享给其他程序呢?当然,这个是要视情况而定的,比如我们的帐号密码这样的数据显然不能共享绘其他程序的。不过一些可以让其他程序进行二次开发的数据是可以共享的。例如系统的通讯录程序,它的数据库中保存了很多联系人信息,如果这些数据都不允许共享第二方程序进行访问的话,恐怕很多应用程序的功能要大大折扣了。除了通讯录之外,还有短信,媒体库等程序都实现了跨程序数据共享功能,而使用的技术当然就是我们接下来要学习的ContentProvider了。

8.1 ContentProvider简介

ContentProvider主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制。允许一个程序另一个程序中的数据,同时还能保证被访问数据的安全性。目前,使用ContentProvider和 Android实现跨程序共享数据的标准方式。

不同于文件存储和SharedPreferences存储中的两种全局可读写的操作模式,ContentProvider可以丢弃只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。

在学习ContentProvider之前,我们首选要了解Android运行时权限,当然,不光是ContentProvider以后我们开发过程中会经常使用运行时权限。

8.2 运行时权限

Android的权限机制燕不是什么新鲜的事物,从系统的第一个版本开始就已经存在了。但其实之前Android的权限机制在保护用户安全和隐私等方面起到的作用比较有限。为此Android开发团队在Android6.0系统中引入了运行时权限这个功能,从而更好地保护了用户的安全和隐私。

8.2.1 Android权限机制详解

我们之前为了监听开机广播,我们在AndroidManfest.xml文件中添加了这样一句权限声明:

<uses-permission android:name=”android.permission.RECEIVE_BOOT_COMPLETED”/>

因为监听开机广播涉及了用户设备的安全,因此必须在AndroidManfest.xml中加入权限声明,否则我们的程序就会崩溃。

那么问题了,加入了这句权限声明后,对于用户来说到底有什么影响呢?为什么这样就可以保护用户设备的安全了呢?

用户主要在两个方面得到了保护。一方面,如果用户在低于Android6.0系统的设备上安装该程序,会在安装界面给出该程序的所有权限申请,这样用户就可以清楚地知道该程序一共申请了哪些权限,从而决定是否要安装这个程序。

另一方面,用户可以随时在应用程序管理界面查看任何一个程序的权限申请状况。这样该程序申请的所有权限就一目了然,什么都瞒不过用户的眼睛,以此保证应用程序不会出现各种滥用程序的情况。

这样权限机制的设计思路其实非常简单,就是用户如果认可你所申请的权限,就会安装你的程序,如果不认可你所申请的权限,那么直接选择不安装就可以了。

但是理想总是美好的,实现是残酷的。很多我们离开不的常用软件普遍存在滥用权限的情况,不管我们到底用不用得到,总之先申请了再说,比如微信所申请的权限。

Android开发团队当然也意识到了这个问题,于是 在Android6.0系统中加入了运行时权限功能,也就是说,用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中再对某一项权限进行授权。比如一款相机应用在运行时申请了地理位置定位权限。就算我拒绝了这个权限,也应该可以使用这个应用其他功能,而不是像之前那样超越无法安装它。

Android将常用的权限大致归成了两类:一类是普通权限,一类是危险权限。准确来说,其实还有一些特殊权限,不过这些权限使用得相对较少。普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,对于这部分权限申请,系统会自动帮我们进行授权,不需要用户操作。危险权限表示那些可以会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等。对于这部分权限申请,必须由用户手动授权才可以,否则程序就无法使用相应的功能。

权限组名权限名
CALENDARREAD_CALENDAR、WRITE_CALENDAR
CALL_LOGREAD_CALL_LOG/WRITE_CALL_LOG/PROCESS_OUTGOING_CALLS
CAMERACAMERA
CONTACTSREAD_CONTACTS/WRITE_CONTACTS/GET_ACCOUNTS
LOCATIONACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION / ACCESS_BACKGROUPD_LOCATION
MICROPHONERECORD_AUDIO
PHONEREAD_PHONE_START/READ_PHONE_NUMBERS/CALL_PHONE/ANSWER_PHONE_CALLS/ADD_VOICEMAIL/USE_SIP/ACCEPT?HANDDVER
SESORSBODY_SENSORS
ACTIVITY_REOGNITIONACTIVITY_RECOGNITION
SMSSEND_SMS/RECEIVE_SMS/READ_SMS/RECEIVE_WAP_PUSH/RECEIVE_MMS
STORAGEREAD_EXTERNAL_STORAGE/WRITE_EXTERNAL_STORAGE/ACCESS_MEDIA_LOCATION

这张表你看越来可能并不会那么轻松,因为里面的权限全部是你没有使用过的。不过没有关系,你并不需要了解表格中的每个权限的作用,只要把它当成一个参照表来查看就行了,每当使用一个权限时,可以先互这张表中查一下,如果是这张表中的权限,就需要进行运行时权限处理,否则,只需要在AndroidManfest.xml文件中添加一下权限的声明就可以了。

另外注意,表格中每个危险危险权限都属于一个权限组,我们在进行运行时权限处理时使用的是权限名。原则上,用户一旦同意了某个权限申请之后,同组的其他权限也会被系统自动授权,但是请谨记,不要基本此规则来实现任何功能逻辑,因为Android系统随时有可能调整权限的分组,

8.2.2 在程序运行时申请权限

我们用表格中的CALL_PHONE来作为示例吧。

CALL_PHONE这个权限是编写拨打电话功能的时候需要声明的,因为拨打电话会涉及用户手机的资费问题,因而被列为危险权限。在Android6.0 系统出现之后,拨打电话功能的实现其实非常简单。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">


   <Button
       android:id="@+id/button1"
       android:text="直接拨打电话至10086"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
</LinearLayout>

我们在布局文件中添加一个按钮,点击按钮就去触发拨打电话的逻辑,接着修改MainActivity.java’

MainActivity.java

package com.ziyia.runtimepermissiontest;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContentProviderCompat;
import androidx.core.content.ContentResolverCompat;
import androidx.viewpager.widget.PagerAdapter;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

   @RequiresApi(api = Build.VERSION_CODES.M)
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);


       // 如果没有获取权限,则获取
       if (checkSelfPermission(Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
           ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CALL_PHONE}, 1);
      } else {// 如果已经获取权限,执行我们的测试代码。
           call();
      }
  }


   /**
    * 申请权限时,系统会弹出一个提示框,用户可以选择同意或拒绝我们的权限申请,不论是哪种结果,最终都会回调到该方法
    * ,而授权结果保存在grantResults参数当中,我们只需要判断一下最后的授权结果,如执行我们的逻辑即可。
    * @param requestCode
    * @param permissions
    * @param grantResults
    */
   @Override
   public void onRequestPermissionsResult(int requestCode, @NonNull @org.jetbrains.annotations.NotNull String[] permissions, @NonNull @org.jetbrains.annotations.NotNull int[] grantResults) {
       super.onRequestPermissionsResult(requestCode, permissions, grantResults);
       switch (requestCode) {
           case 1:
               if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                   call();
              } else {
                   Toast.makeText(MainActivity.this, "无权限", Toast.LENGTH_SHORT).show();
              }
               break;
      }
  }

   // 获取权限之后要执行的动作
   private void call() {
       Button button1 = findViewById(R.id.button1);

       button1.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               try {
                   Intent intent = new Intent(Intent.ACTION_CALL);
                   intent.setData(Uri.parse("tel:10086"));
                   startActivity(intent);
              } catch (Exception e) {
                   e.printStackTrace();
              }
          }
      });
  }
}

可见,在按钮的点击事件中,我们构建了一个隐式Intent,Intent的action指定为Intent.ACTION_CALL,这是一个系统内置的拨打电话操作,然后在data部分指定了协议是tal,号码是10086,之前我们在学广播的时候已经做过类似的操作了,和上次不同的是,之前是打开拨号页面,本次是直接拨打电话,因此必须声明权限,为了防止程序崩溃,我们将所有代码都放在了try代码块当中。

AndroidManfest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.ziyia.runtimepermissiontest">

   <uses-permission android:name="android.permission.CALL_PHONE"/>

   <application
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/Theme.MyApplication">
       <activity android:name=".MainActivity">
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>
       </activity>
   </application>

</manifest>

这样,我们拨打电话的功能就实现了,并且在低于Android6.0系统的手机上都是可以正常运行的。但是,如果我们在Android6.0以或者更高版本系统的手机上运行,点击 “直接拨打电话至10086” 按钮就没有任何效果了,这时,观察Logcat中的打印日志,你会看到错误信息。

要修复这个问题,在AndroidManfest.xml中声明权限即可。

<uses-permission android:name=”android.permission.CALL_PHONE”/>

上方例子覆盖了运行时权限的完整流程,下面我们具体解析一下。说白了,运行时权限的核心就是在程序运行过程中由用户授权我们去执行某些危险操作,程序是不可以擅自做主去执行这个危险操作的。因此,第一步就是要先判断用户是不是已经给过我们授权了,借助的是checkSelfPermission()方法,checkSelfPermission()方法接收一个参数,需要传递权限名。比如打电话的权限名就是Manifest.parmission.CALL_PHONE,然后我们使用方法的返回值和PackageManger.PEMISSION_GRANTED做比较,相等就说明已经授权过,不等就表示用户没有授权。

如果已经授权就简单了,我们直接执行拨打电话逻辑就可以了,这里我们把拨打电话的逻辑封装到了cell()方法当中。如果没有授权的话,则需要调用ActivityCompat.request.Permissions()方法向用户申请授权。requestPermissions()方法接收3个参数:第一个参数要求是Activity的实例;第二个参数是一个String数组,我们把要申请的权限名放在数组中既可;第三个参数是请求码,只要是唯一的就可以了。

调用完requestPermissions()方法之后,系统会弹出一个权限申请的对话框,用户可以选择同意或拒绝我们的权限申请,否认是哪种结果,最终都会回调到onRequestPermissionResult()方法中,而授权的结果则会封装在grantResults参数当中,我们最后再判断一下最后的授权结果,如果同意的话,则执行我们拨打电话的逻辑,否则弹出一条失败提示。

现在我们重新编译程序,点击 “直接拨打电话至10086”

由于用户还没有授权过我们拨打电话的权限,因此第一次运行会弹出这样一个权限申请的对话框,用户可以选择同意或者拒绝。比如说我们点击 “拒绝”

由于用户没有授权,我们只能弹出一个操作失败的提示,我们再次点击 “直接拨打电话至10086”按钮,我们这次点击 “始终允许” ,可见,这次我们成功进行拨打电话界面了。并且由于用户已经完成了授权操作,之后再次点击 “直接拨打电话至10086”按钮不会再次弹出权限申请对话框,而是可以直接拨打电话,如果我们后悔了?咋办?没有关系,我们随时可以将授予程序的危险权限进行关闭,进入系统设置,找到权限管理,取消授予相应的权限即可。

8.3 访问其他程序中的数据

ContentProvider的用法一般有两种:一种是使用现有的ContentProvider读取和操作相应程序中的数据;另一种是创建自己的ContentProvider,给程序的数据提供外部访问接口。

如果一个程序通过ContentProvider对其数据提供了外部访问接口,那么任何其他的应用程序都可以对这部分数据进行访问。Android系统中自带的通讯录、短信、媒体库等软件都提供了类似的访问接口,这就使第三方应用程序可以充分地利用这部分数据实现更好的功能。

8.3.1 ContentProvider的基本用法

对于每个程序来说,如果想要访问ContentProvider中的共享数据,就一定要借助ContentResolver类,可以通过Context中的getContentResolver()方法获取该类的实例。ContentResolver中提供了一系列的方法用于对数据进行增删改查操作,其中insert()方法用于添加数据,update()方法用于更新数据,deletr()方法用于删除数据,query()方法用于查询数据。有没有相识的感觉?没错,SQLList中也是使用这几个方法进行增删改查操作,

不同于SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI,内容URI给ContentProvider中的数据建立了唯一标识符,它主要由两部分组成:authority和path,authority是用于对不同的应用程序做区分的,一般为了避免冲突,会采用应用包名和方式进行命名。比如某个应用的饭锅是com.ziyia.app。那么该应用对应的authority就可以命名为com.ziyia.provider。path则是用于对同一应用程序中不同的表做区分的。通常会添加到authority的后面。比如某个应用的数据库里存在两张表table和talbe2,这时就可以将path分别命名为/table,/table2,然后把authority和path进行组合,内容URI变成了com.ziyia.app.provider/table1和com.ziyia.app.provider/table2。不过,目前还很难辨认出这两个字符串就是两个内容URI,我们还需要在字符串的头部加上协议声明,因此内容URI最标准的格式如下:

content://com.ziyia.app.provider/table1

comtent://com.ziyia.app.provider/talbe2

有没有发现。内容URI可以非常清楚了表达我们想要访问哪个程序中表里的数据,也正是如此,ContentResolver中的增删改查方法才拼接Uri对象作为参数。如果使用表名的话,系统将无法得知我们期望访问的是哪个应用程序里的表。

得到了内容URI之后,我们还需要将它解析成Uri对象才可以作为参数传入,语法也非常简单:

Uri uri = Uri.parse(“content://com.ziyia.app.provider/talbe1”);

现在我们可以使用这个Uri对象查询talbe1表中的数据了

contentResolver.query(uri, projection, selection, selectionArgs, sorOrder);

这些参数和SQLiteDatabase中query()方法里参数很像,但总体来说要简单一些。毕竟这是在访问其他程序中的数据,没必要构建过于复杂的查询语句,以下是参数详细的解释:

query()参数对应SQL语句描述
uriFROM TABLE name指定查询某个应用程序下的某一些表
projectionSELECT column1, column2指定查询的列名
selectionWHERE column = value指定WHERE的约束条件
selectionArgs指定WHERE中的点位符提供具体的值
sortOrderORDER BY column1, column2指定查询结果的排序方式

查询完成后返回的仍然还是一个Cursor对象,这时我们就可以将数据从Cursor对象中逐个读取出来了。读取的思路仍然是通过移动游标的位置遍历Cursor的所有行。然后取出每一行中相应列的数据,如

while(cursor.moveNext()) {

Object column1 = cursor.getString(cursor.getColumnIndex(“column1”));

Object column2 = cursor.getString(cursor..getColumnIndex(“column2”)):

}

insert()方法

ContentValues values = new ContentValues();
values.put("column1", "text");
values.put("column2", 1);
getContentResolver().insert(uri, values);

可见,狐朋狗友是将等添加的数据组装到ContentValues中,然后调用ContentResolver的insert()方法,将Uri和ContentValues作为参数传入即可。

update()方法

ContentValues values = new ContentValues();
values.put("column1", "");
getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new
String[] {"text", "1"})

注意,上方代码使用了selection和selectionArgs参数来对想要的更新的数据进行约束。

delete()方法

getContentResolver().delete(uri, "column2 = ?", new String[] { "1" });

8.2.3 读取系统联系人

由于我们是在模拟器中操作的,通讯录里面并没有联系人存在,所以我们要自己添加一些联系人用于测试。

笔者新建一个ContactsTest项目,读者们看情况创建 activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">
   <Button
       android:id="@+id/readphone"
       android:text="获取手机号"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>

   <ListView
       android:id="@+id/listView"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>
</LinearLayout>

这里我们简单添加一个ListView来显示所有联系人

MainActivity.java

package com.ziyia.contactstest;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.telephony.SmsManager;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Toast;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EventListener;
import java.util.List;

public class MainActivity extends AppCompatActivity {
   Activity T = MainActivity.this;
   private Button readphone;
   private ListView listView;

   private List<String> list = new ArrayList<>();
   private ArrayAdapter<String> arrayAdapter;

   private void init() {
       readphone = findViewById(R.id.readphone);
       listView = findViewById(R.id.listView);
  }
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);

       init();


       arrayAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, list);
       listView.setAdapter(arrayAdapter);

           readphone.setOnClickListener(new View.OnClickListener() {
               @Override
               public void onClick(View v) {
                   if (ContextCompat.checkSelfPermission(T, Manifest.permission.READ_SMS) != PackageManager.PERMISSION_GRANTED) {
                       ActivityCompat.requestPermissions(T, new String[]{Manifest.permission.READ_SMS}, 2);
                  } else {
                       list.clear();
                       readContacts ();
                  }
              }
          });
  }

   @Override
   public void onRequestPermissionsResult(int requestCode, @NonNull @org.jetbrains.annotations.NotNull String[] permissions, @NonNull @org.jetbrains.annotations.NotNull int[] grantResults) {
       super.onRequestPermissionsResult(requestCode, permissions, grantResults);
       switch (requestCode) {
           case 1:
               if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                   readContacts();
              } else {
                   Toast.makeText(T, "没有权限读取电话", Toast.LENGTH_SHORT).show();
              }
               break;
      }

  }

   private void readContacts() {
       ContentResolver contentResolver = getContentResolver();
       Cursor query = contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
       while (query.moveToNext()) {
           // 获取联系人姓名
           String displayName = query.getString(query.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
           // 获取联系人手机号
           String number = query.getString(query.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
           list.add("姓名" + displayName + ",手机号" + number);
      }
       arrayAdapter.notifyDataSetChanged();
       query.close();
  }


}

在onCreate()方法中,我们首选按照ListView的标准用法对初始化,然后开始调用运行时权限的处理逻辑,因为READ_CONTACTS权限属于危险权限。关于运行时权限的处理流程、

下面我们重点看一下readContacts()方法,可见,这里使用了ContentResolver的query()方法查询系统的联系人数据。不过传入的Uri参数怎么有点奇怪呢?为什么没有调用Uri.parse()方法去解析一片内容的URI字符串呢?这是因为ContentsContentract.CommonDataKinds.Phone类已经帮我们做好了封装,提供一个CONTENT_URI常量,而这个常量就是使用Uri.parse()方法解析出来的结果。接着我们对query()方法返回的Cursor对象进行遍历。联系人姓名对应的常量是ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,联系人手机号对应的常量是ContactsContract.CommonDataKinds.Phone.NUMBER,将两个数据取出后进行拼接。然后将拼接后的数据添加到ListView的数据源里,并通知刷新一下ListView,最后千万不要忘记将Cursor对象关闭。

这样就结束了吗?还差一点点,读取系统联系人的权限千万不能忘记声明,修改AndroidManfest.xml中的代码:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.ziyia.contactstest">
   <uses-permission android:name="android.permission.READ_CONTACTS"/>

   <application
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/Theme.MyApplication">
       <activity android:name=".MainActivity">
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>
       </activity>
   </application>

</manifest>

加入了android.permission.READ_CONTACTS权限,这样我们的程序就可以访问系统的联系人数据了。

8.4 创建自己的ContentProvider

前面我们学习了如何在自己的程序中访问其他应用程序的数据,总体来说,思路还是非常简单的,只需要获得该应用程序的内容URI,然后借助ContentResolver进行增删改查操作就可以一。可是你没有没有想过,那些提供外部访问接口的应用程序教师如何实现这种功能的呢?

8.4.1 创建ContentProvider的步骤

前面已经提到过,如果想要实现跨程序共享数据的功能,可以通过新建一个类去继承ContentProvider的方式来实现。ContentProvider类中有6个抽象方法,我们在使用子类继承它的时候,需要将这6个子方法全部重写。

  1. onCreate()初始化ContentProvider的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回true表示ContentProvider初始化成功,返回false则表示失败。
  2. query()从ContentProvider中查询数据。Uri参数用于确定查询哪张表,projection参数用于确定查询哪些列,selection和selectionArgs参数用于约束查询哪些行,SortOrder()参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。
  3. insert()向ContentProvider中添加一条数据。uri参数用于确定要添加到的表,待添加的数据保存在values参数中。添加完成后,返回一个用于表示这条新记录的URI。
  4. update()更新ContentProvider中已有数据。uri参数用于确定更新哪一张表的数据,新数据保存在values参数中,selection和selectionArsgs参数用于约束更新哪些行。
  5. delete()从ContentProvider中删除数据。uri参数用于确定删除哪一张表中的数据,selection和selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。
  6. getType()根据传入的内容URI返回相应的MIME类型。

可见,很多方法里带有uri这个参数,这个参数正是调用ContentResolver的增删改查方法时传递过来的。而现在我们需要对传入的uri参数进行解析,从中分析出调用方法所期望的访问时表的数据。

标准写法

content://com.ziyia.app.provider/table1

这就表示调用方期望访问的慢com.ziyia.app这个应用的table1表中的数据。

除此之外,我们还可以在这个内容URI的后面加上一个id,如

content://com.ziyia.app.provider/table1/1

这就表示调用方期望访问的是com.ziyia.app这个应用的table1表中id为1的数据。

内容URI的格式主要就只有以上两种,以路径结尾的表示期望访问表中所有数据。以id结尾表示期望访问该表中拥有相应id的数据。我们可以使用通配符分别匹配这两种格式的内容URI,规则如下

*表示匹配任意长度的任意字符

#表示匹配任意长度的的数字

所以,一个能匹配任意表的内容的URI格式就可以写成

content://com.ziyia.app/provider/*

一个能够匹配table1表中任意一行数据的内容URI格式就可以写成、

contant://com.ziyia.app/rpvider/table1/#

接着,我们再借助UriMatcher这个类就可以轻松地实现匹配内容URI的功能。UriMatcher中提供了一个addURI()方法,这个方法接收3个参数,可以分别把authority path和一个自定义代码传递过去。这样,当我们去调用UriMatcher的match()方法时,就可以将一个uri对象传入返回值是某个能匹配这个uri对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是哪张表中的数据了。

package com.ziyia.databasetest;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Intent;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;

public class DatabaseProvider extends ContentProvider {
   private static final int table1Dir = 0;
   private static final int table1Item = 1;
   private static final int table2Dir = 2;
   private static final int table2Item = 3;
   private static final String AUTHORITY = "com.ziyia.databasetest.provider";
   private MyDatabaseHelper myDatabaseHelper = null;
   private static UriMatcher uriMatcher;


   static {
       uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
       uriMatcher.addURI(AUTHORITY, "table1", table1Dir);
       uriMatcher.addURI(AUTHORITY, "table1/#", table1Item);
       uriMatcher.addURI(AUTHORITY, "table2", table2Dir);
       uriMatcher.addURI(AUTHORITY, "table2/#", table2Item);

  }
   public DatabaseProvider() {

  }

   @Override
   public Cursor query(Uri uri, String[] projection, String selection,
                       String[] selectionArgs, String sortOrder) {
       SQLiteDatabase db = myDatabaseHelper.getReadableDatabase();
       Cursor cursor = null;
       switch (uriMatcher.match(uri)) {
           case table1Dir:
               // 查询table1中的所有数据
               break;
           case table1Dir:
               // 查询table1中的单条数据
               break;
           case table1Item:
               // 查询table2表中的所有数据
               break;
           case table2Item:
               // 查询table2表中的单条数据
               break;
           default:
               break;
      }
  }

}

可见,我们新增了4个整型变量,tableDir表示访问talbe1表中的所有数据,table1Item表示访问table1表中的单条数据,table2Dir表示访问table2表中的所有数据,table2Item表示访问table2表中的单条数据。我们在DatabaseProvider实例化的时候在静态代码块中创建了UriMatcher的实例,并调用addURI()方法,将期望匹配的内容URI格式传递进去,注意这里传入的路径参数是可以使用通配符的,然后当query()方法被调用的时候,就会通过UriMatcher的match()方法对传入的Uri对象进行匹配,如果发现UriMatcher中某个内容URI格式成功匹配了该Uri对象,则会返回相应的自定义代码,然后我们就可以判断出调用方期望访问的到底是什么数据了。

如果够细心,会发现还有一个getType()方法,它是所有CotentProvider都必须提供的一个方法,用于获取Uri对象所对应的MIME类型。一个内容URI所对应的MIME字符串主要由3部分组成,分别遵守如下格式规定:

  1. 必须以vnd开头
  2. 如果内容URI以路径结尾,则后接android.cursor.dir/;如果内容URI以id结尾,则后接android.cursor.item/
  3. 最后接上vnd.<authority>.<path>@Override
    public String getType(Uri uri) {
       String str = “”;
       switch (uriMatcher.match(uri)) {
           case bookDir:
               str = “vnd.android.cursor.dir/vnd.com.ziyia.databasetest.provider.book”;
               break;
           case bookItem:
               str = “vnd.android.cursor.item/vnd.com.ziyia.databasetest.provider.book”;
               break;
           case categoryDir:
               str = “vnd.android.cursor.dir/vnd.com.ziyia.databasetest.provider.category”;
               break;
           case categoryItem:
               str = “cnd.android.cursor.item/vnd.com.ziyia.databasetest.provider.category”;
               break;
      }
       return str;
    }

至此,一个完整的ContentProvider就创建完成了,现在任何一个程序都可以通过ContentResolver访问我们程序中的数据,那么,如果才能保证这些数据的安全呢?其实多亏了ContentProvider良好的机制,这个问题在不知不觉中已经被解决了。因为所有的增删改查操作都一定要匹配到URI才能进行,而我们当然不可能向UriMatcher中添加隐私数据的URI,所有这部分内容根本无法被访问,安全问题就不存在了。

8.4.2 实现跨程序数据共享

package com.ziyia.databasetest;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;

public class DatabaseProvider extends ContentProvider {
   private static final int bookDir = 0;
   private static final int bookItem = 1;
   private static final int categoryDir = 2;
   private static final int categoryItem = 3;
   private static final String AUTHORITY = "com.ziyia.databasetest.provider";
   private MyDatabaseHelper myDatabaseHelper = null;
   private static UriMatcher uriMatcher;


   static {
       uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
       uriMatcher.addURI(AUTHORITY, "book", bookDir);
       uriMatcher.addURI(AUTHORITY, "book/#", bookItem);
       uriMatcher.addURI(AUTHORITY, "category", categoryDir);
       uriMatcher.addURI(AUTHORITY, "category", categoryItem);

  }
   public DatabaseProvider() {

  }

   @Override
   public int delete(Uri uri, String selection, String[] selectionArgs) {
       SQLiteDatabase db = myDatabaseHelper.getWritableDatabase();
       int deleteRows = 0;
       switch (uriMatcher.match(uri)) {
           case bookDir:
               deleteRows = db.delete("Book", selection, selectionArgs);
               break;
           case bookItem:
               String bookId = uri.getPathSegments().get(1);
               deleteRows = db.delete("Book", "id = ?", new String[]{bookId});
               break;
           case categoryDir:
               deleteRows = db.delete("Category", selection, selectionArgs);
               break;
           case categoryItem:
               String categoryId = uri.getPathSegments().get(1);
               deleteRows = db.delete("Category", "id = ?", new String[]{categoryId});
               break;
           default:
               break;
      }
       return deleteRows;
  }

   @Override
   public String getType(Uri uri) {
       String str = "";
       switch (uriMatcher.match(uri)) {
           case bookDir:
               str = "vnd.android.cursor.dir/vnd.com.ziyia.databasetest.provider.book";
               break;
           case bookItem:
               str = "vnd.android.cursor.item/vnd.com.ziyia.databasetest.provider.book";
               break;
           case categoryDir:
               str = "vnd.android.cursor.dir/vnd.com.ziyia.databasetest.provider.category";
               break;
           case categoryItem:
               str = "cnd.android.cursor.item/vnd.com.ziyia.databasetest.provider.category";
               break;
      }
       return str;
  }

   @Override
   public Uri insert(Uri uri, ContentValues values) {
       SQLiteDatabase db = myDatabaseHelper.getWritableDatabase();
       Uri uriReturn = null;
       switch (uriMatcher.match(uri)) {
           case bookDir:
           case bookItem:
               long newBookId = db.insert("Book", null, values);
               uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
               break;
           case categoryDir:
           case categoryItem:
               long newCategoryId = db.insert("Category", null, values);
               uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
               break;
           default:
               break;
      }
       return uriReturn;
  }

   @Override
   public boolean onCreate() {
       // TODO: Implement this to initialize your content provider on startup.
       myDatabaseHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
       return true;
  }

   @Override
   public Cursor query(Uri uri, String[] projection, String selection,
                       String[] selectionArgs, String sortOrder) {
       SQLiteDatabase db = myDatabaseHelper.getReadableDatabase();
       Cursor cursor = null;
       switch (uriMatcher.match(uri)) {
           case bookDir:
               cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
               break;
           case bookItem:
               String bookId = uri.getPathSegments().get(1);
               cursor = db.query("Book", projection, "id = ?", new String[]{bookId}, null, null, sortOrder);
               break;
           case categoryDir:
               cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder);
               break;
           case categoryItem:
               String id = uri.getPathSegments().get(1);
               cursor = db.query("Category", projection, "id = ?", new String[]{id}, null, null, sortOrder);
               break;
           default:
               break;
      }
  }

   @Override
   public int update(Uri uri, ContentValues values, String selection,
                     String[] selectionArgs) {
       SQLiteDatabase db = myDatabaseHelper.getWritableDatabase();
       int updateRows = 0;
       switch (uriMatcher.match(uri)) {
           case bookDir:
               updateRows = db.update("Book", values, selection, selectionArgs);
               break;
           case bookItem:
               String bookId = uri.getPathSegments().get(1);
               updateRows = db.update("Book", values, "id = ?", new String[]{bookId});
               break;
           case categoryDir:
               updateRows = db.update("Category", values, selection, selectionArgs);
               break;
           case categoryItem:
               String categoryId = uri.getPathSegments().get(1);
               updateRows = db.update("Category", values, "id = ?", new String[]{categoryId});
               break;
           default:
               break;
      }
       return updateRows;
  }
}

代码虽然可以很长,不过不用担心,这些内容都不难理解,因为使用的全部都是刚才我们学到的知识。首先,在类的一开始,同样是定义了4个变量,分别用于表示访问 Book 表中的所有数据。访问 Book 表中的单条数据。访问Category表中的所有数据和访问Category表中的单条数据。然后static静态代码块是对UriMatcher进行初始化操作,将期望匹配的几种URI格式都添加了进去。

接下来就是抽象方法的实现了

  1. onCreate()这个方法的代码很短,首先调用了getContext()方法然后返回true表示初始化成功。
  2. query()在这个方法中先获取了SQLiteDatabase的实例,然后根据传入的URI参数判断用户想要访问哪张表,再调用SQLiteDatabase的query()方法进行简单的查询,并将Cursor对象返回就好了,注意,当访问单条数据的时候,调用了Uri对象的getPathSegments()方法它会将内容URI权限之后的部分以 “/” 进行分割,并把分割后的结果放入一个字符列表中,那这个字符串列表0的位置存放的就是路径,第1个位置存放的就是id了。得到了id之后,再通过selection和selectionArgs参数进行约束,就实现了查询单条数据的功能。
  3. insert()它也是先获取SQLiteDatabase()的实例,然后根据传入的Uri参数判断用户想要往哪张表添加数据,再调用SQLiteDatabase中的insert()方法将数据进行添加。注意insert()方法要求返回一个能够表示这条新增数据的URI,所以我们还需要调用Uri.parse()方法,将一个内容URI解析成Uri对象并返回。
  4. update()它也是先获取 了SQLiteDatabase的实例,然后根据传入的Uri参数判断用户想往哪张表更新数据,再调用SQLiteDatabase的update()方法进行数据的更新。受影响的行数作为返回值返回。
  5. delete()仍然是先获取SQLiteDatabase,然后根据传入的uri参数判断用户想要往哪张表删除数据,再调用SQLiteDatabase中的delete()方法删除就行了,被删除的行数和作为返回值进行返回。
  6. getType()这个方法完全是按照以下规则所编写的:
    • 必须以vnd开头
    • 如果内容URI以路径结尾,由接上 android.cursor.dir/;如果内容URI以id结尾,则接上 android.cursor.item/
    • 最后接上<authority>.<path>

另外,还有一点需要注意,ContentProvier一定要在AndroidManfest.xml文件中注册才可以使用,不过幸运的是,我们Android studio已经帮我们做了这一步工作了

发表回复

相关

浙ICP备2021031744号-3