探究跨程序数据共享的关键途径————内容提供器
上文我们总结了安卓数据持久化存储的几种方案,不难发现,它们只能为当前应用程序提供数据存储及访问,而其他应用程序无法获取到当前应用程序存储的数据。即使是SharedPreferences
提供的MODE_WORLD_READABLE
和MODE_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 在程序运行时申请权限
既然运行时权限无需在安装软件时一次性授予全部所需权限,那么在用到需要授权的功能时,肯定就要用户授权了。
现在先看一个拨号的实例。
在按钮的点击触发监听中添加一个拨号至10086 的Intent
:
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" />
但是当我们拨号时,发现手机什么反应都没有。
控制台日志
观察控制台输出日志,发现是没有获取到拨号权。
这便是由于没有获取到运行时权限。
修改拨号按钮监听器事件,通过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类
,可以通过Context
的getContentResolver()
方法获取到其实例。类似于SQLiteDatabase,ContentResolver类
也是通过insert()
、update()
、delete()
、query()
方法进行CRUD操作的。
与SQLiteDatabase
不同的是,ContentResolver类
不是通过表名参数来连接数据库的,而是通过“内容URI
”来确定唯一标识符。内容URI是由authority
和path
两部分组成,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
相似,具体解释如下:
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_NAME
和ContactsContract.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_DIR
、BOOK_ITEM
、BOOK_DIR
、BOOK_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部分组成:
然后重写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 );
});
在按钮的触发事件中,通过向getContentResolver
的insert()
、update()
、delete()
、query()
等方法传入内容URI和参数列表,即可通过内容提供器访问和修改数据。