Android开发 | 探究内容提供器——Content Provider

探究跨程序数据共享的关键途径————内容提供器

探究跨程序数据共享的关键途径————内容提供器

上文我们总结了安卓数据持久化存储的几种方案,不难发现,它们只能为当前应用程序提供数据存储及访问,而其他应用程序无法获取到当前应用程序存储的数据。即使是SharedPreferences提供的MODE_WORLD_READABLEMODE_WORLD_WRITEABLE两种供外部应用访问数据的操作方式,也在Android 4.2的时候就被Google遗弃了。

那么当外部程序需要访问当前应用程序的数据时,怎么实现数据共享呢?比如微信添加朋友时,需要获取手机通讯录里的联系人信息,这部分数据当然是存储在数据库中的,但如果仅仅依靠上文的持久化存储方案,我们就不能完美且安全地处理这一问题。

这时就需要内容提供器Content Provider)大显身手了。它不仅保证了访问外部程序数据的安全性,也能够指定外部程序对给定的数据进行共享。

一、运行时权限

1.1 关于运行时权限

在了解内容提供器之前,先需要知道什么是“运行时权限”。从安卓2.3开始使用安卓机的朋友一定知道,在Android 2.3 ~ Android 5.0期间,安装应用时会列出一大堆当前所安装的应用的所需权限,并且你只能点击“确定”(不能指定授予哪些权限),否则应用就不会被安装。但是从Android 6.0开始,安装应用时,软件包安装器就没有提示该应用的所要用到的权限信息,取而代之的是,当首次进入软件(请求联网、请求读写存储设备)或是使用到某个需要权限的功能时(相机、麦克风),软件才会弹出对话框提示用户是否授权,这就是运行时权限。用户即使拒绝了某一个不是特别关键的权限请求,也不会影响到该软件绝大部分功能的正常工作。相比之下,Android 6.0之前不给予全部权限软件都无法安装,显得就有些“店大欺客”了。

1.2 在程序运行时申请权限

既然运行时权限无需在安装软件时一次性授予全部所需权限,那么在用到需要授权的功能时,肯定就要用户授权了。

现在先看一个拨号的实例。

在按钮的点击触发监听中添加一个拨号至10086Intent

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
EditText editNum = (EditText) findViewById(R.id.editNumber);
Button btn_call = (Button) findViewById(R.id.btn_call);
btn_call.setOnClickListener(v -> {
        call(editNum.getText().toString());
});

private void call(String phoneNumber) {
    try {
        Intent intent = new Intent(Intent.ACTION_CALL);
        intent.setData(Uri.parse("tel:" + phoneNumber));
        startActivity(intent);
    } catch (SecurityException e) {
        e.printStackTrace();
    }
}

然后在清单文件中声明拨号权限:

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

但是当我们拨号时,发现手机什么反应都没有。

/androiddev-charpter7-content-provider/log.webp
控制台日志

观察控制台输出日志,发现是没有获取到拨号权。

这便是由于没有获取到运行时权限。

修改拨号按钮监听器事件,通过checkSelfPermission()方法添加对当前上下文的权限取得情况的判断,若拨号权限未获取到,则通过requestPermission()方法请求用户授权;否则调用拨号方法。

1
2
3
4
5
6
7
btn_call.setOnClickListener(v -> {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CALL_PHONE}, 1);
    } else {
        call(editNum.getText().toString());
    }
);

当拨号权限已经被授予时,点击CALL按钮后将跳转到拨号界面;当用户还未授予拨号权限时,则会弹出授权对话框,但不论用户在对话框中授予权限与否,最终都会调用onRequestPermission()方法,笔者重写该方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    switch (requestCode) {
        case 1:
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                call(editNum.getText().toString());
            } else {
                Log.d("onRequestPermission", "Permission denied");
            }
    }
}

在该方法中,通过请求id分辨请求的权限,对于之前请求id为“1”的权限请求,如果用户授权了则调用拨号,否则控制台输出“拒绝授权”的日志。

以上检查权限并请求授权、授权结果处理的整个流程,构成了运行时权限请求的基本过程。

二、跨程序数据访问——ContentResolver

对于应用程序而言,访问内容提供器ContentProvider中的内容需要通过ContentResolver类,可以通过ContextgetContentResolver()方法获取到其实例。类似于SQLiteDatabase,ContentResolver类也是通过insert()update()delete()query()方法进行CRUD操作的。

SQLiteDatabase不同的是,ContentResolver类不是通过表名参数来连接数据库的,而是通过“内容URI”来确定唯一标识符。内容URI是由authoritypath两部分组成,authority一般用于区分不同应用程序之间的数据,而path则用于对同一应用程序之中的不同数据库进行区分。

内容URI的一般写法如下:

content://com.cosyspark.testcontentresolver/table123

其中,“/”前面为authority,后面为path

通过URI.parse()方法,即可将内容URI字符串转化为URI对象

1
URI uri = Uri.parse("content://com.cosyspark.testcontentprovider/table123")

2.1 查找数据

利用该URI对象,便可对table123表中数据进行查找:

1
2
3
4
5
6
Cursor cursor = getContentResolver().query(
  uri,
  projection,
  selection,
  selectionArgs,
  sortOrder);

该方法参数与SQLiteDatabase相似,具体解释如下:

/androiddev-charpter7-content-provider/query.webp
query()方法

返回cursor对象后,对游标的操作与SQLiteDatabase相同,示例见后文。

2.2 插入数据

1
2
3
4
ContentValues values = new ContentValues();
values.put("column1", "text it");
values.put("column2", "text us");
getContentResolver(uri,values);

2.3 更新数据

利用update()方法,将“column1”和“column2”属性为“text it”和“test us”的“domain”列修改为“cosyspark.space”:

1
2
3
ContentValues values = new ContentValues();
values.put("domain", "cosyspark.space");
getContentResolver().update(uri,values,"column1 = ? and column2 = ?", new String[]{"text it","text us"});

2.4 删除数据

1
getContentValues.delete(uri, "column1 = ?", new String[]{"text it"});

2.5 综合示例——读取通讯录联系人

利用Android系统提供的内容URI:ContactsContract.CommonDataKinds.Phone.CONTENT_URI‍作为query()方法的URI参数,置空约束条件,即可获取全部联系人的URI实例。

再通过游标的getString()方法遍历表,获取联系人名称和联系电话(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAMEContactsContract.CommonDataKinds.Phone.NUMBER分别是联系人姓名联系人电话的列属性常量),最后添加到ListView的适配器队列中,通知刷新ListView,并关闭游标对象,实现系统通讯录联系人列表的获取:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
private void readContacts() {
    Cursor cursor = null;
    try {
        cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
        if (cursor != null) {
            while (cursor.moveToNext()) {
                @SuppressLint("Range") String name = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                @SuppressLint("Range") String phone = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                contactsList.add(name + "\n" + phone);
            }
            editNum.setText("");
            adapter.notifyDataSetChanged();
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (cursor != null) {
            cursor.close();
        }
    }
}

当然不能忘记在清单文件中添加读取联系人的权限:

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

三、自定义内容提供器

3.1 重写ContentProvider类抽象方法

自定义内容提供器,首先要创建一个继承于ContentProvider类的自定义类,且必须重写ContentProvider类的六个抽象方法onCreate()query()insert()update()delete()getType()

其中,getType()方法用于根据传入的内容URI来指定返回的MIME类型

内容URI支持通配符

1
2
3
4
5
6
7
// *表示匹配任意长度的任意字符
// 一个匹配任意表的内容URI
content://com.cosyspark.app.provider/*

// #表示匹配任意长度的数字
// 一个匹配表table1内任意一行数据的内容URI
content://com.cosyspark.app.provider/table1/#

3.2 添加UriMatcher的自定义代码匹配

1
2
3
4
5
6
7
static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
        uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
        uriMatcher.addURI(AUTHORITY, "CATEGORY", CATEGORY_DIR);
        uriMatcher.addURI(AUTHORITY, "CATEGORY/#", CATEFORY_ITEM);
    }

此时如果调用uriMatcher.match(uri)便会返回以下结果之一:BOOK_DIRBOOK_ITEMBOOK_DIRBOOK_ITEM

此时重写query()方法时,便可根据UriMatcher返回的自定义代码来以不同约束查询表的不同内容,基本框架为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Override
public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
     ···        
     ···
          
switch (uriMatcher.match(uri)) {
     case BOOK_DIR:
         ···
     case BOOK_ITEM:
         ···
     case CATEGORY_DIR:
         ···
     case CATEFORY_ITEM:
         ···
     default:
         break;
    }
     ···
}

此外,getType()方法还需特别介绍。getType()方法用于获取内容URI对应的MIME类型,而MIME字符串主要由3部分组成:

  • vnd开头

  • 若内容URI以路径结尾,则vnd后接android.cursor.dir/,如果URI以i结尾接android.cursor.item/

  • 最后再追加vnd.<authority>.<path>

然后重写getType()方法,此时自定义内容提供器的步骤就完成了:

1
2
3
4
5
6
7
// 对于内容URI content://com.cosyspark.app.provider/table1/123
// 其IMME为
vnd.android.cursor.item/vnd.com.cosyspark.app.provider/table1

// 对于内容URI content://com.cosyspark.app.provider/table1
// 其IMME为
vnd.android.cursor.dir/vnd.com.cosyspark.app.provider/table1

四、用内容提供器实现数据的跨程序共享

在之前SQLite的Demo程序中新建DatabaseProvider类,重写内容提供器的6个抽象方法,另外再新建一个ProviderTest测试程序,通过内容提供器访问SQLite数据库。

首先定义好UriMatcher,以及添加UriMatcher的自定义代码匹配:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static final int BOOK_ITEM = 1;
public static final int CATEGORY_DIR = 2;
public static final int CATEFORY_ITEM = 3;
public static final String AUTHORITY = "com.cosyspark.sqlite.provider";
private static UriMatcher uriMatcher;
private MyDatabaseHelper myDbHelper;
static {
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
    uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
    uriMatcher.addURI(AUTHORITY, "CATEGORY", CATEGORY_DIR);
    uriMatcher.addURI(AUTHORITY, "CATEGORY/#", CATEFORY_ITEM);
}

然后重写6个抽象函数:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
    SQLiteDatabase db = myDbHelper.getWritableDatabase();
    int deleteRows = 0;
    switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
            deleteRows = db.delete("Book", "id=?", selectionArgs);
            break;
        case BOOK_ITEM:
            String bookId = uri.getPathSegments().get(1);
            deleteRows = db.delete("Book", "id=?", new String[]{bookId});
            break;
        case CATEGORY_DIR:
            deleteRows = db.delete("Category", "id=?", selectionArgs);
            break;
        case CATEFORY_ITEM:
            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) {
    switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
            return "vnd.android.cursor.dir/vnd.com.cosyspark.sqlite.privider.book";
        case BOOK_ITEM:
            return "vnd.android.cursor.item/vnd.com.cosyspark.sqlite.privider.book";
        case CATEGORY_DIR:
            return "vnd.android.cursor.dir/vnd.com.cosyspark.sqlite.provider.category";
        case CATEFORY_ITEM:
            return "vnd.android.cursor.item/vnd.com.cosyspark.sqlite.provider.category";
    }
    return null;
}

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

@Override
public boolean onCreate() {
    myDbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 1);
    return true;
}

@Override
public Cursor query(Uri uri, String[] projection, String selection,
                    String[] selectionArgs, String sortOrder) {
    SQLiteDatabase db = myDbHelper.getReadableDatabase();
    Cursor cursor = null;
    switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
            cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
            break;
        case BOOK_ITEM:
            String BookID = uri.getPathSegments().get(1);
            cursor = db.query("Book", projection, "id=?", new String[]{BookID}, null, null, sortOrder);
            break;
        case CATEGORY_DIR:
            cursor = db.query("CATEGORY", projection, selection, selectionArgs, null, null, sortOrder);
            break;
        case CATEFORY_ITEM:
            String CategoryID = uri.getPathSegments().get(1);
            cursor = db.query("CATEGORY", projection, "id=?", new String[]{CategoryID}, null, null, sortOrder);
            break;
        default:
            break;
    }
    return cursor;
}

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

最后在ProviderTest中测试内容提供器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
btn_add = findViewById(R.id.btn_add_data);
btn_add.setOnClickListener(v -> {
    Uri uri = Uri.parse("content://com.cosyspark.sqlite.provider/book");
    ContentValues values = new ContentValues();
    values.put("name", "A Clash of Kings");
    values.put("author", "George Martin");
    values.put("pages", 1040);
    values.put("price", 16.98);
    Uri newUri = getContentResolver().insert(uri, values);
    newId = newUri.getPathSegments().get(1);
});
btn_query = findViewById(R.id.btn_query_data);
btn_query.setOnClickListener(v -> {
    dataList.clear();
    Uri uri = Uri.parse("content://com.cosyspark.sqlite.provider/book");
    Cursor cursor = getContentResolver().query(uri, null, null, null, null);
    if (cursor != null) {
        while (cursor.moveToNext()) {
            @SuppressLint("Range") String name = cursor.getString(cursor.getColumnIndex("name"));
            @SuppressLint("Range") String auther = cursor.getString(cursor.getColumnIndex("author"));
            @SuppressLint("Range") int pages = cursor.getInt(cursor.getColumnIndex("pages"));
            @SuppressLint("Range") double price = cursor.getDouble(cursor.getColumnIndex("price"));
            dataList.add("Book name: 《" + name + "》\nAuther: " + auther + "\nPages: " + pages + "\nPrice: " + price);
        }
        adapter.notifyDataSetChanged();
    }
});
btn_update = findViewById(R.id.btn_update_data);
btn_update.setOnClickListener(v -> {
    Uri uri = Uri.parse("content://com.cosyspark.sqlite.provider/book/" + newId);
    ContentValues values = new ContentValues();
    values.put("name", "A Song of Ice and Fire");
    values.put("price", 42.99);
    values.put("pages", 2134);
    getContentResolver().insert(uri, values);
});
btn_delete = findViewById(R.id.btn_delete_data);
btn_delete.setOnClickListener(v -> {
    Uri uri = Uri.parse("content://com.cosyspark.sqlite.provider/book/" + newId);
    getContentResolver().delete(uri, null, null);
});

在按钮的触发事件中,通过向getContentResolverinsert()update()delete()query()等方法传入内容URI和参数列表,即可通过内容提供器访问和修改数据。

给作者倒杯卡布奇诺 ~
Albresky 支付宝支付宝
Albresky 微信微信