之前博文《Android学习笔记之——Content Providers 》已经介绍了运行时权限以及在自己的程序中访问其他应用程序的数据。只需要获取到该应用程序的内容URI,然后借助ContentResolver进行CRUD操作就可以了。本博文来学习一下如何创建Content Providers (内容提供器)
目录
创建内容提供器
如果想要实现跨程序共享数据的功能,官方推荐的方式就是使用内容提供器,可以通过新建一个类去继承ContentProvider 的方式来创建一个自己的内容提供器。ContentProvider 类中有6个抽象方法,我们在使用子类继承它的时候,需要将这6个方法全部重写。新建MyProvider 继承自ContentProvider ,代码如下所示:
public class MyProvider extends ContentProvider {
//初始化内容提供器的时候调用。通常会在这里完成对数据库的创建和升级等操作,
//返回true 表示内容提供器初始化成功,返回false 则表示失败。
@Override
public boolean onCreate() {
return false;
}
//从内容提供器中查询数据。
//使用uri 参数来确定查询哪张表,
//projection 参数用于确定查询哪些列
//selection 和selectionArgs 参数用于约束查询哪些行,
//sortOrder 参数用于对结果进行排序,
//查询的结果存放在Cursor 对象中返回。
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[]
selectionArgs, String sortOrder) {
return null;
}
//向内容提供器中添加一条数据。
//使用uri 参数来确定要添加到的表,
//待添加的数据保存在values 参数中。
//添加完成后,返回一个用于表示这条新记录的URI。
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
//更新内容提供器中已有的数据。
//使用uri 参数来确定更新哪一张表中的数据,
//新数据保存在values 参数中,
//selection 和selectionArgs 参数用于约束更新哪些行
//受影响的行数将作为返回值返回。
@Override
public int update(Uri uri, ContentValues values, String selection, String[]
selectionArgs) {
return 0;
}
//从内容提供器中删除数据。
//使用uri 参数来确定删除哪一张表中的数据,
//selection和selectionArgs 参数用于约束删除哪些行,被删除的行数将作为返回值返回。
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
//根据传入的内容URI来返回相应的MIME类型。
@Override
public String getType(Uri uri) {
return null;
}
}
Uri 参数
通过上面,我们可以看到,几乎每一个方法都会带有Uri 这个参数,这个参数也正是调用ContentResolver的增删改查方法时传递过来的。通过对传入的Uri 参数进行解析,从中分析出调用方期望访问的表和数据。
一个标准的内容URI写法是这样的:
content://com.example.app.provider/table1
这就表示调用方期望访问的是com.example.app这个应用的table1表中的数据。除此之外,还可以在这个内容URI的后面加上一个id,如下所示:
content://com.example.app.provider/table1/1
这就表示调用方期望访问的是com.example.app这个应用的table1表中id为1的数据
内容URI的格式主要就只有以上两种,以路径结尾就表示期望访问该表中所有的数据,以id结尾就表示期望访问该表中拥有相应id的数据。我们可以使用通配符的方式来分别匹配这两种格式的内容URI,规则如下。
- *:表示匹配任意长度的任意字符。
- #:表示匹配任意长度的数字。
所以,一个能够匹配任意表的内容URI格式就可以写成:
content://com.example.app.provider/*
而一个能够匹配table1表中任意一行数据的内容URI格式就可以写成:
content://com.example.app.provider/table1/#
UriMatcher类匹配内容URI
接着,再借助UriMatcher这个类就可以轻松地实现匹配内容URI的功能。UriMatcher中提供了一个addURI() 方法,这个方法接收3个参数,可以分别把authority 、path 和一个自定义代码传进去。这样,当调用UriMatcher的match() 方法时,就可以将一个Uri 对象传入,返回值是某个能够匹配这个Uri 对象所对应的自定义代码,利用这个代码,就可以判断出调用方期望访问的是哪张表中的数据了。如下所示:
public class MyProvider extends ContentProvider {
public static final int TABLE1_DIR = 0;
public static final int TABLE1_ITEM = 1;
public static final int TABLE2_DIR = 2;
public static final int TABLE2_ITEM = 3;
private static UriMatcher uriMatcher;
//创建一个静态的代码块
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);、、创建UriMatcher的实例
//调用addURI() 方法,将期望匹配的内容URI格式传递进去,
uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_DIR);//TABLE1_DIR表示访问table1表中的所有数据
uriMatcher.addURI("com.example.app.provider ", "table1/#", TABLE1_ITEM);//TABLE1_ITEM 表示访问table1表中的单条数据
uriMatcher.addURI("com.example.app.provider ", "table2", TABLE2_DIR);//TABLE2_DIR表示访问table2表中的所有数据
uriMatcher.addURI("com.example.app.provider ", "table2/#", TABLE2_ITEM);//TABLE2_ITEM 表示访问table2表中的单条数据
}
...
//当query方法被调用时,会通过uriMatcher的match()方法对传入的Uri对象进行匹配
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[]
selectionArgs, String sortOrder) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:
// 查询table1表中的所有数据
break;
case TABLE1_ITEM:
// 查询table1表中的单条数据
break;
case TABLE2_DIR:
// 查询table2表中的所有数据
break;
case TABLE2_ITEM:
// 查询table2表中的单条数据
break;
default:
break;
}
...
}
...
}
通过UriMatcher类匹配了对应的URI内容,然后调用相关的方法
getType() 方法
getType() 方法是所有的内容提供器都必须提供的一个方法,用于获取Uri 对象所对应的MIME类型。一个内容URI所对应的MIME字符串主要由3部分组成,Android对这3个部分做了如下格式规定。
- 必须以vnd 开头。
- 如果内容URI以路径结尾,则后接android.cursor.dir/ ,如果内容URI以id结尾,则后接android.cursor.item/ 。
- 最后接上vnd.<authority>.<path> 。
所以,对于content://com.example.app.provider/table1这个内容URI(以路径结尾),它所对应的MIME类型就可以写成:
vnd.android.cursor.dir/vnd.com.example.app.provider.table1
对于content://com.example.app.provider/table1/1这个内容URI(以id结尾),它所对应的MIME类型就可以写成:
vnd.android.cursor.item/vnd.com.example.app.provider.table1
接下来看看实现getType() 方法中的逻辑:
public class MyProvider extends ContentProvider {
...
//创建一个静态的代码块
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);、、创建UriMatcher的实例
//调用addURI() 方法,将期望匹配的内容URI格式传递进去,
uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_DIR);//TABLE1_DIR表示访问table1表中的所有数据
uriMatcher.addURI("com.example.app.provider ", "table1/#", TABLE1_ITEM);//TABLE1_ITEM 表示访问table1表中的单条数据
uriMatcher.addURI("com.example.app.provider ", "table2", TABLE2_DIR);//TABLE2_DIR表示访问table2表中的所有数据
uriMatcher.addURI("com.example.app.provider ", "table2/#", TABLE2_ITEM);//TABLE2_ITEM 表示访问table2表中的单条数据
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:
return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1";
case TABLE1_ITEM:
return "vnd.android.cursor.item/vnd.com.example.app.provider.table1";
case TABLE2_DIR:
return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2";
case TABLE2_ITEM:
return "vnd.android.cursor.item/vnd.com.example.app.provider.table2";
default:
break;
}
return null;
}
}
Demo
如何才能保证隐私数据不会泄漏出去呢?其实多亏了内容提供器的良好机制,这个问题在不知不觉中已经被解决了。因为所有的CRUD操作都一定要匹配到相应的内容URI格式才能进行的,而我们当然不可能向UriMatcher中添加隐私数据的URI,所以这部分数据根本无法被外部程序访问到,安全问题也就不存在了。
创建一个DatabaseTest项目。然后创建一个内容提供器。右击com.example.databasetest包→New→Other→Content Provider。将内容提供器命名为DatabaseProvider,authority 指定为com.example.databasetest.provider ,Exported 属性表示是否允许外部程序访问我们的内容提供器,Enabled 属性表示是否启用这个内容提供器。将两个属性都勾中,点击Finish完成创建。
接着修改DatabaseProvider中的代码,如下所示:
其实我真的比较讨厌这种说什么基于上一节的一个大项目,然后这里改一点那里改一点,《第一行代码》这本书在这里就是这样,而由于对于书中的第六章我没有深入的理解,所以这里也没法把demo实现出来,后面会对这部分进行补充。