目录
3.8 ContentResolver使用案例:读取系统联系人
1.概述
1.1 引入
在数据储存中学习了Android数据持久化技术,包括文件储存、SharedPreferences储存以及数据库储存,这些储存技术所保存的数据都只能在当前应用程序中访问,但在Android开发中,有时会访问到其他应用程序中的数据。例如:使用支付宝转账时需要填写收款人的电话号码,此时就需要获取到系统联系人的信息。ContentProvider(内容提供者)就可以实现这种跨程序共享数据的功能。
文件和SharedPreferences储存中提供了MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE这两种操作模式,用于供给其他应用访问当前应用程序,但是这两种模式在Android4.2版本中就废弃了,因为它们是全局的可读写操作模式,不安全。因此,为了能够允许一个程序访问另外一个程序中的数据,而且还能保证被访问的数据的安全性,需要使用ContentProvider。
1.2 简介
ContentProvider(内容提供者)是Android系统四大组件之一,标准API,其功能是在不同程序之间实现数据共享,为储存和获取数据提供了统一的接口,具体的数据来源可以是表,也可以是文件,甚至网络等等。为应用程序之间共享数据提供了可能,比如读取电话薄里面的联系人。
不仅可以允许一个程序访问另一个程序中的数据,同时还可以选择只对哪一部分数据进行共享,从而保证程序只能国的隐私数据不被泄露。
对于ContentProvider而言,无论数据的来源是什么,它都认为是一种表,然后把数据组织成表,以数据库的形式操作数据。
1.3 工作原理
工作原理如下图:
从上图可以看出,A程序需要使用ContentProvider共享数据,才能被其他程序操作。B程序必须通过ContentResolver操作程序A共享出来的数据,而程序A会将操作结果返回给ContentResolver,然后ContentResolver再将操作结果返回给程序B。
1.5 应用场景
1.想要提供完整的数据和文件给其他应用程序。 2.想要允许用户从应用程序中复制完整的数据到其他的应用程序。 3.想要使用搜索框架来提供自定义的搜索建议。 1.6 用法
内容提供器的用法一般有两种:
1.使用现有的内容提供器来读取和操作相应程序中的数据。 2.创建自己的内容提供器给程序的数据提供外部访问的接口。 1.7 分类
1.ContentResolver(内容访问者) 用于访问内容提供器中共享的数据。 2.ContentProvider(内容提供者) 用于提供外部访问数据的接口。
2.URI
2.1 概念
ContentResolver与SQLiteDatabase类似,提供了一系列增、删、改、查的方法对数据进行操作。不同的是,ContentResolver的增、删、改、查方法都不接收表名参数的,而是使用一个Uri参数代替,也称为内容URI。这个URI为内容提供者中的数据建立了唯一标识符,它主要由scheme、authorities和path三部分组成。一个应用程序可能包含多个ContentProvider,所以需要授权来自各个标识,外部通过匹配这些标识来确定唯一的一个ContentProvider。如下所示:
解释如下:
shcheme shcheme部分“content://”是一个标准的前缀,表明这个数据被ContentProvider所控制,它不会被修改。协议声明 authority authority部分"cn.itcast.mycontentprovider"是在创建内容提供者时指定的authority属性值,主要迎来区分不同应用程序。 path path部分"/person"部分代表资源(或者数据),当访问者需要操作不同的数据时,这个部分可以动态的改变。主要用于
对同一个应用程序中不同的表做区分。
2.2 类别
一个标准的内容URI的写法:以包名加类名的形式
content://com.example.app.provider/table1
如果在这个内容URI后面加上一个id,如下所示:就表示要访问这个应用的table1表中id为1的数据
content://com.example.app.provider/table1/1
因此URI的格式主要有以下两种:
1.以路径结尾,就表示期望访问该表中所有的数据。 2.以id结尾,就表示期望访问该表中拥有相应id的数据 2.3 通配符
可以使用通配符来分别匹配上述两种格式的内容URI,其规则如下:
*:表示匹配任意长度的任意字符 #:表示匹配任意长度的数字
如下所示:
匹配任意表的内容URI:content://com.example.app.provider/* 匹配table1表中任意一行数据的内容URI:content://com.example.app.provider/table1/#
2.4 使用UriMatcher匹配内容URI
UriMatcher中提供了一个addURI()方法,这个方法接收三个参数,分别把authority、path和一个自定义代码传递进去。这样,当调用UriMatcher的match()方法时,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码。然后利用这个代码,就能判断出期望访问的是那张表中的数据了。
详细使用见后面创建自定义的ContentProvider。
2.5 解析
URI很清楚地表示出想要访问那个程序中那张表的数据,有了内容URI字符串后,还需要将它解析成Uri对象才可以作为参数传入,方法如下所示:
Uri uri = Uri.parse("content://cn.itcast.mycontentprovider/person");
3.创建ContentResolver
3.1 简介
对于每一个应用程序来说,如果想要访问ContentProvider中的共享数据,就需要借助ContentResolver。
3.2 获取实例
可以通过Context中的getContentResolver()方法获取ContentResolver实例。
3.3 方法
ContentResolver中提供了一系列的方法用于对数据进行CRUD操作,如下:
1.insert() 添加数据 2.update() 更新数据 3.delete() 删除数据 4.query() 查询数据 3.4 query() 方法
有了Uri对象就可用查询table表中的数据了,如下所示:
Cursor cursor = getContentResolver().query( uri, projection, selection, selectionArgs, sortOrder);
参数解释如下所示:
query()方法参数 对应SQL部分 描述 uri from table_name 指定查询某个应用程序下的某一张表 projection select column1,column2 指定查询的列名 selection where column = value 指定where的约束条件 selectionArgs - 为where中的占位符提供具体的值 sortOrder order by column1,column2 指定查询结果的排序方式 查询完成后返回的是一个Cursor对象,然后将数据从Cursor对象中逐个读取出来即可。读取的思路是通过移动游标的位置来遍历Cursor的所有行,然后再取出每一行中相应列的数据,示例如下所示:
if(cursor != null){ while(cursor.moveToNext()){ String column1 = cursor.getString(cursor.getColumnIndex("column1")); int column2 = cursor.getInt(cursor.getColumnIndex("column2")); } cursor.close(); }
3.5 insert()方法
添加数据的示例代码如下所示:
ContentValues values = new ContentValues(); values.put("column1","text"); values.put("column2",1); getContentResolver().insert(uri,values);
注意:这里需要将需要添加的数据组装到ContentValues中去
3.6 update()方法
假设需要将column1的值清空,代码如下所示:
ContentValues values = new ContentValues(); values.put("column1",""); getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new String[]{"text", "1"});
3.7 delete()方法
示例代码如下:
getContentResolver().delete(uri, "column2 = ?", new String[]{"1});
3.8 ContentResolver使用案例:读取系统联系人
3.8.1 效果图
1)联系人
2)得到联系人信息
3.8.2 布局
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ListView android:id="@+id/contacts_view" android:layout_width="match_parent" android:layout_height="match_parent"> </ListView> </LinearLayout>
3.8.3 代码
public class MainActivity extends AppCompatActivity { ArrayAdapter<String> adapter; //储存联系人 List<String> contactsList = new ArrayList<>(); ListView contactsView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); contactsView = findViewById(R.id.contacts_view); adapter = new ArrayAdapter<>(this,android.R.layout.simple_list_item_1,contactsList); contactsView.setAdapter(adapter); //判断使用拥有读取权限 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() { Cursor cursor = null; try{ //查询联系人数据 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 number = cursor.getString(cursor.getColumnIndex(ContactsContract .CommonDataKinds.Phone.NUMBER)); contactsList.add("姓名:"+name + "\n"+"号码:"+number); } //通知刷新ListView adapter.notifyDataSetChanged(); } }catch (Exception e){ e.printStackTrace(); }finally { if(cursor != null){ //关闭Cursor cursor.close(); } } } //处理权限请求结果 @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode){ case 1: if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){ readContacts(); }else{ Toast.makeText(this,"You denied the permisssion",Toast.LENGTH_SHORT).show();; } break; default: break; } } }
这里ContactsContract.CommonDataKinds.Phone已经封装好了,提供了一个CONTENT_URI的内容URI字符常量,这个常量就是使用Uri.parse()解析出来的结果。
3.8.4 添加权限
在AndroidManifest.xml中,添加如下代码:
<uses-permission android:name="android.permission.READ_CONTACTS"/>
4.创建自己的ContentProvider
4.1 创建原理
如果想要创建自己的ContentProvider,需要新建子类继承自ContentProvider,需要重写其6个抽象方法。
点击【new】->【other】->【ContentPrivider】选项,创建的如下所示:
java文件:
public class MyContentProvider extends ContentProvider { @Override public int delete(Uri uri, String selection, String[] selectionArgs) { throw new UnsupportedOperationException("Not yet implemented"); } @Override public String getType(Uri uri) { throw new UnsupportedOperationException("Not yet implemented"); } @Override public Uri insert(Uri uri, ContentValues values) { throw new UnsupportedOperationException("Not yet implemented"); } @Override public boolean onCreate() { return false; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { throw new UnsupportedOperationException("Not yet implemented"); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { throw new UnsupportedOperationException("Not yet implemented"); } }
如上所示,onCreate()方法是在内容提供者创建时调用,insert()、delete()、update()、query()分贝用于根据指定的URI对数据进行增、删、改、查。getType()用于返回指定URI代表的数据的MIME类型,例如Windows系统中的.txt文件和.jpg文件就是两种不同的MIME类型。
注意:insert()和update()方法,里面的新数据都保存在ContentValues里面。
清单文件AndroidManifest.xml中注册如下:
<application> <provider android:name="com.example.contentprovider.MyContentProvider" android:authorities="com.example.contentprovider" android:enabled="true" android:exported="true"> </provider> </application>
4.2 分析
ContentProvider是一个抽象类,是Android应用程序中主要的组成部分之一,为应用程序提供对外的内容,它封装了数据然后提供给那些通过实现ContentResolver接口的应用程序,这些数据是可以跨应用访问的。
4.3 使用UriMatcher
4.3.1 示例
示例代码如下:
public class MyProvider extends ContentProvider{ //表示查询table1表中的所有数据 public static final int TABLE1_DIR = 0; //表示查询table1表中的单条数据 public static final int TABLE1_ITEM = 1; //表示查询table2表中的所有数据 public static final int TABLE2_DIR = 2; //表示查询table2表中的单条数据 public static final int TABLE2_ITEM = 3; private static UriMatcher uriMatcher; static{ uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI("com.example.app.prodiver","table1",TABLE1_DIR); uriMatcher.addURI("com.example.app.prodiver","table1/#",TABLE1_ITEM); uriMatcher.addURI("com.example.app.prodiver","table2",TABLE2_DIR); uriMatcher.addURI("com.example.app.prodiver","table2/#",TABLE2_ITEM); } @Override public boolean onCreate() { return false; } @Nullable @Override public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable 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; } return null; } @Nullable @Override public String getType(@NonNull Uri uri) { return null; } @Nullable @Override public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { return null; } @Override public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { return 0; } @Override public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { return 0; } }
4.3.2 getType()方法
用于获取Uri对象所对应的MIME类型。一个内容URI所对应的MIME字符串主要由3部分组成,规定如下所示:
1.必须以vnd开头 2.如果内容URI以路径结尾,则vnd后接android,cursor,dir/;如果内容URI以id结尾,则vnd后接android.cursor.item/ 3.最后接上vnd.<authority>.<path> 如下所示:
1.对于content://com.example.app.provider/table1这个内容URI,对应的MIME类型为: vnd.android.cursor.dir/vnd.content://com.example.app.provider/table1 2.对于content://com.example.app.provider/table1/1这个内容URI,对应的MIME类型为: vnd.android.cursor.item/vnd.content://com.example.app.provider/table1
完善getType()代码如下所示:
@Nullable @Override public String getType(@NonNull 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.ContentObserver
对于每一个应用程序来说,如果想要访问内容提供器中的共享数据,那么就需要使用Android系统提供的内容观察者ContentObserver。
5.1 内容观察者简介
内容观察者ContentObserver是用来观察指定Uri所代表的数据。当ContentObserver观察到指定Uri代表的数据发生变化时,就会触发ContentObserver的onChange()方法,此时在onChange()方法里使用ContentObserver就可以查询到变化的数据。如下图所示:
下面是ContentObserver的两个常用方法:
》public void ContentObserver(Handler handler):ContentObserver的派生类都调用该构造方法,参数可以是主线程Handler,也可以说是任意Handler对象。
》public void onChange(boolean selfChange):当观察到的Uri代表的数据发生变化时,就会触发该方法。
用于ContentProvider是通过delete()、insert()、update()这几个方法让数据发生变化的,因此要使用ContentObserver观察数据变化,就必须在ContentProvider的delete()、insert()、update()方法中调用ContentResolver的notifyChanger()方法,示例代码如下:
//添加数据 public Uri insert(Uri uri,ContentValues values){ if(matcher.match(uri) == INSERT){ //匹配Uri路径 //匹配成功,返回查询的结果集 SQLiteDatabase db = helper.getWritavbleDatabase(); db.insert("person",null,values); getContext().getContentResolver().notifyChange(PersonDao.messageuri,null); }else{ //匹配失败 throw new IllegalArgumentException("路径不匹配,不能执行插入操作"); } return null; }
notifyChange()方法实现了将数据变化的消息发送至消息中心,接收两个参数,第一个表示更改内容的Uri对象,第二个表示指定某个观察者接收消息,不指定可填写null。
接下来实现在程序中注册内容观察者,并监听数据变化的功能,如下:
public class ContentObserverTest extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //获取ContentResolver对象 ContentResolver resolver = getContentResolver(); Uri uri = Uri.parse("content://aaa.bbb.ccc"); //注册内容观察者 resolver.registerContentObserver(uri,true,new MyObserver(new Handler())); } //自定义的内容观察者 public class MyObserver extends ContentObserver { //构造方法 public MyObserver(Handler handler){ super(handler); } //当内容观察者观察到数据库的内容发生变化时调用该方法 public void onChange(boolean selfChange){ super.onChange(selfChange); Toast.makeText(ContentObserverTest.this, "数据库内容变化了", Toast.LENGTH_SHORT).show(); Uri uri = Uri.parse("content://aaa.bbb.ccc"); //获取ContentResolver对象 ContentResolver resolver = getContentResolver(); //通过ContentResolver对象查询出变化的数据 Cursor cursor = resolver.query(uri,new String[]{"address","date","type","body"},null,null,null); cursor.moveToFirst(); String address = cursor.getString(0); String body = cursor.getString(3); Log.v("MyObserver",body); cursor.close(); } } }