Content Provider(内容提供器)主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。目前,使用内容提供器是Android实现跨程序共享数据的标准方式。
不同于文件存储和SharedPreferences存储中的两种全局可读写操作模式,内容提供器可以选择只对哪一部分数据进行共享,从而保证程序中的隐私数据不会有泄漏的风险。

运行时权限

Android 6.0以前,Android的权限机制在保护用户安全和隐私等方面起到的作用比较有限,Android开发团队在Android 6.0系统中引用了运行时权限这个功能,从而更好地保护了用户的安全和隐私。

Android权限机制

当应用程序要访问系统相关信息(例如网络状态)或是设置系统设置,这会涉及用户设备的安全性,必须要在AndroidManifest.xml文件中添加权限声明,否则程序会崩溃

<!--添加权限-->
<uses-permission android:name="android.permission.READ_CONTACTS"/>

声明权限之后用户在两个方面得到了保护:

  1. 如果用户在低于6.0系统的设备上安装程序,会在安装界面上提醒用户程序所要获取的权限,这样用户就可以清楚的知道该程序一共申请了那些权限,从而决定是否要安装程序。
  2. 用户可以随时在应用程序管理界面查看任意一个程序的权限申请情况

虽然Android系统通过权限保护了用户的信息安全,但是很多常用软件普遍存在滥用权限的情况,不管最后到底用不用,先把申请权限了。Android开发团队意识到了这个问题,于是在6.0系统中加入了运行时权限功能。用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中再对某一项权限申请进行授权。

Android将所有的权限归成了两类:

  • 普通权限:指那些不会直接威胁到用户的安全和隐私的权限,对于这部分权限申请,系统会自动帮用户进行授权,而不需要用户再去手动操作。
  • 危险权限:指那些可能会触及用户隐私或者对设备安全性造成影响的权限,对于这部分权限申请,必须要由用户手动点击授权才可以,否则
    程序就无法使用相应的功能。

这样做可以避免用户繁琐的授权。

Android中有一共有上百种权限,危险权限总共就几个,除了危险权限之外,剩余的就都是普通权限。Android中所有的危险权限,一共是9组24个权限,如下图所示
Android危险权限
用户一旦同意授权了,那么该权限所对应的权限组中所有的其他权限也会同时被授权。每当要使用一个权限时,可以先到这张表中来查一下,如果是属于这张表中的权限,那么就需要进行运行时权限处理,如果不在这张表中,那么只需要在AndroidManifest.xml文件中添加一下权限声明就可以。

运行时申请权限

运行时权限的核心就是在程序运行过程中由用户授权去执行某些危险操作,程序不可以擅自做主去执行这些危险操作的。第一步就是要先判断用户是不是已经授权了,通过ContextCompat.checkSelfPermission()方法实现。

public static int checkSelfPermission(@NonNull Context context, @NonNull String permission) 

checkSelfPermission() 方法接收两个参数:

  • Context context:context对象(Activity等)
  • String permission:具体的权限名(打电话的权限:Manifest.permission.CALL_PHONE)

通过checkSelfPermission()方法的返回值与PackageManager.PERMISSION_GRANTED 做比较,相等就说明用户已经授权,不等就表示
用户没有授权。

如果没有授权的话,则需要调用ActivityCompat.requestPermissions()方法来向用户申请授权。

public static void requestPermissions(final @NonNull Activity activity, final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode)

requestPermissions()方法接收3个参数:

  • Activity activity:Activity的实例
  • String[] permissions:要申请的权限名放在数组中
  • int requestCode:请求码,要求是唯一值

调用完了requestPermissions()方法之后,系统会弹出一个权限申请的对话框,然后用户可以选择同意或拒绝程序的权限申请,不论是哪种结果,最终都会回调到onRequestPermissionsResult()方法中,而授权的结果则会封装在grantResults参数当中。
相关代码如下:

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
            //没有授权,先执行授权操作
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS}, 1);
        } else {
            //若已经授权,执行其他操作
        }

        //回调方法
        @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
            //授权与否都会调用这个方法,用回调方法执行程序相应的逻辑
        }

若用户想回收赋予程序的权限,可以在设置中回收程序的权限。

Content Provider

Content Provider的用法一般有两种:

  • 使用现有的内容提供器来读取和操作相应程序中的数据;
  • 创建自己的内容提供器给程序的数据提供外部访问接口

一个应用程序通过内容提供器对其数据提供了外部访问接口,那么任何其他的应用程序就都可以对这部分数据进行访问

使用现有Content Provider

要访问内容提供器*享的数据,需要借助ContentResolver类,可以通过Context中的getContentResolver()**方法获取到该类的实例。
ContentResolver中提供了一系列的方法用于对数据进行CRUD操作:

  • insert():添加数据
  • update():更新数据
  • delete():删除数据
  • query():查询数据

这些操作与SQLiteDatabase类似。ContentResolver中的CRUD方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI。
内容URI给内容提供器中的数据建立了唯一标识符,它主要由两部分组成:

  • authority:用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名
  • path:用于对同一应用程序中不同的表做区分的,通常都会添加到authority的后面

除此之外,还需要在URI字符串前添加协议声明,使其能够被识别成内容URI,内容URI最标准的格式写法如下:

content://cn.chenjianlink.android.contactstest/table

在得到了内容URI字符串之后,需要将它解析成Uri对象才可以作为参数传入,调用Uri.parse()方法,就可以将内容URI字符串解析成Uri 对象:

Uri uri = Uri.parse("content://cn.chenjianlink.android.contactstest/table")

使用Uri 对象来查询表中的数据:

Cursor cursor = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);

query()方法中的参数如下表所示,与SQLiteDatabase类似:
query参数

查询完成后返回的是一个Cursor 对象,这时可以将数据从Cursor对象中逐个读取出来了。通过移动游标的位置来遍历Cursor 的所有行,然后再取出每一行中相应列的数据,相关代码如下:

if (cursor != null) {
    while (cursor.moveToNext()) {
        String column1 = cursor.getString(cursor.getColumnIndex("column1"));
        int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
    }
    cursor.close();
}

对Content Provider增加操作如下所示:

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

将待添加的数据组装到ContentValues中,然后调用ContentResolver的insert()方法,将Uri和ContentValues作为参数传入。

对Content Provider修改操作如下所示:

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

对Content Provider删除操作如下所示:

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

自定义Content Provider

过新建一个类去继承ContentProvider 的方式来创建一个自己的内容提供器。ContentProvider 类中有6个抽象方法,在使用子类继承它的时候,需要将这6个方法全部重写:

public class MyProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        return false;
    }
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[]
        selectionArgs, String sortOrder) {
        return null;
    }
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[]
        selectionArgs) {
        return 0;
    }
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }
    @Override
    public String getType(Uri uri) {
        return null;
    }
}
  • onCreate():初始化Content Provider的时候调用。返回true 表示内容提供器初始化成功,返回false 则表示失败。
  • query():从Content Provider中查询数据。使用uri 参数来确定查询哪张表,projection 参数用于确定查询哪些列,selection 和selectionArgs 参数用于约束查询哪些行,sortOrder 参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。
  • insert():向Content Provider中添加一条数据。使用uri 参数来确定要添加到的表,待添加的数据保存在values 参数中。添加完成后,返回一个用于表示这条新记录的URI
  • update():更新Content Provider中已有的数据。使用uri 参数来确定更新哪一张表中的数据,新数据保存在values 参数中,selection 和selectionArgs 参数用于约束更新哪些行,受影响的行数将作为返回值返回
  • delete():从Content Provider中删除数据。使用uri 参数来确定删除哪一张表中的数据,selection 和selectionArgs 参数用于约束删除哪些行,被删除的行数将作为返回值返回。
  • getType():根据传入的内容URI来返回相应的MIME类型。

每一个方法都会带有Uri 这个参数,这个参数也正是调用Content Resolver的增删改查方法时传递过来的。
一个标准的内容URI写法如下:

content://cn.chenjianlink.android.contactstest/table

可以在这个内容URI的后面加上一个id,表示调用方期望访问的是cn.chenjianlink.android.contactstest这个应用的table表中id为1的数据

content://cn.chenjianlink.android.contactstest/table/1

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

  • *:表示匹配任意长度的任意字符
  • #:表示匹配任意长度的数字

借助UriMatcher这个类就可以轻松地实现匹配内容URI的功能,UriMatcher中提供了一个addURI() 方法,这个方法接收3个参数:

public void addURI(String authority, String path, int code)

  • String authority
  • String path
  • int code:自定义代码

调用UriMatcher的match()方法时,就可以将一个Uri 对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,就可以判断出调用方期望访问的是哪张表中的数据了。

对于getType()方法。它是所有的内容提供器都必须提供的一个方法,用于获取Uri 对象所对应的MIME类型。一个内容URI所对应的MIME字符串主要由3部分组成,Android对这3个部分做了如下格式规定:

  • 必须以vnd 开头
  • 如果内容URI以路径结尾,则后接android.cursor.dir/ ,如果内容URI以id结尾,则后接android.cursor.item/
  • 最后接上vnd.<authority>.<path>

对于content://cn.chenjianlink.android.contactstest/table 这个内容URI,它所对应的MIME类型就可以写成:

vnd.android.cursor.dir/vnd.cn.chenjianlink.android.contactstest.table

对于content://cn.chenjianlink.android.contactstest/table/1 这个内容URI,它所对应的MIME类型就可以写成:

vnd.android.cursor.item/vnd.cn.chenjianlink.android.contactstest.table

自定义Content Resolver需要在AndroidManifest.xml文件中注册才可以使用,相关代码如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
            package="cn.chenjianlink.android.contactstest">
            <application
                        android:allowBackup="true"
                        android:icon="@mipmap/ic_launcher"
                        android:label="@string/app_name"
                        android:supportsRtl="true"
                        android:theme="@style/AppTheme">
                        ...
                        <provider
                                android:name=".MyProvider"
                                android:authorities="cn.chenjianlink.android.contactstest"
                                android:enabled="true"
                                android:exported="true">
                        </provider>
            </application>
</manifest>

在<application>标签中添加<provider>标签,<provider>标签属性如下:

  • android:name:指定MyProvider的类名
  • android:authorities:指定MyProvider的authority
  • android:enabled:表示是否启用这个内容提供器
  • android:exported:表示是否允许外部程序访问自定义的内容提供器