一、 内容提供器简介
1、内容提供器主要用于在不同的应用程序/进程之间
实现数据共享&交互
的功能,他提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性
2、内容提供器可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险
3、ContentProvider = 中间者角色(搬运工),真正存储&操作数据的数据源为原来存储数据的方式(数据库(sqlite)、文件、XML、网络等等)
4、ContentProvider一般为存储和获取数据提供统一的接口
,可以在不同的应用程序之间共享数据。
二、原理
内容提供器运用了Android中的Binder机制,该机制是一种Android中实现跨进程通信(IPC)的方式的机制
三、访问其他程序中的数据
1、内容提供器的用法一般有两种:
- 使用现有的内容提供器来
读取和操作
相应程序中的数据; -
创建自己的内容提供器
给我们的程序的数据提供外部访问的接口。
2、若一个应用程序通过内容提供器对其数据提供了外部访问接口,那么任何其他的应用程序就都可以对这部分数据进行访问,如电话簿、短信、媒体库等程序都提供了类似的访问接口。
3.1 常用工具类
3.1.1 ContentResolver类
1、通过借助ContentResolver类以访问内容提供器中共享的数据
2、ContentResolver还用于统一管理不同 ContentProvider间的操作
3、ContentResolver中提供了一系列的方法用于对数据进行CRUD操作
3.1.2 ContentUris类
1、作用:操作URI
2、具体使用
//withAppendedId()作用:向URI追加一个id
Uri uri = Uri.parse("content://cn.scu.myprovider/user")
Uri resultUri = ContentUris.withAppendedId(uri, 7);
// 最终生成后的Uri为:content://cn.scu.myprovider/user/7
// parseId()作用:从URL中获取ID
Uri uri = Uri.parse("content://cn.scu.myprovider/user/7")
long personid = ContentUris.parseId(uri);
//获取的结果为:7
3.1.3 UriMatcher类
作用:在ContentProvider中注册URI;根据URI匹配ContentProvider对应的数据表
3.1.4 ContentObserver类
1、内容观察者作用:观察 Uri 引起 ContentProvider 中的数据变化 & 通知外界
(即访问该数据访问者):当ContentProvider 中的数据发生变化(增、删 & 改)时,就会触发该 ContentObserver类通知数据变化
2、适用场景:需要频繁检测的数据库或者某个数据是否发生改变,如果使用线程去操作,很不经济而且很耗时 。
3.2 ContentResolver的基本用法
1、ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI
2、该URI又称为统一资源标识符(Uniform Resource Identifier),用于唯一标识 ContentProvider & 其中的数据
3、外界进程通过 URI 找到对应的ContentProvider & 其中的数据,再进行数据操作
4、具体使用:URI分为 系统预置 & 自定义,分别对应系统内置的数据(如通讯录、日程表等等)和自定义数据库
5、URI主要由authority和path两部分组成。
- authority是用于对不同的应用程序做区分的,一般采用 程序包名.provider 的方式来进行命名
- path是用于对同一应用程序中不同的表做区分的,通常添加到authority的后面
- 一个URI最标准的格式写法如下:content://com.example.app.provider/table1,其中:
content: 为协议声;
com.example.app为程序包名;
table1为表名。
6、得到内容URI字符串之后,通过调用parse()方法将它解析成Uri对象作为参数传入,解析的代码如下:
Uri uri = Uri.parse("content://com.example.app.provider/table1");
3.3 通过Uri实现CRUD操作
1、下面4个方法由外部进程回调,并运行在ContentProvider进程的Binder线程池中(不是主线程)
2、存在多线程并发访问时,需要实现线程同步
- 若ContentProvider的数据存储方式是使用SQLite & 一个,则不需要,因为SQLite内部实现好了线程同步,若是多个SQLite则需要,因为SQL对象之间无法进行线程同步
- 若ContentProvider的数据存储方式是内存,则需要自己实现线程同步
3.3.1 query
Cursor cursor = getContentResolver().query(uri,projection,selection,selectionArgs,sortOrder);
通过上述方法获取一个Cursor对象,用于打印查询数据,其中括号里的参数情况如下:
if(cursor != null){
while(cursor.moveToNext()){
String column1 = cursor.getString(cursor.getColumnIndex("column1"));
int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
}
}
基本上同SQLite一样,通过get方法获取数据然后操作
3.3.2 insert
ContentValues values = new ContentValues();
values.put("column1","text");
values.put("column2",1);
getContentResolver().insert(uri,values);
3.3.3 delete
getContentResolver().delete(uri,"column2 = ?",new String[]{"1"});
3.3.4 update
ContentValues values = new ContentValues();
values.put("column1","");
getContentResolver().update(uri,values,"column1 = ? and column2 = ?",new String[]{"text","1"});
3.4 举例:读取系统联系人
用一个ListView+运行时权限
来完成对联系人的读取
public class MainActivity extends AppCompatActivity {
ArrayAdapter<String> adapter;
List<String> contactsList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
...
ListView contactsView = (ListView)findViewById(R.id.contacts_view);
adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,contactsList);
contactsView.setAdapter(adapter);
}
private void readContacts(){
....
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
....
}
}
首先实例化ListView并弄好适配器,其中 android.R.layout.simple_list_item_1是自带的一种元素
@Override
protected void onCreate(Bundle savedInstanceState) {
...
if(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_CONTACTS},1);
}else{
readContacts();
}
}
private void readContacts(){
....
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode){
case 1:
if(grantResults.length>0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
readContacts();
}else{
Toast.makeText(this,"你取消了申请",Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
然后就是运行时权限判断
private void readContacts(){
Cursor cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,null,null,null);
if(cursor != null){
while (cursor.moveToNext()){
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String telephone = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
contactsList.add(name+"\n"+telephone);
}
arrayAdapter.notifyDataSetChanged();
}
}
重点看readContacts()方法里面的代码
1、首先通过query()方法得到一个Cursor对象,其中的ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME是系统已经封装好的一个常量
2、然后就是通过循环,将数据读取出来,并存入到List集合里面,即ListView的数据源里
3、调用notifyDataSetChanged()方法通知刷新一下ListView
<uses-permission android:name="android.permission.READ_CONTACTS"/>
最后是声明权限
四、 创建自己的内容提供器
1、通过新建一个类去继承ContentProvider的方式来创建一个自己的内容提供器,代码如下:
public class MyProvider extends ContentProvider {
//初始化内容提供器的时候调用。通常会在这里完成对数据库的创建和升级等操作
@Override
public boolean onCreate() {
return false;
}
//从内容提供器中查询数据:使用uri参数确定查询那张表,projection用于确定查询那些列, selection和selectionArgs用于查询那些行, sortOrder用于对结果进行排序
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
//根据传入的URI来返回对应的MIME类型
@Override
public String getType(Uri uri) {
return null;
}
//添加数据:uri参数确定要添加到的表,待添加的数据保存在values参数中
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
//删除数据:uri参数确定要删除哪个表的数据,selection和selectionArgs用于约束删除哪些行
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
//更新数据:uri参数确定要更新哪个表的数据,values保存新数据,selection和selectionArgs用于约束更新哪些行
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
}
2、创建内容提供器时,内容URI写法如下:content://com.example.app.provider/table1;
- 现在若内容后面加数字,表示期望访问该表中拥有相应id的数据,如
content://com.example.app.provider/table1/1
表示访问id为1的数据;(这个id是自己定义的数据库里面的一个自增型的id序号) - 内容URI的格式主要是以上两种:①以路径结尾表示期望访问该表中所有的数据;②以id结尾表示期望访问该表中拥有相应id的数据
- 使用通配符来分别匹配这两种格式的内容URI,规则如下:
- 1) * :表示能够匹配任意长度的任意字符,如
content://com.example.app.provider/ *
,表示一个能够匹配任意表的内容URI格式; - 2) # :表示能够匹配任意长度的数字,如
content://com.example.app.provider/table1/ #
,表示一个能够匹配table1表中任意一行数据的内容URI格式;
3、借助UriMatcher这个类就可以实现匹配内容URI的功能。该类中提供了一个addURI()方法,该方法接收authority、path和一个自定义代码,这样调用match()方法时就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是那张表中的数据了,代码如下:
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("com.example.app.provider","table1",TABLE1_DIR);//表示访问table1表中所有数据
uriMatcher.addURI("com.example.app.provider","table1/#",TABLE1_ITEM);//表示访问table1表中单条数据
uriMatcher.addURI("com.example.app.provider","table2",TABLE2_DIR);//表示访问table12表中所有数据
uriMatcher.addURI("com.example.app.provider","table2/#",TABLE2_ITEM);//表示访问table2表中单条数据
}
@Override
public Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal) {
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;
}
return (要求返回的数据);
}
4、getType()方法:它是所有的内容提供器都必须提供的一个方法,用于获取Uri对象所对应的MIME类型。
5、MIME数据类型的作用在于ContentProvider根据 URI 返回MIME类型
6、一个内容URI所对应的MIME字符串主要由3部分组成,Android对这三部分做出了如下格式规定:
- 1)必须以vnd开头;
- 2)如果内容URI以路径结尾,则后面接
android.cursor.dir/
,返回多条记录;如果内容URI以id结尾,则后面接android.cursor.item/
,返回单条记录; - 3)最后接上
vnd.<authority>.<path>
- 4)例如
content://com.example.app.provider/table1
对应的MIME类型是vnd.android.cursor.dir/vnd.com.example.app.provider.table1;
content://com.example.app.provider/table1/1
对应的MIME类型是vnd.android.cursor.item/vnd.com.example.app.provider.table1;
现在我们来完善getType()方法中的逻辑:
@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;
}
5、内容提供器的安全机制:只要不向UriMatcher中添加隐私数据的URI,那么该部分的数据就根本无法被外部程序访问到,安全问题也就不复存在了。