为什么微信、QQ、淘宝等App都能访问联系人(通讯录)呢?是因为Android存在一种应用之间的数据共享机制,即ContentProvider,ContentProvider作为Android四大组件之一,为存储和获取数据提供统一的接口,可以在不同的应用程序之间共享数据。对于ContentProvier而言,无论数据的来源是什么,它都认为是种表(同时也支持文件数据,只是表格形式用得比较多),然后把数据组织成表格返回给使用者。
自定义ContentProvider step1、自定义类继承于ContentProvider,实现要求的方法 step2、在配置文件中通过provider标签配置,通过android:name属性指定待配置的类,通过android:authorities属性授权,指定当前内容提供者的uri标识,必须唯一。
下面来展示一个B应用来操作A应用中的数据的例子:
首先在ContentProviderDemo这个工程里写一个名为MyContentProvider的ContentProvider:
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 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) { 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; } @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中声明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?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:name =".MyContentProvider" 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:
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 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 = 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<>(); 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(); 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匹配器,并且给出匹配规则如下:
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 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 ; } ... @Override public boolean onCreate () { ... sqLiteDatabase = helper.getWritableDatabase(); matcher = new UriMatcher(UriMatcher.NO_MATCH); matcher.addURI("cn.tim.myprovider" , "hello" , 1001 ); return true ; } ... }
这样在另一个App中使用MyContentProvider的delete()方法的时候就会进行URI匹配判断:
1 contentResolver.delete(Uri.parse("content://cn.tim.myprovider/hello" ), null , null );
大家在今后的开发中可能会用到更多的匹配模式,接下来我们学习UriMatcher更多匹配:
UriMatcher还可以使用匹配通配符来匹配任意不确定的值:
1 2 3 4 5 6 7 8 9 10 11 matcher = new UriMatcher(UriMatcher.NO_MATCH); matcher.addURI("cn.tim.myprovider" , "hello" , 1001 ); matcher.addURI("cn.tim.myprovider" , "hello/#" , 1002 ); matcher.addURI("cn.tim.myprovider" , "world/*" , 1003 ); return true ;
Uri与Uri自带的解析方法 使用Uri自带的解析方法 现在ContentAcquireDemo假设添加方法是这样调用的,即把参数写在Uri里面,这样在ContentProviderDemo工程的MyContentProvider中又是如何解析的呢?
1 2 3 4 Uri insertUri = Uri.parse("content://cn.tim.myprovider/whatever?name=Tim&age=22&gender=男" ); Uri uri = contentResolver.insert(insertUri, new ContentValues()); long newId = ContentUris.parseId(uri);Toast.makeText(this , "添加成功: Id" + newId, Toast.LENGTH_SHORT).show();
MyContentProvider.java的关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @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结构与代码提取》 。
使用系统的ContentProvider 下面是通过读写系统通讯录和读取短信的几个小例子,作为ContentProvider使用练习:
读取通讯录 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void visitAddressBook (View view) { ContentResolver resolver = getContentResolver(); 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(); }
添加通讯录 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 public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity" ; private static final int REQUEST_READ_SMS = 1001 ; 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) { 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
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 public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity" ; private static final int REQUEST_READ_SMS = 1001 ; 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配置一下权限:
1 <uses-permission android:name ="android.permission.READ_SMS" />
ContentProvider的优点 ContentProvider的底层实现是Binder,更多关于Binder的内容可以参考官方文档《Binder》 。ContentProvider为应用间的数据交互提供了一个安全的环境:允许把自己的应用数据根据需求开放给其他应用进行CRUD,而不用担心因为直接开放数据库权限而带来的安全问题。而其他对外共享数据的方式,数据访问方式会因数据存储的方式而不同而发生变化,底层存储方式变更会影响上层,使访问数据变得更加复杂。而采用ContentProvider方式,其解耦了底层数据的存储方式,使得无论底层数据存储采用何种方式,外界对数据的访问方式都是统一的,这使得访问简单且高效。
原文地址:《探究ContentProvider》