为什么微信、QQ、淘宝等 App 都能访问联系人 (通讯录) 呢?是因为 Android 存在一种应用之间的数据共享机制,即 ContentProvider,ContentProvider 作为 Android 四大组件之一,为存储和获取数据提供统一的接口,可以在不同的应用程序之间共享数据。对于 ContentProvier 而言,无论数据的来源是什么,它都认为是种表(同时也支持文件数据,只是表格形式用得比较多),然后把数据组织成表格返回给使用者。
自定义 ContentProvider
step1、自定义类继承于 ContentProvider,实现要求的方法
step2、在配置文件中通过 provider 标签配置,通过 android:name 属性指定待配置的类,通过 android:authorities 属性授权,指定当前内容提供者的 uri 标识,必须唯一。
下面来展示一个 B 应用来操作 A 应用中的数据的例子:
首先在 ContentProviderDemo 这个工程里写一个名为 MyContentProvider 的 ContentProvider:
public class MyContentProvider extends ContentProvider {
private static final String TAG = "MyContentProvider";
private SQLiteDatabase sqLiteDatabase;
public MyContentProvider() {
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
Log.i(TAG, "delete: ");
return sqLiteDatabase.delete("stu_info", selection, selectionArgs);
}
@Override
public String getType(Uri uri) {
// TODO: Implement this to handle requests for the MIME type of the data
// at the given URI.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public Uri insert(Uri uri, ContentValues values) {
Log.i(TAG, "insert: ");
// 参数解释:操作表的名称、可以为空的列、参数
sqLiteDatabase.insert("stu_info", null, values);
return uri;
}
// 在ContentProvider创建时调用
@Override
public boolean onCreate() {
SQLiteOpenHelper helper = new SQLiteOpenHelper(getContext(), "stu.db", null, 1) {
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("create table stu_info (id integer primary key autoincrement," +
" name varchar(30) not null, age integer," +
" gender varchar(2) not null)");
Log.i(TAG, "onCreate: 数据库创建成功");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
};
sqLiteDatabase = helper.getWritableDatabase();
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Log.i(TAG, "query: ");
return sqLiteDatabase.query("stu_info", projection, selection, selectionArgs, sortOrder, null, null);
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
Log.i(TAG, "update: ");
return sqLiteDatabase.update("stu_info", values, selection, selectionArgs);
}
}
对于四大组件之一的 ContentProvider 同样需要在 AndroidManifest.xml 中声明:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="cn.tim.contentproviderdemo">
<application
android:allowBackup="true"
....
android:theme="@style/AppTheme">
<provider
android:
android:authorities="cn.tim.myprovider"
android:enabled="true"
android:exported="true" />
...
</application>
</manifest>
必须通过 android:name 属性指定待配置的类,通过 android:authorities 属性授权,指定当前内容提供者的 uri 标识,必须唯一,因为对于使用 ContentProvier 的 App 来说,这是唯一可以找到该 ContentProvier 的信息,就像坐标一样,是唯一可以确定你的位置的信息。
可以看到在这个类里面主要包含了 CRUD 等方法,还有 getType() 方法和 onCreate() 方法。所以为什么说对于 ContentProvier 而言,无论数据的来源是什么,它都认为是种表,然后把数据组织成表格。因为这恰好对应了表中数据的 CRUD。至于 getType() 方法是做什么现在可以不管,整个 MyContentProvider 不过是在初始化的时候创建了数据库,拿到了 SQLiteDatabase 对象,然后 MyContentProvider 其中的 CRUD 方法实现成了数据库的操作方法而已。如果对数据库不太熟悉,可以参考之前的文章《SQLite 原理与运用》,里面有具体介绍使用方法。
值得注意的是,虽然我们在 MyContentProvider 的 CRUD 中使用了 SQLite 数据库,但是其实这和 ContentProvider 本身并没有关系,数据的增删改查我们完全也可以用 HashMap 这种数据结构存在内存中,或者存成文件的形式,一行文本就代表一个数据对象,这里为了方便演示所以直接采用了 SQLite。
ContentProviderDemo 这个工程就结束了,因为作为内容提供者,它无需提供操作界面。下面看看使用者,也就是图中的 OtherApplication。当然在这个示例中,这个工程名称为 ContentAcquireDemo,界面和《SQLite 原理与运用》中的界面一模一样,只不过是在 CRUD 的时候不再是操作本地 SQLite,而是操作 ContentProviderDemo 中的 MyContentProvider:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
ContentResolver contentResolver;
private EditText etId;
private EditText etName;
private EditText etAge;
private String sex = "男";
private ListView lvData;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etId = findViewById(R.id.et_id);
etName = findViewById(R.id.et_name);
etAge = findViewById(R.id.et_age);
// 单选框组件
RadioGroup rgSex = findViewById(R.id.rg_sex);
lvData = findViewById(R.id.lv_data);
// 获取ContentResolver对象
contentResolver = getContentResolver();
// 设置单选框的监听
rgSex.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
switch (checkedId){
case R.id.rb_female:
sex = "女";
break;
case R.id.rb_male:
sex = "男";
break;
}
}
});
flushStuData();
}
private void flushStuData() {
Uri uri = Uri.parse("content://cn.tim.myprovider");
List<StudentInfo> stuList = new ArrayList<>();
// 参数解释:表名、要查询的字段、列条件、列条件参数、GroupBy、having、orderBy
Cursor cursor = contentResolver.query(uri, null, null, null, null);
if(cursor !=null && cursor.moveToFirst()){
do{
int id = cursor.getInt(0);
String name = cursor.getString(1);
int age = cursor.getInt(2);
String sex = cursor.getString(3);
stuList.add(new StudentInfo(id, name, age, sex));
} while (cursor.moveToNext());
cursor.close();
}
lvData.setAdapter(new StuInfoAdapter(this, stuList));
}
public void operatorData(View view) {
Uri uri = Uri.parse("content://cn.tim.myprovider");
int viewId = view.getId();
switch (viewId) {
case R.id.btn_add:
ContentValues values = new ContentValues();
values.put("name", etName.getText().toString());
values.put("age", Integer.parseInt(etAge.getText().toString()));
values.put("gender", sex);
contentResolver.insert(uri, values);
// 刷新数据展示
flushStuData();
Toast.makeText(MainActivity.this, "添加成功", Toast.LENGTH_SHORT).show();
break;
case R.id.btn_update:
String idStr = etId.getText().toString();
ContentValues updateValues = new ContentValues();
// Key - value
updateValues.put("name", etName.getText().toString());
updateValues.put("age", Integer.parseInt(etAge.getText().toString()));
updateValues.put("gender", sex);
contentResolver.update(uri, updateValues, "id=?", new String[]{idStr});
Toast.makeText(MainActivity.this, "更新成功", Toast.LENGTH_SHORT).show();
flushStuData();
break;
case R.id.btn_delete:
String deleteIdStr = etId.getText().toString();
contentResolver.delete(uri, "id=?", new String[]{deleteIdStr});
// 刷新数据展示
flushStuData();
Toast.makeText(MainActivity.this, "删除成功", Toast.LENGTH_SHORT).show();
break;
}
}
}
可以看到,其实使用 content://cn.tim.myprovider 这个 ContentProvider 同样达到了 CRUD 的效果,需要注意的就是别把 URI 写错了就行,所以下面来看看 URI 的解析:
Uri 匹配之 UriMatcher
UriMatcher:在 ContentProvider 创建时,制定好匹配规则,当调用了 ContentProvider 中的操作方法时,利用匹配类去匹配传的 uri,根据不同的 uri 给出不同的处理。
现在在 MyContentProvider 的 onCrate() 方法中定义一个 UriMatcher 匹配器,并且给出匹配规则如下:
public class MyContentProvider extends ContentProvider {
...
private UriMatcher matcher;
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
Log.i(TAG, "delete: ");
int matchCode = matcher.match(uri);
switch (matchCode){
case 1001:
Log.i(TAG, "delete: 匹配到路径是/hello");
break;
default:
Log.i(TAG, "delete: 执行删除数据库内容操作");
return sqLiteDatabase.delete("stu_info", selection, selectionArgs);
}
return 0;
}
...
// 在ContentProvider创建时调用
@Override
public boolean onCreate() {
...
sqLiteDatabase = helper.getWritableDatabase();
// 参数代表无法匹配
// content://cn.tim.myprovider/hello
matcher = new UriMatcher(UriMatcher.NO_MATCH);
// Authority 、路径、匹配码
matcher.addURI("cn.tim.myprovider", "hello", 1001);
return true;
}
...
}
这样在另一个 App 中使用 MyContentProvider 的 delete() 方法的时候就会进行 URI 匹配判断:
contentResolver.delete(Uri.parse("content://cn.tim.myprovider/hello"), null, null);
大家在今后的开发中可能会用到更多的匹配模式,接下来我们学习 UriMatcher 更多匹配:
UriMatcher 还可以使用匹配通配符来匹配任意不确定的值:
matcher = new UriMatcher(UriMatcher.NO_MATCH);
// Authority 、路径、匹配码
matcher.addURI("cn.tim.myprovider", "hello", 1001);
// 匹配 cn.tim.myprovider/hello/任意数字
matcher.addURI("cn.tim.myprovider", "hello/#", 1002);
// 匹配 cn.tim.myprovider/world/任意字符串
matcher.addURI("cn.tim.myprovider", "world/*", 1003);
return true;
使用 Uri 自带的解析方法
现在 ContentAcquireDemo 假设添加方法是这样调用的,即把参数写在 Uri 里面,这样在 ContentProviderDemo 工程的 MyContentProvider 中又是如何解析的呢?
Uri insertUri = Uri.parse("content://cn.tim.myprovider/whatever?);
Uri uri = contentResolver.insert(insertUri, new ContentValues());
long newId = ContentUris.parseId(uri);
Toast.makeText(this, "添加成功: Id" + newId, Toast.LENGTH_SHORT).show();
MyContentProvider.java 的关键代码:
@Override
public Uri insert(Uri uri, ContentValues values) {
long id = 0;
// 为了保持原来的方式不做变更,所以这里需要判断一下
if(values.size() > 0){
id = sqLiteDatabase.insert("stu_info", null, values);
}else {
String authority = uri.getAuthority();
String path = uri.getPath();
String query = uri.getQuery();
String name = uri.getQueryParameter("name");
String age = uri.getQueryParameter("age");
String gender = uri.getQueryParameter("gender");
Log.i(TAG, "insert:->主机名:" + authority
+ ",路径:" + path + ",查询数据:" + query + ",姓名:" + name
+ ",age:" + age + ",gender:" + gender);
values.put("name", name);
values.put("age", age);
values.put("gender", gender);
id = sqLiteDatabase.insert("stu_info", null, values);
}
return ContentUris.withAppendedId(uri, id);
}
果然通过这样的 Uri 自带的解析方式来传递参数也是 OK 的。
关于 Uri 必须知道的
这样的解析方式涉及到 Uri 的组成和结构问题,首先来说一说 URI 和 Uri 是什么关系吧,Uri 是 Android 的 API,扩展了 JavaSE 中 URI 的一些功能来特定的适用于 Android 开发,所以大家在开发时,只使用 Android 提供的 Uri 即可。
Uri 统一资源标识符 (Uniform Resource Identifier),有时我们又看到 URL 这样的东西,他们之间的又是什么关系呢?统一资源标志符 URI 就是在某一规则下能把一个资源独一无二地标识出来,比如想要标识一个我国公民,只要用身份证号就可以作为唯一标识,但是使用其他方式也可以用来标识唯一个人,比如:个人定位协议:// 中华人名共和国 / 陕西省 / 西安市 / 临潼区 / 斜口街道 / 西安工程大学 / 8# 宿舍 / A120 / 邹长林, 这个字符串同样标识出了唯一的一个人,起到了 URI 的作用,所以 URL 是 URI 的子集。URL 是以描述人的位置来唯一确定一个人的。
所以统一资源标志符 URI 就是在某一规则下能把一个资源独一无二地标识出来,URL 就是某主机上的某路径上的文件来唯一确定一个资源,也就是定位的方式来实现的 URI,即 URL 是 URI 的一种实现。
关于更多 Uri 结构和代码提取的资料可以参考《Uri 详解之——Uri 结构与代码提取》。
使用系统的ContentProcider
下面是通过读写系统通讯录和读取短信的几个小例子,作为 ContentProvider 使用练习:
读取通讯录
public void visitAddressBook(View view) {
ContentResolver resolver = getContentResolver();
//联系人姓名 + Id
Uri uri = ContactsContract.Contacts.CONTENT_URI;
//联系人电话
Uri uriPhone = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
Cursor cursor = resolver.query(uri, null, null, null, null);
while(cursor!= null && cursor.moveToNext()){
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
String contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID));
Log.i(TAG, "visitAddressBook: name = " + name + ", id = " + contactId);
String selection = ContactsContract.CommonDataKinds.Phone.CONTACT_ID + "=" +contactId;
Cursor phoneCursor = resolver.query(uriPhone,null, selection, null, null);
while (phoneCursor != null && phoneCursor.moveToNext()){
String phone = phoneCursor.getString(phoneCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
Log.i(TAG, "visitAddressBook: name = " + name + ", phone = " + phone);
}
if(phoneCursor != null) phoneCursor.close();
}
if(cursor != null) cursor.close();
}
添加通讯录
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
// 申请权限请求码
private static final int REQUEST_READ_SMS = 1001;
// 检查权限,这种写法主要是针对比较新的Android6.0及以后的版本
public static void verifyStoragePermissions(Activity activity) {
int smsPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_SMS);
int contactsPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS);
int writeContactsPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_CONTACTS);
if (smsPermission != PackageManager.PERMISSION_GRANTED
|| contactsPermission != PackageManager.PERMISSION_GRANTED
|| writeContactsPermission != PackageManager.PERMISSION_GRANTED) {
// 如果没有权限需要动态地去申请权限
ActivityCompat.requestPermissions(
activity,
// 权限数组
new String[]{Manifest.permission.READ_SMS, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS},
// 权限请求码
REQUEST_READ_SMS
);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
verifyStoragePermissions(this);
}
public void addAddressBook(View view) {
//1、往一个ContentProvider中插入一条空数据,获取新生成的Id
//2、利用刚刚生成的Id分别组合姓名和电话号码往另一个ContentProvider中插入数据
ContentValues values = new ContentValues();
ContentResolver resolver = getContentResolver();
Uri uri = resolver.insert(ContactsContract.RawContacts.CONTENT_URI, values);
if(uri == null) throw new RuntimeException("插入新联系人失败");
values.clear();
long id = ContentUris.parseId(uri);
// 插入姓名
values.put(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, id);
values.put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, "Mike");
values.put(ContactsContract.CommonDataKinds.StructuredName.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
uri = resolver.insert(ContactsContract.Data.CONTENT_URI, values);
if(uri != null) Log.i(TAG, "addAddressBook: 插入姓名,id = " + ContentUris.parseId(uri));
//插入电话信息
values.clear();
values.put(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, id);
values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, "15720918678"); //添加号码
values.put(ContactsContract.CommonDataKinds.Phone.MIMETYPE,
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
values.put(ContactsContract.CommonDataKinds.Phone.TYPE,
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE); //添加号码类型
uri = resolver.insert(ContactsContract.Data.CONTENT_URI, values);
if(uri != null) Log.i(TAG, "addAddressBook: 插入电话号码,id = " + ContentUris.parseId(uri));
}
}
读取短信
| 短信类型 | Uri |
|---|---|
| 短信箱 | content://sms |
| 收件箱 | content://sms/inbox |
| 发件箱 | content://sms/sent |
| 草稿箱 | content://sms/draft |
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
// 申请权限请求码
private static final int REQUEST_READ_SMS = 1001;
// 检查权限,这种写法主要是针对比较新的Android6.0及以后的版本
public static void verifyStoragePermissions(Activity activity) {
int smsPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_SMS);
int contactsPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS);
if (smsPermission != PackageManager.PERMISSION_GRANTED
|| contactsPermission != PackageManager.PERMISSION_GRANTED) {
// 如果没有权限需要动态地去申请权限
ActivityCompat.requestPermissions(
activity,
// 权限数组
new String[]{Manifest.permission.READ_SMS, Manifest.permission.READ_CONTACTS},
// 权限请求码
REQUEST_READ_SMS
);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
verifyStoragePermissions(this);
}
public void visitMessage(View view) {
ContentResolver resolver = getContentResolver();
Uri uri = Uri.parse("content://sms");
Cursor cursor = resolver.query(uri, null, null, null, null);
while(cursor != null && cursor.moveToNext()){
int addressIndex = cursor.getColumnIndex("address");
int bodyIndex = cursor.getColumnIndex("body");
String address = cursor.getString(addressIndex);
String body = cursor.getString(bodyIndex);
Log.i(TAG, "visitMessage:" + address + ":" + body);
}
if(cursor != null) cursor.close();
}
}
在 AndroidManifest.xml 配置一下权限:
<uses-permission android:/>
ContentProvider的优点
ContentProvider 的底层实现是 Binder,更多关于 Binder 的内容可以参考官方文档《Binder》。ContentProvider 为应用间的数据交互提供了一个安全的环境:允许把自己的应用数据根据需求开放给其他应用进行 CRUD,而不用担心因为直接开放数据库权限而带来的安全问题。而其他对外共享数据的方式,数据访问方式会因数据存储的方式而不同而发生变化,底层存储方式变更会影响上层,使访问数据变得更加复杂。而采用 ContentProvider 方式,其解耦了底层数据的存储方式,使得无论底层数据存储采用何种方式,外界对数据的访问方式都是统一的,这使得访问简单且高效。