浅谈ContentProvider

ContentProvider即内容提供者,它是Android系统中提供的专门用于不同应用间进行数据共享的组件。ContentProvider提供了一套标准的接口来获取及操作数据,准许开发者把自己的应用数据根据需求开放给其他应用进行增删改查,而无须担心直接开放数据库权限而带来的安全问题。系统预置了许多ContentProvider用于获取用户数据,比如消息、联系人、日程表等。

在以前介绍其它组件时说过,除了BroadcastReceiver之外,其它组件都需要在清单文件中进行注册,因此ContentProvider也不例外。

注册ContentProvider是通过provider标记实现的,provider标记中包含一个描述Provider类名的name属性和一个authorities属性。

一般provider的标记如下格式。

<provider
	android:name=".provider.PersonContentProvider"
	android:authorities="com.sunny.demo.provider"
	android:exported="true" />

android:exported

虽然ContentProvider就是为了在不同应用间共享数据而存在的,但是它的android:exported值默认却不一定就是true,所以为了防止因疏忽而导致无法访问当前应用的数据,建议注册时直接显式赋值为true。

  • true:当前提供者可以被其它应用使用。任何应用可以使用Provider通过URI来获得它,也可以通过相应的权限来使用Provider。
  • false:当前提供者不能被其它应用使用。设置android:exported="false"来限制其它应用获得应用的Provider。只有拥有同样的user ID 的应用可以获得当前应用的Provider。

当Android SDK的最小版本为16或者更低时他的默认值是true。如果是17和以上的版本默认值是false。

authorities和Content URI

authorities

用于唯一标识某个ContentProvider,外部调用者根据这个标识来指定要操作的ContentProvider。authorities一定要唯一,所以建议和应用的包名组合使用,如果一个应用需要对外提供多个ContentProvider,也可以直接使用ContentProvider的全类名做标识。

Content URI

第三方应用时如何访问当前应用ContentProvider提供的数据接口呢?其实是通过一个内容URI。每个ContentProvider都会通过一个公有静态的Content URI属性公开它的授权。

一般Content URI格式如下:

  • content://<authority>/<path>/<id>
<authority>内容就是上文的authorities;<path>数据类型地址,标识当前ContentProvider想要提供的

数据类型,如果想对外提供的是Person对象类型,就可以使用person,一般使用小写的形式。<id>就是指定的特定的请求记录,假设某一个人的ID是5,那么URI后面看起来应该是这样的:content://xxxx/person/5。

CONTENT_URI跟HTTP协议的接口地址很相似,特别是跟现在很推崇使用的的REST API很相似。

private static final String AUTHORITY = "com.sunny.demo.provider";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/person");

Content URI支持通配符格式:

  • *: 适配任意长度的字符串类型;
  • #: 适配任意长度的数值类型。

假如一个Content URI的格式如下:

  • content://com.sunny.demo.provider/*

那么它可以适配任意Content URI。

  • content://com.sunny.demo.provider/person/*

那么该Content URI可以适配如下格式:

  • content://com.sunny.demo.provider/person/table01
  • content://com.sunny.demo.provider/person/table02

但是不能适配如下格式:

  • content://com.sunny.demo.provider/person01/table03

  • content://com.sunny.demo.provider/person/#

那么该Content URI可以适配如下格式:

  • content://com.sunny.demo.provider/person/6

UriMatcher和ContentUris

Android系统提供了用于操作URI的工具类,它们分别是UriMatcher和ContentUris,在使用ContentProvider时会经常用到这两个类。

UriMatcher

UriMatcher主要用于匹配Uri,使用方式如下:

//1、初始化UriMatcher。
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);

//2、注册需要的Uri
static {
	URI_MATCHER.addURI(AUTHORITY, "person", CODE_PERSONS);
	URI_MATCHER.addURI(AUTHORITY, "person/#", CODE_PERSON);
}

//3、与已经注册的Uri匹配
public String getType(Uri uri) {
	int flag = URI_MATCHER.match(uri);
	switch (flag) {
		case CODE_PERSONS:
			return "vnd.android.cursor.dir/persons";
		case CODE_PERSON:
			return "vnd.android.cursor.item/person";
	}
	return null;
}

UriMatcher的match()方法会返回一个匹配码,该匹配码是在addURI()方法注册时的第3个入参。

在ContentProvider中getType()方法用于返回某个指定MIME类型字符串,返回类型主要包括两种形式,一种表示单条数据,另一种表示多条数据。

如果是单条记录,应该返回以vnd.android.cursor.item/ 为首的字符串; 如果是多条记录,应该返回以vnd.android.cursor.dir/ 为首的字符串。

getType()方法可以说有两个主要作用,第一个作用是当第三方应用调用当前应用时,可以使用getType()方法简单的判断一下MIME类型,然后根据MIME类型执行不同的逻辑。第二个作用就是第三方应用可以通过ContentResolver的getType()方法得到当前应用给出的MIME类型。

Uri uri=Uri.parse("content://" + AUTHORITY + "/person/10");
Log.d(TAG,"=====>"+contentResolver.getType(uri));
//=====>vnd.android.cursor.item/person

ContentUris

ContentUris用于处理Uri路径的ID部分。

为Uri添加一个ID。

Uri result = ContentUris.withAppendedId(CONTENT_URI, id);

从Uri中获取ID。

long id = ContentUris.parseId(uri);

通过Uri获取ID还有其它方式。

long id = uri.getPathSegments().get(1);

get(1)中的1的含义是把Uri中以/作为分割,0部分是路径,1部分则是ID。

获取调用者的包名

如果第三方应用通过ContentProvider拉起了当前应用,可以通过如下方式拿到调用者的包名信息。

//方式一
String callPkg = getCallingPackage();
Log.d(TAG, "=======calling package:" + callPkg);

//方式二
int uid=Binder.getCallingUid();
Log.d(TAG, "====calling id:" + uid);
PackageManager pm=getContext().getPackageManager();
String[] callingPkg=pm.getPackagesForUid(uid);
for(int i=0;i<callingPkg.length;i++){
	Log.d(TAG, "====calling package:" + callingPkg[i]+"  ");
}

//方式三
String cPkg=pm.getNameForUid(uid);
Log.d(TAG, "=========calling package:" + cPkg+"  ");

线程关系

在ContentProvider中,除了onCreate()方法位于主线程,其余的几个包括增删改查的方法:getType()、insert()、delete()、update()、query()都是位于Binder线程池中。

由于onCreate()方法位于主线程中,所以不建议在onCreate()中做太多复杂的操作,以免影响应用的启动。

ContentResolver

在使用ContentProvider过程中,需要借助另外一个类ContentResolver,ContentResolver类可以用于处理ContentProvider所暴露的接口,作为代理来间接操作ContentProvider以获取数据。

在 Context.java 的源码中如下抽象方法

/** Return a ContentResolver instance for your application's package. */
public abstract ContentResolver getContentResolver();

所以可以在所有继承Context的类中通过getContentResovler()方法获取ContentResolver。

ContentResolver contentResolver = getContentResovler();

ContentProvider中的几个抽象方法在ContentResolver中均有着一一对应的同名方法。

ContentProvider生命周期

Android应用程序至少包含一个Application,而且Application声明周期的onCreate()方法执行会在其它组件的onCreate()方法之前,但是ContentProvider确实是例外,在四大组件中只有该组件是个特殊的存在,ContentProvider的onCreate()方法的执行会在Application的onCreate()之前执行。如果不常使用ContentProvider,相信多少会在这里踩坑。

ContentProvider示例

如果使用过ContentProvider,可以发现它所提供的方法天生就是匹配SQLite数据操作的,一般建议与数据库结合并借助SQLiteOpenHelper类使用,当然了也可以使用其它格式的数据,只要实现了ContentProvider对应的方法即可,因为外部使用的时候只是接口调用方式,并不关心接口的实现。

在本示例中也是使用的SQLiteOpenHelper类,因为该类可以有效的延迟创建和打开数据库,直到使用的时候才会创建和打开数据库。

实现ContentProvider

public class PersonContentProvider extends ContentProvider {
	private static final String TAG = "PersonContentProvider";
    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
    private static final String AUTHORITY = "com.sunny.demo.provider.PersonContentProvider";
    private static final String ID = "id";
    private static final int CODE_PERSONS = 1;
    private static final int CODE_PERSON = 2;
	public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/person");
	private DBHelper dbHelper;

    static {
        URI_MATCHER.addURI(AUTHORITY, "person", CODE_PERSONS);
        URI_MATCHER.addURI(AUTHORITY, "person/#", CODE_PERSON);
    }

    @Override
    public boolean onCreate() {
        Log.d(TAG, "====PersonContentProvider onCreate");
        dbHelper = new DBHelper(getContext());
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        String callPkg = getCallingPackage();
        Log.d(TAG, "=======calling package:" + callPkg);
        
        SQLiteDatabase database = dbHelper.getReadableDatabase();
        int flag = URI_MATCHER.match(uri);
        Cursor cursor;
        switch (flag) {
            case CODE_PERSON:
                long id = ContentUris.parseId(uri);
                selection += ID + "=" + id + (TextUtils.isEmpty(selection) ? "" : " and (" + selection + ")");
                break;
        }
        cursor = database.query(DBHelper.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
        return cursor;
    }

    @Override
    public String getType(Uri uri) {
        int flag = URI_MATCHER.match(uri);
        switch (flag) {
            case CODE_PERSONS:
                return "vnd.android.cursor.dir/persons";
            case CODE_PERSON:
                return "vnd.android.cursor.item/person";
        }
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues contentValues) {
        SQLiteDatabase database = dbHelper.getWritableDatabase();
        long id = database.insert(DBHelper.TABLE_NAME, null, contentValues);
        //注意这里uri,并不是入参的
        Uri result = ContentUris.withAppendedId(CONTENT_URI, id);
        Log.d(TAG, "===>" + result.toString());
        return result;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        SQLiteDatabase database = dbHelper.getWritableDatabase();
        int flag = URI_MATCHER.match(uri);
        int deleteCount = 0;
        switch (flag) {
            case CODE_PERSON:
                long id = ContentUris.parseId(uri);
                selection += ID + "=" + id + (TextUtils.isEmpty(selection) ? "" : " and (" + selection + ")");
                deleteCount = database.delete(DBHelper.TABLE_NAME, selection, selectionArgs);
                break;
        }
        return deleteCount;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        SQLiteDatabase database = dbHelper.getWritableDatabase();
        int flag = URI_MATCHER.match(uri);
        int updateCount = 0;
        switch (flag) {
            case CODE_PERSON:
                long id = ContentUris.parseId(uri);
                selection += ID + "=" + id + (TextUtils.isEmpty(selection) ? "" : " and (" + selection + ")");
                updateCount = database.update(DBHelper.TABLE_NAME, values, selection, selectionArgs);
                break;
        }
        return updateCount;
    }
}

测试ContentProvider

@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
    @Test
    public void useAppContext() {
        // Context of the app under test.
        Context appContext = InstrumentationRegistry.getTargetContext();
        assertEquals("com.sunny.demo", appContext.getPackageName());
    }

    @Test
    public void insert() {
        Context appContext = InstrumentationRegistry.getTargetContext();
        ContentResolver resolver=appContext.getContentResolver();

        ContentValues values=new ContentValues();
        values.put("name","admin01");
        values.put("age",21);
        resolver.insert(PersonContentProvider.CONTENT_URI,values);
    }

    @Test
    public void query() {
        Context appContext = InstrumentationRegistry.getTargetContext();
        ContentResolver resolver=appContext.getContentResolver();
        Cursor cursor=resolver.query(PersonContentProvider.CONTENT_URI,null,null,null,null);
        while(cursor.moveToNext()){
            int id=cursor.getInt(cursor.getColumnIndex("id"));
            int age=cursor.getInt(cursor.getColumnIndex("age"));
            String name=cursor.getString(cursor.getColumnIndex("name"));
            Log.d(TAG, "===> id:"+id+" name:"+name+" age:"+age);
        }
    }
}

观察ContentProvider数据变化

当ContentProvider中数据有变化时,如果通知第三方应用呢,这一点不用担心,ContentProvider提供了相应的方法用于监听数据变化。

假设需要在insert()方法中监听添加数据变化,只需要添加一个如下方法即可。

@Override
public Uri insert(Uri uri, ContentValues contentValues) {
	...
	// 监听数据变化 
	getContext().getContentResolver().notifyChange(CONTENT_URI, null);
	return result;
}

然后在第三方应用中实现一个ContentObserver的类,为指定的Uri注册一个ContentObserver派生类实例,当给定的Uri发生改变时,回调该实例对象去处理。

代码如下:

public class MainActivity extends AppCompatActivity {
	private static final String TAG = "MainActivity";
    private static final String AUTHORITY = "com.sunny.demo.provider.PersonContentProvider";
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/person");

    private TextView textView;
    private ContentResolver contentResolver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.textView);
		
        contentResolver=getContentResolver();
        contentResolver.registerContentObserver(CONTENT_URI, true, new MyContentObserver(new Handler()));
    }
    private class MyContentObserver extends ContentObserver {
        public MyContentObserver(Handler handler) {
            super(handler);
        }

        @Override
        public void onChange(boolean selfChange) {
            Log.d(TAG, "onChange selfChange:" + selfChange);
            setTextInfo("on Change \n");
        }
    }
    private void setTextInfo(String from) {
        StringBuffer buffer = new StringBuffer();
        buffer.append(from);
        Cursor cursor = contentResolver.query(CONTENT_URI, null, null, null, null);
        if(cursor==null){
            Log.e(TAG, "cursor====null");
            return;
        }
        while (cursor.moveToNext()) {
            int id = cursor.getInt(cursor.getColumnIndex("id"));
            int age = cursor.getInt(cursor.getColumnIndex("age"));
            String name = cursor.getString(cursor.getColumnIndex("name"));
            Log.d(TAG, "===> id:" + id + " name:" + name + " age:" + age);
            buffer.append("id:" + id + " name:" + name + " age:" + age + " \n");
        }
        textView.setText(buffer.toString());
    }
}

评论

您确定要删除吗?删除之后不可恢复