原文出处:http://www.ccbu.cc/index.php/android/android-contentprovider.html
ContentProvider中文名“内容提供者”,是Android系统不同应用程序之间进行数据交换的标准API,ContentProvide以Uri的形式对外提供数据,允许其他应用访问和修改数据;其他应用使用ContentResolve根据Uri进行访问操作指定的数据。Android内置的许多数据也都是使用ContentProvider形式,供开发者调用的;如视频,音频,图片,通讯录等。
访问ContentProvider都是统一通过Uri进行访问的,URI(Uniform Resource Identifier
,即统一资源标识符)由三部分组成,即协议(scheme),所有者(authority),路径(path)。
在ContentProvider中,scheme统一为content:// ;authority用来作为当前Provider的唯一标识,一般以公司域名+数据标识; Path为具体的数据路径,一般为 “数据类型 / 数据ID” 的形式 。如上面例子中的100即为id。
既然ContentProvider需要通过RUI进行访问,所以android系统提供了UriMatcher来进行Uri的匹配处理。提供了以下两个接口,一个用来添加匹配规则,一个用来获取匹配结果。
public void addURI(String authority, String path, int code)
public int match(Uri uri)
定义匹配规则
public static final int ID1 = 1;
public static final int ID2 = 2;
public static final String HOST = "com.test.provider.DataContentProvider";
public static final String PATH = "students";
private static UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
uriMatcher.addURI(HOST,PATH, ID1);
uriMatcher.addURI(HOST,PATH + "/#", ID2); // # 为通配符
}
匹配路径
switch (uriMatcher.match(Uri uri)) {
case ID1:
break;
case ID2:
break;
default:
break;
}
如匹配的uri为:content://com.test.provider.DataContentProvider/students/10 , 则得到的匹配码为ID2。
这个类是一个操作Uri字符串的工具类,主要是拼接Uri字符串用,它有两个方法:
public static Uri withAppendedId(Uri uri, long id) // 用于为路径加上id部分
public static long parseId(Uri uri) // 用于从指定的Uri中解析出所包含的id
Uri uri = Uri.parse("content://com.test.provider.DataContentProvider/students");
Uri newUri = ContentUris.withAppendedId(uri, 100);
Uri uri = Uri.parse("content://com.test.provider.DataContentProvider/students/100")
long personid = ContentUris.parseId(uri); // 获取的结果为:100
ContentProvider类是一个抽象类,具体的实现类需要实现其主要的几个虚函数,也正是通过这些虚函数的实现来完成具体的ContentProvider的具体功能的。主要的函数如下:
函数 | 说明 |
---|---|
boolean onCreate() | ContentProvider创建后就会被调用,ContentProvider在其它应用第一次访问它时才会被创建 |
Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder) | 用于供外部应用从ContentProvider中获取数据 |
String getType(Uri uri) | 用于返回当前Url所代表数据的MIME类型 |
Uri insert(Uri uri, ContentValues values) | 用于供外部应用往ContentProvider添加数据 |
int delete(Uri uri, String selection, String[] selectionArgs) | 用于供外部应用从ContentProvider删除数据 |
int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) | 用于供外部应用更新ContentProvider中的数据 |
下面通过一个实列来展示ContentProvider的实现。
public class StudentProvider extends ContentProvider {
public static final int TYPE_ITEM = 1;
public static final int TYPE_TABLE = 2;
public static final String HOST = "cc.ccbu.provider.StudentProvider";
public static final String PATH = "students";
private static UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
public static final String CONTENT_TYPE_ITEM = "vnd.android.cursor.dir/students.item";
public static final String CONTENT_TYPE_TABLE = "vnd.android.cursor.item/students.table";
static {
uriMatcher.addURI(HOST, PATH, TYPE_TABLE);
uriMatcher.addURI(HOST,PATH + "/#", TYPE_ITEM);
}
private DbOpenHelper dbOpenHelper;
@Override
public boolean onCreate() {
dbOpenHelper = new DbOpenHelper(getContext());
return true;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)){
case TYPE_ITEM:
return CONTENT_TYPE_ITEM;
case TYPE_TABLE:
return CONTENT_TYPE_TABLE;
default:
throw new IllegalArgumentException("this is unknown uri:" + uri);
}
}
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case TYPE_TABLE:
long id = db.insert(DbOpenHelper.STUDENT_TABLE_NAME, null, values);
Uri insertUri = ContentUris.withAppendedId(uri, id);
return insertUri;
}
return null;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case TYPE_TABLE:
return db.query(DbOpenHelper.STUDENT_TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
case TYPE_ITEM:
long id = ContentUris.parseId(uri);
String where = "_id = " + id;
if (null != selection && !"".equals(selection.trim()))
{
where += " and " + selection;
}
return dbOpenHelper.getWritableDatabase().query(DbOpenHelper.STUDENT_TABLE_NAME, projection, where, selectionArgs, null, null, sortOrder);
}
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case TYPE_TABLE:
return db.delete(DbOpenHelper.STUDENT_TABLE_NAME, selection, selectionArgs);
case TYPE_ITEM:
long id = ContentUris.parseId(uri);
String where = "_id = " + id;
if (null != selection && !"".equals(selection.trim()))
{
where += " and " + selection;
}
return db.delete(DbOpenHelper.STUDENT_TABLE_NAME, where, selectionArgs);
}
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case TYPE_TABLE:
return db.update(DbOpenHelper.STUDENT_TABLE_NAME, values, selection, selectionArgs);
case TYPE_ITEM:
long id = ContentUris.parseId(uri);
String where = "_id = " + id;
if (null != selection && !"".equals(selection.trim()))
{
where += " and " + selection;
}
return db.update(DbOpenHelper.STUDENT_TABLE_NAME, values, where, selectionArgs);
}
return 0;
}
}
Provider类写好后还需要在AndroidManifest.xml中进行注册。为了能使其他应用可以访问ContentProvider中的数据,需要在xml中将android:exported属性设置为true;若设置成false,则只能被自己所在的应用使用。
<provider
android:authorities="cc.ccbu.provider.StudentProvider"
android:name=".StudentProvider"
android:enabled="true"
android:exported="true"/>
下面是一个简单的测试例子。展示了常见的insert, update, qurey, delete操作。
public class MainActivity extends Activity {
public static final String AUTHORITY = "cc.ccbu.provider.StudentProvider";
public static final String STUDENT = "students";
private static final String TAG = "MainActivity";
private Uri uri;
private long insertId = -1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
uri = Uri.parse("content://" + AUTHORITY + "/" + STUDENT);
onInsert();
onQuery();
onUpdate();
onQuery();
onDelete();
onQuery();
}
public void onInsert() {
ContentValues cvs = new ContentValues();
cvs.put("name", "tom");
Uri uriRet = getContentResolver().insert(uri, cvs);
if (uriRet != null) {
insertId = ContentUris.parseId(uriRet);
Log.d(TAG, "onInsert : " + insertId);
}
}
public void onUpdate() {
if (insertId >= 0) {
Uri uriUpdate = ContentUris.withAppendedId(uri, insertId);
ContentValues cvs = new ContentValues();
cvs.put("name", "jack");
int updateId = getContentResolver().update(uriUpdate, cvs, null, null);
Log.d(TAG, "onUpdate : " + updateId);
}
}
public void onQuery(){
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
Log.d(TAG, "onQuery : " + cursor.getInt(0) + ", " + cursor.getString(1));
}
}
}
public void onDelete() {
Uri uriDelete = ContentUris.withAppendedId(uri, insertId);
getContentResolver().delete(uriDelete, null, null);
Log.d(TAG, "onDelete");
}
}
运行结果如下:
D/MainActivity: onInsert : 1
D/MainActivity: onQuery : 1, tom
D/MainActivity: onUpdate : 1
D/MainActivity: onQuery : 1, jack
D/MainActivity: onDelete
ContentProvider以Uri的形式为其他应用提供了简单的访问方式,通过次方式,外部应用可以很轻松的访问ContentProvider中的数据,但这些访问动作都是应用主动发起的,当ContentProvider中数据变化了之后,有没有办法被通知数据变化了呢,答案当时是有的。ContentObserver是系统为我们提供的ContentProvider数据监听的基类,需要监听ContentProvider的数据变化必须继承此类,从类名可以看出,此次的监听是以观察者模式来实现的。
private void testObserver() {
getContentResolver().registerContentObserver(uri, true, new ContentObserver(new Handler(Looper.getMainLooper())) {
@Override
public void onChange(boolean selfChange, Uri uri) {
Log.d(TAG, "onChange : " + uri.getPath());
}
});
}
ContentObserver在构造函数中需要提供一个Handler实例,当我们通过ContentResolver
的registerContentObserver
注册某一Uri的监听后,当此Uri的数据发生变化时,我们就可以在onChange
回调函数中收到通知。当然,需要完全实现此功能,在ContentProvider中还需要做些相应的处理,当ContentProvider中的数据发生变化时,需要通知数据变化了。
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case TYPE_TABLE:
long id = db.insert(DbOpenHelper.STUDENT_TABLE_NAME, null, values);
Uri insertUri = ContentUris.withAppendedId(uri, id);
getContext().getContentResolver().notifyChange(insertUri, null);
return insertUri;
default:
throw new IllegalArgumentException("this is unknown uri:" + uri);
}
}
上面的例子中,当插入新的数据时,将新数据的Uri通知出去;监听者也就可以收到想要的通知。
ContentProvider可以被其他应用公开的访问,同时也可以设置为只有本应用自己才可以访问,或者在对其他应用公开访问时设置访问权限。ContentProvider需要在provider
标签中设置android:permission
属性来设置访问权限。权限通常为一个字符串,为了与其他应用区分,防止重复,一般以应用的PackageName开头。
<provider
android:authorities="cc.ccbu.provider.StudentProvider"
android:name=".StudentProvider"
android:enabled="true"
android:exported="true"
android:permission="cc.ccbu.provider.StudentProvider"/>
同时需要在AndroidManifest.xml的根节点下用permission标签来申明权限。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.ccbu.provider">
<permission android:name="cc.ccbu.provider.StudentProvider"
android:label="StudentProvider"
android:protectionLevel="normal"/>
......
manifest>
此时,其他应用需要有权限访问此ContentProvider的数据,则需要在AndroidManifest.xml申请权限。
访问权限还可以进一步的细化,分为读权限和写权限。通过来设置android:readPermission
属性和android:writePermission
属性项来设置。
<provider
android:authorities="cc.ccbu.provider.StudentProvider"
android:name=".StudentProvider"
android:enabled="true"
android:exported="true"
android:readPermission="cc.ccbu.provider.StudentProvider.READ"
android:writePermission="cc.ccbu.provider.StudentProvider.WRITE"/>
同样的需要申明一下这两种权限
<permission android:name="cc.ccbu.provider.StudentProvider.READ"
android:label="StudentProvider.read"
android:protectionLevel="normal"/>
<permission android:name="cc.ccbu.provider.StudentProvider.WRITE"
android:label="StudentProvider.write"
android:protectionLevel="normal"/>
访问的应用则也需要在AndroidManifest
中进行对应的权限申请才可以进行访问。
另外,android:writePermission
与android:readPermission
权限的优先级比android:permission
的优先级高,所以设置了read和write权限后,访问方就必须申请相应的read和write权限,否则是无法访问的。