探究ContentProvider

为什么微信、QQ、淘宝等App都能访问联系人(通讯录)呢?是因为Android存在一种应用之间的数据共享机制,即ContentProvider,ContentProvider作为Android四大组件之一,为存储和获取数据提供统一的接口,可以在不同的应用程序之间共享数据。对于ContentProvier而言,无论数据的来源是什么,它都认为是种表(同时也支持文件数据,只是表格形式用得比较多),然后把数据组织成表格返回给使用者。

自定义ContentProvider

step1、自定义类继承于ContentProvider,实现要求的方法 step2、在配置文件中通过provider标签配置,通过android:name属性指定待配置的类,通过android:authorities属性授权,指定当前内容提供者的uri标识,必须唯一。

下面来展示一个B应用来操作A应用中的数据的例子:

首先在ContentProviderDemo这个工程里写一个名为MyContentProvider的ContentProvider:

 1public class MyContentProvider extends ContentProvider {
 2    private static final String TAG = "MyContentProvider";
 3    private SQLiteDatabase sqLiteDatabase;
 4
 5    public MyContentProvider() {
 6    }
 7
 8    @Override
 9    public int delete(Uri uri, String selection, String[] selectionArgs) {
10        Log.i(TAG, "delete: ");
11        return sqLiteDatabase.delete("stu_info", selection, selectionArgs);
12    }
13
14    @Override
15    public String getType(Uri uri) {
16        // TODO: Implement this to handle requests for the MIME type of the data
17        // at the given URI.
18        throw new UnsupportedOperationException("Not yet implemented");
19    }
20
21    @Override
22    public Uri insert(Uri uri, ContentValues values) {
23        Log.i(TAG, "insert: ");
24        // 参数解释:操作表的名称、可以为空的列、参数
25        sqLiteDatabase.insert("stu_info", null, values);
26        return uri;
27    }
28
29    // 在ContentProvider创建时调用
30    @Override
31    public boolean onCreate() {
32        SQLiteOpenHelper helper = new SQLiteOpenHelper(getContext(), "stu.db", null, 1) {
33            @Override
34            public void onCreate(SQLiteDatabase db) {
35                db.execSQL("create table stu_info (id integer primary key autoincrement," +
36                        " name varchar(30) not null, age integer," +
37                        " gender varchar(2) not null)");
38                Log.i(TAG, "onCreate: 数据库创建成功");
39            }
40
41            @Override
42            public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
43
44            }
45        };
46        sqLiteDatabase = helper.getWritableDatabase();
47        return true;
48    }
49
50    @Override
51    public Cursor query(Uri uri, String[] projection, String selection,
52                        String[] selectionArgs, String sortOrder) {
53        Log.i(TAG, "query: ");
54        return sqLiteDatabase.query("stu_info", projection, selection, selectionArgs, sortOrder, null, null);
55    }
56
57    @Override
58    public int update(Uri uri, ContentValues values, String selection,
59                      String[] selectionArgs) {
60        Log.i(TAG, "update: ");
61        return sqLiteDatabase.update("stu_info", values, selection, selectionArgs);
62    }
63}

对于四大组件之一的ContentProvider同样需要在AndroidManifest.xml中声明:

 1<?xml version="1.0" encoding="utf-8"?>
 2<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 3    package="cn.tim.contentproviderdemo">
 4
 5    <application
 6        android:allowBackup="true"
 7        ....
 8        android:theme="@style/AppTheme">
 9        <provider
10            android:name=".MyContentProvider"
11            android:authorities="cn.tim.myprovider"
12            android:enabled="true"
13            android:exported="true" />
14		...
15    </application>
16</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:

 1public class MainActivity extends AppCompatActivity {
 2    private static final String TAG = "MainActivity";
 3    ContentResolver contentResolver;
 4    private EditText etId;
 5    private EditText etName;
 6    private EditText etAge;
 7    private String sex = "男";
 8    private ListView lvData;
 9
10    @Override
11    protected void onCreate(Bundle savedInstanceState) {
12        super.onCreate(savedInstanceState);
13        setContentView(R.layout.activity_main);
14        etId = findViewById(R.id.et_id);
15        etName = findViewById(R.id.et_name);
16        etAge = findViewById(R.id.et_age);
17
18        // 单选框组件
19        RadioGroup rgSex = findViewById(R.id.rg_sex);
20        lvData = findViewById(R.id.lv_data);
21
22        // 获取ContentResolver对象
23        contentResolver = getContentResolver();
24
25        // 设置单选框的监听
26        rgSex.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
27            @Override
28            public void onCheckedChanged(RadioGroup group, int checkedId) {
29                switch (checkedId){
30                    case R.id.rb_female:
31                        sex = "女";
32                        break;
33                    case R.id.rb_male:
34                        sex = "男";
35                        break;
36                }
37            }
38        });
39        flushStuData();
40    }
41
42    private void flushStuData() {
43        Uri uri = Uri.parse("content://cn.tim.myprovider");
44        List<StudentInfo> stuList = new ArrayList<>();
45        // 参数解释:表名、要查询的字段、列条件、列条件参数、GroupBy、having、orderBy
46        Cursor cursor = contentResolver.query(uri, null, null, null, null);
47        if(cursor !=null && cursor.moveToFirst()){
48            do{
49                int id = cursor.getInt(0);
50                String name = cursor.getString(1);
51                int age = cursor.getInt(2);
52                String sex = cursor.getString(3);
53                stuList.add(new StudentInfo(id, name, age, sex));
54            } while (cursor.moveToNext());
55            cursor.close();
56        }
57        lvData.setAdapter(new StuInfoAdapter(this, stuList));
58    }
59
60
61    public void operatorData(View view) {
62        Uri uri = Uri.parse("content://cn.tim.myprovider");
63        int viewId = view.getId();
64        switch (viewId) {
65            case R.id.btn_add:
66                ContentValues values = new ContentValues();
67                values.put("name", etName.getText().toString());
68                values.put("age", Integer.parseInt(etAge.getText().toString()));
69                values.put("gender", sex);
70                contentResolver.insert(uri, values);
71                // 刷新数据展示
72                flushStuData();
73                Toast.makeText(MainActivity.this, "添加成功", Toast.LENGTH_SHORT).show();
74                break;
75            case R.id.btn_update:
76                String idStr = etId.getText().toString();
77                ContentValues updateValues = new ContentValues();
78                // Key - value
79                updateValues.put("name", etName.getText().toString());
80                updateValues.put("age", Integer.parseInt(etAge.getText().toString()));
81                updateValues.put("gender", sex);
82                contentResolver.update(uri, updateValues, "id=?", new String[]{idStr});
83                Toast.makeText(MainActivity.this, "更新成功", Toast.LENGTH_SHORT).show();
84                flushStuData();
85                break;
86            case R.id.btn_delete:
87                String deleteIdStr = etId.getText().toString();
88                contentResolver.delete(uri, "id=?", new String[]{deleteIdStr});
89                // 刷新数据展示
90                flushStuData();
91                Toast.makeText(MainActivity.this, "删除成功", Toast.LENGTH_SHORT).show();
92                break;
93        }
94    }
95}

可以看到,其实使用content://cn.tim.myprovider这个ContentProvider同样达到了CRUD的效果,需要注意的就是别把URI写错了就行,所以下面来看看URI的解析:

Uri匹配之UriMatcher

UriMatcher:在ContentProvider创建时,制定好匹配规则,当调用了ContentProvider中的操作方法时,利用匹配类去匹配传的uri,根据不同的uri给出不同的处理。

现在在MyContentProvider的onCrate()方法中定义一个UriMatcher匹配器,并且给出匹配规则如下:

 1public class MyContentProvider extends ContentProvider {
 2   	...
 3	private UriMatcher matcher;
 4    
 5    @Override
 6    public int delete(Uri uri, String selection, String[] selectionArgs) {
 7        Log.i(TAG, "delete: ");
 8        int matchCode = matcher.match(uri);
 9        switch (matchCode){
10            case 1001:
11                Log.i(TAG, "delete: 匹配到路径是/hello");
12                break;
13            default:
14                Log.i(TAG, "delete: 执行删除数据库内容操作");
15                return sqLiteDatabase.delete("stu_info", selection, selectionArgs);
16        }
17        return 0;
18    }
19	...
20    // 在ContentProvider创建时调用
21    @Override
22    public boolean onCreate() {
23        ...
24        sqLiteDatabase = helper.getWritableDatabase();
25
26        // 参数代表无法匹配
27        // content://cn.tim.myprovider/hello
28        matcher = new UriMatcher(UriMatcher.NO_MATCH);
29        // Authority 、路径、匹配码
30        matcher.addURI("cn.tim.myprovider", "hello", 1001);
31        return true;
32    }
33    ...
34}

这样在另一个App中使用MyContentProvider的delete()方法的时候就会进行URI匹配判断:

1contentResolver.delete(Uri.parse("content://cn.tim.myprovider/hello"), null, null);

大家在今后的开发中可能会用到更多的匹配模式,接下来我们学习UriMatcher更多匹配:

UriMatcher还可以使用匹配通配符来匹配任意不确定的值:

 1matcher = new UriMatcher(UriMatcher.NO_MATCH);
 2// Authority 、路径、匹配码
 3matcher.addURI("cn.tim.myprovider", "hello", 1001);
 4
 5// 匹配 cn.tim.myprovider/hello/任意数字
 6matcher.addURI("cn.tim.myprovider", "hello/#", 1002);
 7
 8// 匹配 cn.tim.myprovider/world/任意字符串
 9matcher.addURI("cn.tim.myprovider", "world/*", 1003);
10
11return true;

Uri与Uri自带的解析方法

使用Uri自带的解析方法

现在ContentAcquireDemo假设添加方法是这样调用的,即把参数写在Uri里面,这样在ContentProviderDemo工程的MyContentProvider中又是如何解析的呢?

1Uri insertUri = Uri.parse("content://cn.tim.myprovider/whatever?name=Tim&age=22&gender=男");
2Uri uri = contentResolver.insert(insertUri, new ContentValues());
3long newId = ContentUris.parseId(uri);
4Toast.makeText(this, "添加成功: Id" + newId, Toast.LENGTH_SHORT).show();

MyContentProvider.java的关键代码:

 1@Override
 2public Uri insert(Uri uri, ContentValues values) {
 3        long id = 0;
 4    	// 为了保持原来的方式不做变更,所以这里需要判断一下
 5        if(values.size() > 0){
 6            id = sqLiteDatabase.insert("stu_info", null, values);
 7        }else {
 8            String authority = uri.getAuthority();
 9            String path = uri.getPath();
10            String query = uri.getQuery();
11            String name = uri.getQueryParameter("name");
12            String age = uri.getQueryParameter("age");
13            String gender = uri.getQueryParameter("gender");
14            Log.i(TAG, "insert:->主机名:" + authority
15                    + ",路径:" + path + ",查询数据:" + query + ",姓名:" + name
16            + ",age:" + age + ",gender:" + gender);
17            values.put("name", name);
18            values.put("age", age);
19            values.put("gender", gender);
20            id = sqLiteDatabase.insert("stu_info", null, values);
21        }
22        return ContentUris.withAppendedId(uri, id);    
23}

果然通过这样的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结构与代码提取》

使用系统的ContentProvider

下面是通过读写系统通讯录和读取短信的几个小例子,作为ContentProvider使用练习:

读取通讯录

 1public void visitAddressBook(View view) {
 2    ContentResolver resolver = getContentResolver();
 3    //联系人姓名 + Id
 4    Uri uri = ContactsContract.Contacts.CONTENT_URI;
 5    //联系人电话
 6    Uri uriPhone = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
 7    Cursor cursor = resolver.query(uri, null, null, null, null);
 8    while(cursor!= null && cursor.moveToNext()){
 9        String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
10        String contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID));
11        Log.i(TAG, "visitAddressBook: name = " + name + ", id = " + contactId);
12        String selection = ContactsContract.CommonDataKinds.Phone.CONTACT_ID + "=" +contactId;
13        Cursor phoneCursor = resolver.query(uriPhone,null, selection, null, null);
14        while (phoneCursor != null && phoneCursor.moveToNext()){
15            String phone = phoneCursor.getString(phoneCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
16            Log.i(TAG, "visitAddressBook: name = " + name + ", phone = " + phone);
17        }
18        if(phoneCursor != null) phoneCursor.close();
19    }
20    if(cursor != null) cursor.close();
21}

添加通讯录

 1public class MainActivity extends AppCompatActivity {
 2    private static final String TAG = "MainActivity";
 3    // 申请权限请求码
 4    private static final int REQUEST_READ_SMS = 1001;
 5
 6    // 检查权限,这种写法主要是针对比较新的Android6.0及以后的版本
 7    public static void verifyStoragePermissions(Activity activity) {
 8        int smsPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_SMS);
 9        int contactsPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS);
10        int writeContactsPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_CONTACTS);
11
12        if (smsPermission != PackageManager.PERMISSION_GRANTED
13                || contactsPermission != PackageManager.PERMISSION_GRANTED
14                || writeContactsPermission != PackageManager.PERMISSION_GRANTED) {
15            // 如果没有权限需要动态地去申请权限
16            ActivityCompat.requestPermissions(
17                    activity,
18                    // 权限数组
19                    new String[]{Manifest.permission.READ_SMS, Manifest.permission.READ_CONTACTS,  Manifest.permission.WRITE_CONTACTS},
20                    // 权限请求码
21                    REQUEST_READ_SMS
22            );
23        }
24    }
25
26    @Override
27    protected void onCreate(Bundle savedInstanceState) {
28        super.onCreate(savedInstanceState);
29        setContentView(R.layout.activity_main);
30        verifyStoragePermissions(this);
31    }
32
33    public void addAddressBook(View view) {
34        //1、往一个ContentProvider中插入一条空数据,获取新生成的Id
35        //2、利用刚刚生成的Id分别组合姓名和电话号码往另一个ContentProvider中插入数据
36        ContentValues values = new ContentValues();
37        ContentResolver resolver = getContentResolver();
38        Uri uri = resolver.insert(ContactsContract.RawContacts.CONTENT_URI, values);
39        if(uri == null) throw new RuntimeException("插入新联系人失败");
40        values.clear();
41        long id = ContentUris.parseId(uri);
42        // 插入姓名
43        values.put(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, id);
44        values.put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, "Mike");
45        values.put(ContactsContract.CommonDataKinds.StructuredName.MIMETYPE,
46                ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
47        uri = resolver.insert(ContactsContract.Data.CONTENT_URI, values);
48        if(uri != null) Log.i(TAG, "addAddressBook: 插入姓名,id = " + ContentUris.parseId(uri));
49        //插入电话信息
50        values.clear();
51        values.put(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, id);
52        values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, "15720918678"); //添加号码
53        values.put(ContactsContract.CommonDataKinds.Phone.MIMETYPE,
54                ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
55        values.put(ContactsContract.CommonDataKinds.Phone.TYPE,
56                ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE); //添加号码类型
57        uri = resolver.insert(ContactsContract.Data.CONTENT_URI, values);
58        if(uri != null) Log.i(TAG, "addAddressBook: 插入电话号码,id = " + ContentUris.parseId(uri));
59    }
60}

读取短信

短信类型 Uri
短信箱 content://sms
收件箱 content://sms/inbox
发件箱 content://sms/sent
草稿箱 content://sms/draft
 1public class MainActivity extends AppCompatActivity {
 2    private static final String TAG = "MainActivity";
 3    // 申请权限请求码
 4    private static final int REQUEST_READ_SMS = 1001;
 5
 6    // 检查权限,这种写法主要是针对比较新的Android6.0及以后的版本
 7    public static void verifyStoragePermissions(Activity activity) {
 8        int smsPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_SMS);
 9        int contactsPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS);
10
11        if (smsPermission != PackageManager.PERMISSION_GRANTED
12                || contactsPermission != PackageManager.PERMISSION_GRANTED) {
13            // 如果没有权限需要动态地去申请权限
14            ActivityCompat.requestPermissions(
15                    activity,
16                    // 权限数组
17                    new String[]{Manifest.permission.READ_SMS, Manifest.permission.READ_CONTACTS},
18                    // 权限请求码
19                    REQUEST_READ_SMS
20            );
21        }
22    }
23
24    @Override
25    protected void onCreate(Bundle savedInstanceState) {
26        super.onCreate(savedInstanceState);
27        setContentView(R.layout.activity_main);
28        verifyStoragePermissions(this);
29    }
30
31    public void visitMessage(View view) {
32        ContentResolver resolver = getContentResolver();
33        Uri uri = Uri.parse("content://sms");
34        Cursor cursor = resolver.query(uri, null, null, null, null);
35        while(cursor != null && cursor.moveToNext()){
36            int addressIndex = cursor.getColumnIndex("address");
37            int bodyIndex = cursor.getColumnIndex("body");
38            String address = cursor.getString(addressIndex);
39            String body = cursor.getString(bodyIndex);
40            Log.i(TAG, "visitMessage:" + address + ":" + body);
41        }
42        if(cursor != null) cursor.close();
43    }
44}

在AndroidManifest.xml配置一下权限:

1<uses-permission android:name="android.permission.READ_SMS"/>

ContentProvider的优点

ContentProvider的底层实现是Binder,更多关于Binder的内容可以参考官方文档 《Binder》 。ContentProvider为应用间的数据交互提供了一个安全的环境:允许把自己的应用数据根据需求开放给其他应用进行CRUD,而不用担心因为直接开放数据库权限而带来的安全问题。而其他对外共享数据的方式,数据访问方式会因数据存储的方式而不同而发生变化,底层存储方式变更会影响上层,使访问数据变得更加复杂。而采用ContentProvider方式,其解耦了底层数据的存储方式,使得无论底层数据存储采用何种方式,外界对数据的访问方式都是统一的,这使得访问简单且高效。

原文地址: 《探究ContentProvider》