Android四大组件之内容提供者Content Provider总结(《第一行代码》第二版):
0.内容提供者简介
内容提供者(Content Provider)主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。目前,使用内容提供者是Android实现跨程序共享数据的标准方式。
不同于文件存储和SharedPreferences存储中的两种全局可读可写操作模式,内容提供者可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会泄露的风险。
不过在在正式开始学习内容提供者之前,我们需要先掌握另一个非常重要的知识————Android运行时权限,因为待会的内容提供者示例中会使用到运行时权限的功能。当然不光是内容提供者,以后我们的开发过程中也会经常使用到运行时权限,因此你必须能够牢牢掌握它才行。
1.运行时权限
1.1 Android当中的权限简介
Android的权限机制并不是什么新鲜事务,从系统的第一个版本开始就已经存在了。但其实之前Android的权限机制在保护用户安全和隐私方面起到的作用比较有限,尤其是一些大家都离不开的常用软件,非常容易“店大欺客”。为此mAndroid开发团队在Android6.0系统中引用了运行时权限这个功能,从而更好地保护了用户的安全和隐私。添加权限声明是在项目下AndroidManifest.xml文件中添加,如果你有设计到权限的代码出现,一定要在AndroidManifest.xml文件中进行声明,否则程序就会崩溃。
那么问题来了,加入权限声明后,对于用户而言怎样保护他们设备的安全呢?其实用户主要在一下两个方面进行了保护:
- 1.当用户Android设备系统版本小于6.0,如果用户安装程序时,就会在安装界面出现该应用程序出现的各种权限的声明信息,用户可以直观看到,从而决定是否安装这个程序。
- 2.当用户Android设备系统版本小于6.0,另一方面,用户可以随时在应用程序管理界面查看任意一个程序的权限申请情况。这样应用程序的所有权限尽收眼底,什么都瞒不过用户的眼睛,以此保证应用程序会出现各种滥用权限的情况。
事实上,大部分用户根本懒得去看应用程序管理界面的权限申请情况,滥用权限依然存在,而且用户必须要用的应用程序出现“店大欺客”的现象,权限滥用让Android开发团队绞尽脑汁,于是在Android6.0中引入了运行时权限功能。也就是说,用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中再对某一项权限申请进行授权。比如说一款相机应用在运行时申请了地理位置定位权限,就算我拒绝了这个权限,但是我应该仍然可以使用这个应用的其他功能,而不是像之前那样直接无法安装它。
当然,并不是所有的权限都需要在运行时申请,对于用户来说,不停地授权也很烦。Android现在将所有的权限归成了两类,一类是普通权限,一类是危险权限。准确地讲,其实还有第三类特殊权限,不过这种权限使用得很少,因此不在本书得讨论范围之内。普通权限指的是那些不会直接威胁到用户得安全和隐私的权限,对于这部分权限申请,系统会自动帮我们进行授权,而不需要用户再去手动操作了。危险权限则表示那些可能触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息,定位设备的地理位置信息等,对于这部分权限申请,必须由用户手动点击授权才可以,否则程序就无法使用相应的功能。
但是Android中一共有上百种权限,我们怎么从中区分哪些是普通权限,哪些是危险权限呢?其实并没有那么难,因为危险权限总共那么几个,处理危险权限之外,剩余的就都是普通权限了。下表列出了Android中所有的危险权限,一共9组24个权限。
访问http://developer.android.google.cn/reference/android/Manifest.permission.html可以查看Android系统中完整的权限列表。
1.2 在程序中运行时权限的申请
比如你要进行拨打电话功能代码的编写,那么你肯定首先需要在AndroidManifest中添加权限申明:
<uses-permission android:name="android.permission.CALL_PHONE" />
然后就是代码部分了:
if(ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission.CALL_PHONE) !=
PackageManager.PERMISSION_CRANTED){
//执行这里说明没有获取android.permission.CALL_PHONE权限,需要获取
ActivityCompat.requestPermission(MainActivity.this,new String[]{Manifest.permission.CALL_PHONE},1);//这里进行权限的申请,将会运行MainActivity重写的方法onRequestPermissionResult
}else{
//执行到这里说明android.permission.CALL_PHONE权限已经获取了,下面就是实现权限相关的逻辑
try{
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}catch(SecurityException e){
e.printStackTrace();
}
}
@Override
public void onRequestPermissionResult(int requestCode,String[] permission,int[] grantResults){
switch(requestCode){
case 1:
if(grantResult.length > 0 && grantResults[0] == packageManager.PERMISSION_GRANTED){
//用户同意了权限的申请,下面就是实现权限相关的逻辑
try{
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}catch(SecurityException e){
e.printStackTrace();
}
}else{
//用户没有同意权限的申请,提示用户:没有同意该权限的申请
Toast.makeTest(this,"您没有同意拨打电话权限的申请!",Toast.LENGTH_SHORT).show();
}
}
}
第一步是判断用户有没有给过android.permission.CALL_PHONE权限,使用ContextCompat.checkSelfPermission()方法进行判断,此方法第一个参数接收一个Context,上下文嘛很好理解,第二个参数接收的是具体的权限名,然后使用ContextCompat.checkSelfPermission()方法的返回值与
PackageManager.PERMISSION_CRANTED作比较,相等则说明用户已经给了权限了,接下俩就执行此权限相关的逻辑了,如果不想等,那么说明用户没有给过此权限,接下来就需要进行申请,如何申请呢?需要调用ActivityCompat类的requestPermission()方法进行申请,requestPermission()方法接收三个参数,第一个参数要求是Activity的实例,第二个参数是一个String[]数组,我们要把申请的权限名放在数组中即可,第三个参数是请求码,只需要一个int值即可。
调用完了ActivityCompat类的requestPermission()方法,系统就会弹出一个权限申请的对话框,然后用户选择同意或拒绝我们的权限申请,不论是哪种结果,最终都会回调到onRequestPermissionResult()方法中,而授权的结果会封装在grantResults参数当中。这里我们只需要判断一下最后的授权结果,如果用户同意的话就执行此权限相关的逻辑,如果用户拒绝的话,就放弃操作,并且吐丝提示用户“您没有同意该权限的申请!”。
2.通过内容提供者访问其他程序的数据
内容提供者的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供者来给我们的程序提供外部访问接口。如果一个程序通过内容提供者对其数据提供外部访问接口,那么任何其他的应用程序就都可以对这部分数据进行访问。Android系统中自带的电话簿,短信,媒体库等程序都提供了类似的访问接口,这就使得第三方应用程序可以充分利用这部分数据来实现更好的功能。下面我们就来看看如何通过内容提供者访问其他程序的数据:
2.1 ContentResolver的基本用法
想要访问内容提供者中共享的数据,就一定要借助CotentResolver类,可以通过Context中的getContentResolver()方法获取该类的实例。ContentResolver中提供了一系列的方法用于对数据进行CRUD(增删改查)操作,其中insert()方法用于添加数据,update()方法用于数据的更新,delete()方法用于数据的删除,query()方法用于数据的查询。这好像SQLite数据库操作有木有?
不同于SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri的参数代替,这个参数被称作内容URI。内容URI给内容提供者中的数据建立了唯一的标识符,它主要由两部分组成:authority和path。authority是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名。比如某个程序的包名为com.example.app,那么该程序对应的authority就可以命名为com.example.app.provider。path则是用于对同一应用程序中不同的表做区分的,通常都会添加到authority的后面。比如某个程序的数据库里存在两张表:table1和table2,这时就可以将path分别命名为/table1和/table2,然后把authority和path进行组合,内容的URI就变成了com.example.app.provider/table1和com.example.app.provider/table2。不过目前还是很难辨认出这两个字符串就是两个内容URI,我们还需要在字符串的头部加上协议声明。因此,内容URI最标准的格式写法如下:
content://com.example.app.provider/table1
content://com.example.app.provider/table2
在得到内容URI字符串之后,我们还需要将它解析成Uri对象才可以作为参数传入。解析的方法也相当简单,代码如下所示:
Uri uri = new Uri.parse("content://com.example.app.provider/table1");
只需要调用Uri的静态方法parse()就可以把内容URI字符串解析成URI对象。
现在,我们可以通过这个Uri对象来查询table1表中的数据了。代码如下所示:
Cursor cursor = getContentResolver()
.query(
uri,projection,selection,selectionArgs,sortOrder
);
query()方法接收的参数跟SQLiteDatabase中的query()方法接收的参数很像,但总体来说这个稍微简单一些,毕竟这是在访问其他程序中的数据,没必要构建复杂的查询语句。下标对内容提供者中的query的接收的参数进行了详细的解释:
查询完成仍然会返回一个Cursor对象,这时我们就可以将数据从Cursor对象中逐个读取出来了。读取的思路仍然是对这个Cursor对象进行遍历,然后一条一条的取出数据即可,代码如下:
if(cursor != null){//注意这里一定要进行一次判空,因为有可能你要查询的表根本不存在
while(cursor.moveToNext()){
String column1 = cursor.getString(cursor.getColumnIndex("column1"));
int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
}
}
查询都会了,那么剩下的增加,删除,修改自然也不在话下了,代码如下所示:
//增加数据
ContentValues values = new ContentValues();
values.put("Column1","text");
values.put("Column2","1");
getContextResolver.insert(uri,values);
//删除数据
getContextResolver.delete(uri,"column2 = ?",new String[]{ "1" });
//更新数据
ContentValues values = new ContentValues();
values.put("Column1","改数据");
getContextResolver.update(uri,values,"column1 = ? and column2 = ?",new String[]{"text","1"});
最后我们来看看一些常用的内容提供者的Uri,看下面表格:
3.创建自己的内容提供者
前面已经提到过,如果要想实现跨程序共享数据的功能,官方推荐的方式就是使用内容提供器,可以新建一个类去继承ContentProvider类的方式来创建一个自己的内容提供器。ContentProvider类有6个抽象方法,我们在使用子类继承它的时候,需要将这6个方法全部重写。新建MyProvider继承字ContentProvider类,代码如下所示:
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;
}
}
在这6个方法中,相信出来增删改查的方法你知道之外,剩下两个方法你可能不知道,下面就对这些方法进行一一介绍:
1.onCreate()方法:
初始化内容提供器的时候调用。通常会在这里完成对数据库的创建和升级等操作。返回true表示内容提供器初始化成功,返回false则表示失败。注意,只有当存在ContentResolver尝试访问我们的程序中的数据时,内容提供器才会被初始化。
2.query()方法:
从内容提供器中查询数据。使用uri参数来确定查询的哪张表,projection参数用于确定查询的哪一列,selection和selectionArgs参数用于约束查询哪些行,sortOrder参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。
3.insert()方法:
向内容提供器中添加一条数据。使用uri参数来确定要添加的表,待添加的数据保存在values参数中。添加完成后,返回一个用于表示这条新纪录的URI。
4.update()方法:
更新内容提供器中已有的数据。使用uri参数来确定更新哪一张表中的数据,新数据保存着values参数当中,selection和selectionArgs参数用于约束更新哪些行,受影响的行数将作为返回值返回。
5.delete()方法:
从内容提供器中删除数据。使用uri参数来确定删除哪一张表中的数据,selection和selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。
6.getType()方法:
根据传入的内容URI来返回相应的MIME类型。
可以看到,几乎每一个方法都会带有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/*
而一个能够匹配table表中任意一行数据的内容URI格式就可以写成:
content://com.example.app.provider/table1/#
接着,我们再借助UriMatcher这个类就可以轻松地实现匹配内容URI的功能。UriMatcher中提供了一个addURI()方法,这个方法接收三个参数,可以分别把authority,path和一个自定义代码传进去,这个自定义代码其实就是一个final的int类型的具值。这样,当调用UriMatcher的match()方法时,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是哪张表中的数据了。修改上述的MyProvider代码如下所示:
public class MyProvider extends ContentProvider {
public static fianl int TABLE1_DIR = 0;
public static fianl int TABLE1_ITEM = 1;
public static fianl int TABLE2_DIR = 2;
public static fianl int TABLE2_ITEM = 3;
private static UriMatcher uriMatcher;
static{
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("com.example.app.provider","table1",TABLE1_DIR);
uriMatcher.addURI("com.example.app.provider","table1/#",TABLE1_ITEM);
uriMatcher.addURI("com.example.app.provider","table2",TABLE2_DIR);
uriMatcher.addURI("com.example.app.provider","table2/#",TABLE2_ITEM);
}
...
@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;
}
return null;
}//查询
...
}
上述代码只是以query()方法为例做了个示范,其实insert()方法,update(),delete()方法的实现也是跟query()方法是差不多的,它们都会携带Uri这个参数,然后同样利用UriMatcher的match()方法判断出调用方期望访问的是哪张表,再对该表中的数据进行相应的操作就可以了。
除此之外,还有个方法你比较陌生,这个方法就是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,它所对应的MIME类型就可以写成:
vnd.android.cursor.item/vnd.com.example.app.provider.table1
现在,我们可以继续完善MyProvider类中的内容,这次实现getType()方法的逻辑,代码如下:
public class MyProvider extends ContentProvider{
...
@Override
public String getType(Uri uri){
switch(uriMatcher.match(uri)){
case TABLE1_DIR:
return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1"
break;
case TABLE1_ITEM:
return "vnd.android.cursor.item/vnd.com.example.app.provider.table1 "
break;
case TABLE2_DIR:
return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2"
break;
case TABLE2_ITEM:
return "vnd.android.cursor.item/vnd.com.example.app.provider.table2 "
break;
}
}
...
}
到这里,一个完整的内容提供器就创建完成了,现在任何一个应用程序都可以使用
ContentResolver 来访问我们程序中的数据。那么前面所提到的,如何才能保证隐私数据不会泄漏出去呢?其实多亏了内容提供器的良好机制,这个问题在不知不觉中已经被解决了。因为所有的 CRUD 操作都一定要匹配到相应的内容 URI 格式才能进行的,而我们当然不可能向UriMatcher中添加隐私数据的URI,所以这部分数据根本无法被外部程序访问到,安全问题也就不存在了。
好了,创建内容提供器的步骤你也已经清楚了,下面就来实战一下,真正体验一回跨程
序数据共享的功能。
还没有结束呢?我们都知道4大组件都需要在AndroidManifest.xml文件中进行注册,既然完成的内容提供器写好了,那么下一步就是去AndroidManifest.xml文件中进行注册,然后这个内容提供器就可以使用了。我们就来拿一个例子进行讲解,一个标准的内容提供器的封装代码如下所示:
public class DatabaseProvider extends ContentProvider {
public static final int BOOK_DIR = 0;
public static final int BOOK_ITEM = 1;
public static final int CATEGORY_DIR = 2;
public static final int CATEGORY_ITEM = 3;
public static final String AUTHORITY = "com.example.databasetest.provider";
private static UriMatcher uriMatcher;
private MyDatabaseHelper dbHelper;//内容提供器数据库支持
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/#", CATEGORY_ITEM);
}
@Override
public boolean onCreate() {
dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);//创建内容提供器要使用的数据库
return true;//这里一定返回true,不然内容提供器无法被使用
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// 查询数据
SQLiteDatabase db = dbHelper.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 CATEGORY_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 Uri insert(Uri uri, ContentValues values) {
// 添加数据
SQLiteDatabase db = dbHelper.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 CATEGORY_ITEM:
long newCategoryId = db.insert("Category", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" +
newCategoryId);
break;
default:
break;
}
return uriReturn;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// 更新数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
int updatedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
updatedRows = db.update("Book", values, selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
updatedRows = db.update("Book", values, "id = ?", new String[]
{ bookId });
break;
case CATEGORY_DIR:
updatedRows = db.update("Category", values, selection,
selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
updatedRows = db.update("Category", values, "id = ?", new String[]
{ categoryId });
break;
default:
break;
}
return updatedRows;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// 删除数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
int deletedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
deletedRows = db.delete("Book", selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
deletedRows = db.delete("Book", "id = ?", new String[] { bookId });
break;
case CATEGORY_DIR:
deletedRows = db.delete("Category", selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
deletedRows = db.delete("Category", "id = ?", new String[]
{ categoryId });
break;
default:
break;
}
return deletedRows;
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.
provider.book";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest.
provider.book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.
provider.category";
case CATEGORY_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest.
provider.category";
}
return null;
}
}
AndroidManifest.xml文件中对内容提供器进行注册,标签为< provider >…< /provider >
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.databasetest"
android:versionCode="1"
android:versionName="1.0" >
……
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
……
<provider
android:name="com.example.databasetest.DatabaseProvider"
android:authorities="com.example.databasetest.provider" >
</provider>
</application>
</manifest>
这样一个完整的内容提供器的创建流程就讲完了。