ContentProvider的一个重要的特点就是它是可以使得某些数据可以被跨进程访问,一般我们的数据库是不可跨进程被访问,因为数据库一般的数据是属于某个应用程序的,如果其他程序可以随意访问其数据库,这是很危险的,但是如果该应用程序的数据想分享给其他应用程序,那么就可以通过建立一个ContentProvider,规定一些安全机制,屏蔽一些比较重要的数据被访问,或是规定访问权限,比如只可读不可写等,使其他应用程序在有限制的前提下访问我们的数据。这样做就会起到对数据进行保护同时又能使得有用的数据能被分享的作用。
内容提供器(ContentProvider)主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。
目前,使用内容提供器是Android实现跨程序共享数据的标准方式。不同于文件存储和 SharedPreferences存储中的两种全局可读写操作模式,内容提供器可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。
内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据(常用到的是访问系统的数据,常见的有通讯录、日历、短信、多媒体等),另一种是创建自己的内容提供器给我们程序的数据提供外部访问接口。
当一个应用程序通过内容提供器对其数据提供了外部访问接口,任何其他的应用程序就都可以对这部分数据进行访问。
Android提供了ContentProvider,一个程序可以通过实现一个ContentProvider的抽象接口将自己的数据完全暴露出去,而且ContentProviders是以类似数据库中表的方式将数据暴露,也就是说ContentProvider就像一个“数据库”。那么外界获取其提供的数据,也就应该与从数据库中获取数据的操作基本一样,只不过是采用URI来表示外界需要访问的“数据库”。外部访问通过ContentResolver去访问并操作这些被暴露的数据。
ContentResolver的基本用法
可以通过 Context 中的 getContentResolver()方法获取到该类的实例。 ContentResolver中提供了一系列的方法用于对数据进行 CRUD操作,其中 insert()方法用于添加数据,update()方法用于更新数据,delete()方法用于删除数据,query()方法用于查询数据。
不同于 SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容 URI。内容 URI给内容提供器中的数据建立了唯一标识符,它主要由两部分组成,权限(authority)和路径(path)。权限是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名。比如某个程序的包名是 com.example.app,那么该程序对应的权限就可以命名为 com.example.app. provider。路径则是用于对同一应用程序中不同的表做区分的,通常都会添加到权限的后面。比如某个程序的数据库里存在两张表,table1和 table2,这时就可以将路径分别命名为/table1 和/table2,然后把权限和路径进行组合,内容 URI就变成了 com.example.app.provider/table1 和 com.example.app.provider/table2。不过,目前还很难辨认出这两个字符串就是两个内容 URI,我们还需要在字符串的头部加上协议声明。因此,内容 URI最标准的格式写法如下:
content://com.example.app.provider/table1
content://com.example.app.provider/table2
在得到了内容 URI字符串之后,我们还需要将它解析成 Uri对象才可以作为参数传入。 解析的方法也相当简单,代码如下所示:
Uri uri = Uri.parse("content://com.example.app.provider/table1")
只需要调用 Uri.parse()方法,就可以将内容 URI字符串解析成 Uri对象了。
更新操作:
读取系统联系人的例子:
public class MainActivity extends Activity {
ListView contactsView;
ArrayAdapter<String> adapter;
List<String> contactsList = new ArrayList<String>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
contactsView = (ListView) findViewById(R.id.contacts_view);
adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, contactsList);
contactsView.setAdapter(adapter);
readContacts();
}
private void readContacts() {
Cursor cursor = null;
try {
// 查询联系人数据
cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
while (cursor.moveToNext()) {
// 获取联系人姓名
String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
// 获取联系人手机号
String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
contactsList.add(displayName + "\n" + number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
}
}
为什么没有调用 Uri.parse()方法去解析一个内容 URI 字符串呢?这是因为 ContactsContract.CommonDataKinds.Phone类已经帮我们做好了封装,提供了一个CONTENT_URI 常量,而这个常量就是使用 Uri.parse()方法解析出来的结果。
接着我们对 Cursor对象进行遍历,将联系人姓名和手机号这些数据逐个取出,联系人姓名这一列对应的常量是ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,联系人手机号这一列对应的常量是 ContactsContract.CommonDataKinds.Phone.NUMBER.
ContentProvider创建自己的内容提供器
那么我们是如何自定义ContentProvider来对其他程序提供访问我们数据的接口的呢?
一般我们如果有数据想被跨进程共享,就可以定义个ContentProvider,并在该ContentProvider定义访问该数据的方法。
完成ContentProvider类,主要需要注意以下几点:
(01) ContentProvider的数据一般是以"数据库"或"网络数据"的方式存储的。如果是数据库,则需要实现SQLiteOpenHelper类。通过SQLiteOpenHelper类新建/管理数据库。
(02) ContentProvider主要是以Uri的形式方式访问的(也可以通过Intent)。要通过UriMatcher注册ContentProvider监听的Uri。
(03) ContentProvider是一个抽象类。当我们需要以继承ContentProvider的方式自定义ContentProvider时,需要实现query(), insert(), update(), delete(), getType(), onCreate()这六个函数。
通过新建一个类去继承ContentProvider的方式来创建一个自己的内容提供器。 ContentProvider类中有六个抽象方法,我们在使用子类继承它的时候,需要将这六个方法全部重写:
public class MyProvider extends ContentProvider {
@Override
public boolean onCreate() {
return false;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public String getType(Uri uri) {
return null;
}
}
可以看到,几乎每一个方法都会带有Uri这个参数,这个参数也正是调用ContentResolver 的增删改查方法时传递过来的。而现在,我们需要对传入的 Uri参数进行解析,从中分析出调用方期望访问的表和数据。回顾一下,一个标准的内容URI写法是这样的:
content://com.example.app.provider/table1
这就表示调用方期望访问的是 com.example.app这个应用的 table1表中的数据。除此之外,我们还可以在这个内容 URI的后面加上一个 id,如下所示:
content://com.example.app.provider/table1/1
这就表示调用方期望访问的是com.example.app这个应用的 table1表中id为1的数据。内容 URI的格式主要就只有以上两种,以路径结尾就表示期望访问该表中所有的数据,以id结尾就表示期望访问该表中拥有相应 id的数据。我们可以使用通配符的方式来分别匹配这两种格式的内容 URI,规则如下。
1. *:表示匹配任意长度的任意字符
2. #:表示匹配任意长度的数字
所以,一个能够匹配任意表的内容 URI格式就可以写成:
content://com.example.app.provider/*
而一个能够匹配 table1表中任意一行数据的内容 URI格式就可以写成:
content://com.example.app.provider/table1/#
接着,我们再借助UriMatcher这个类就可以轻松地实现匹配内容URI的功能。UriMatcher中提供了一个 addURI()方法,这个方法接收三个参数,可以分别把权限、路径和一个自定义代码传进去。说明通过addURI()就可以将URI注册到UriMatcher中,从而实现ContentProvider对URI的监听。这样,当调用 UriMatcher的 match()方法时,就可以将一个 Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是哪张表中的数据了。修改 MyProvider中的代码,如下所示:
在上面的代码中,在静态代码块里我们创建了 UriMatcher的实例,并调用 addURI()方法,将期望匹配的内容 URI格式传递进去,注意这里传入的路径参数是可以使用通配符的。然后当 query()方法被调用的时候,就会通过 UriMatcher的match()方法对传入的 Uri对象进行匹配,如果发现 UriMatcher中某个内容 URI格式成功匹配了该 Uri对象,则会返回相应的自定义代码,然后我们就可以判断出调用方期望访问的到底是什么数据了。
上述代码只是以query()方法为例做了个示范,其实insert()、update()、delete()这几个方法的实现也是差不多的,它们都会携带Uri这个参数,然后同样利用 UriMatcher的 match() 方法判断出调用方期望访问的是哪张表,再对该表中的数据进行相应的操作就可以了。
除此之外,还有一个方法你会比较陌生,即 getType()方法。它是所有的内容提供器都必须提供的一个方法,用于获取 Uri对象所对应的MIME类型。一个内容 URI所对应的 MIME 字符串主要由三部分组分,Android对这三个部分做了如下格式规定。
实现跨程序数据共享实例:
public class DatabaseProvider extends ContentProvider {
public static final int BOOK_DIR = 0;
public static final int BOOK_ITEM = 1;
public static final int CATEGORY_DIR = 2;
public static final int CATEGORY_ITEM = 3;
public static final String AUTHORITY = "com.example.databasetest.provider";
private static UriMatcher uriMatcher;
private MyDatabaseHelper dbHelper;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
}
@Override
public boolean onCreate() {
dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
// 查询数据
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
cursor = db.query("Book", projection, "id = ?", new String[]{bookId}, null, null, sortOrder);
break;
case CATEGORY_DIR:
cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
cursor = db.query("Category", projection, "id = ?", new String[]{categoryId}, null, null, sortOrder);
break;
default:
break;
}
return cursor;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// 添加数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
Uri uriReturn = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
case BOOK_ITEM:
long newBookId = db.insert("Book", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
break;
case CATEGORY_DIR:
case CATEGORY_ITEM:
long newCategoryId = db.insert("Category", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
break;
default:
break;
}
return uriReturn;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
// 更新数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
int updatedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
updatedRows = db.update("Book", values, selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
updatedRows = db.update("Book", values, "id = ?", new String[]{bookId});
break;
case CATEGORY_DIR:
updatedRows = db.update("Category", values, selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
updatedRows = db.update("Category", values, "id= ?", new String[]{categoryId});
break;
default:
break;
}
return updatedRows;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// 删除数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
int deletedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
deletedRows = db.delete("Book", selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
deletedRows = db.delete("Book", "id = ?", new String[]{bookId});
break;
case CATEGORY_DIR:
deletedRows = db.delete("Category", selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
deletedRows = db.delete("Category", "id = ?", new String[]{categoryId});
break;
default:
break;
}
return deletedRows;
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest. provider.book";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest. provider.book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest. provider.category";
case CATEGORY_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest. provider.category";
}
return null;
}
}
在 AndroidManifest.xml文件中注册:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.databasetest"
android:versionCode="1"
android:versionName="1.0">
……
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
……
<provider
android:name="com.example.databasetest.DatabaseProvider"
android:authorities="com.example.databasetest.provider">
</provider>
</application>
</manifest>
首先在类的一开始,同样是定义了四个常量,分别用于表示访问 Book表中的所有数据、访问 Book表中的单条数据、访问 Category 表中的所有数据和访问 Category表中的单条数据。
然后在静态代码块里对 UriMatcher进行了初始化操作,将期望匹配的几种 URI格式添加了进去。 接下来就是每个抽象方法的具体实现了。
先来看下 onCreate()方法,这个方法的代码很短,就是创建了一个 MyDatabaseHelper的实例,然后返回 true表示内容提供器初始化成功,这时数据库就已经完成了创建或升级操作。
接着看一下 query()方法,在这个方法中先获取到了 SQLiteDatabase的实例,然后根据传入的 Uri参数判断出用户想要访问哪张表,再调用 SQLiteDatabase的 query()进行查询,并将 Cursor对象返回就好了。注意当访问单条数据的时候有一个细节,这里调用了 Uri对象的 getPathSegments()方法,它会将内容 URI权限之后的部分以“/”符号进行分割,并把分割后的结果放入到一个字符串列表中,那这个列表的第 0个位置存放的就是路径,第 1个位置存 放的就是 id了。得到了 id之后,再通过 selection和 selectionArgs参数进行约束,就实现了查询单条数据的功能。
再往后就是insert()方法,同样它也是先获取到了 SQLiteDatabase的实例,然后根据传入的 Uri参数判断出用户想要往哪张表里添加数据,再调用 SQLiteDatabase的 insert()方法进行。添加就可以了。注意 insert()方法要求返回一个能够表示这条新增数据的 URI,所以我们还需要调用 Uri.parse()方法来将一个内容 URI解析成 Uri对象,当然这个内容 URI是以新增数据 的 id结尾的。
接下来就是 update()方法了,也是先获取 SQLiteDatabase的实例,然后根据传入的 Uri参数判断出用户想要更新哪张表里的数据,再调用 SQLiteDatabase的 update()方法进行更新就好了,受影响的行数将作为返回值返回。
下面是 delete()方法,这里仍然是先获取到 SQLiteDatabase的实例,然后根据传入的 Uri参数判断出用 户想要删除哪张表里的数据,再调用 SQLiteDatabase的 delete()方法进行删除就好了,被删除的行数将作为返回值返回。
最后是 getType()方法,这个方法中的代码完全是按照上一节中介绍的格式规则编写的,相信已经没有什么解释的必要了。
最后是注册代码,可以看到,这里我们使用了<provider>标签来对 DatabaseProvider这个内容提供器进行注册,在 android:name属性中指定了该类的全名,又在 android:authorities属性中指定了该内容提供器的权限。现在DatabaseTest这个项目就已经拥有了跨程序共享数据的功能了。
其他程序访问contentprovider实例:
现在DatabaseTest这个项目就已经拥有了跨程序共享数据的功能了,我们赶快来尝试一下。首先需要将 DatabaseTest程序从模拟器中删除掉,以防止上一章中产生的遗留数据对我们造成干扰。然后运行一下项目,将 DatabaseTest程序重新安装在模拟器上了。接着关闭掉 DatabaseTest 这个项目,并创建一个新项目 ProviderTest,我们就将通过这个程序去访问 DatabaseTest中的数据。
public class MainActivity extends Activity {
private String newId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button addData = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// 添加数据
Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
ContentValues values = new ContentValues();
values.put("name", "A Clash of Kings");
values.put("author", "George Martin");
values.put("pages", 1040);
values.put("price", 22.85);
Uri newUri = getContentResolver().insert(uri, values);
newId = newUri.getPathSegments().get(1);
}
});
Button queryData = (Button) findViewById(R.id.query_data);
queryData.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// 查询数据
Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndex("name"));
String author = cursor.getString(cursor.getColumnIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
double price = cursor.getDouble(cursor.getColumnIndex("price"));
Log.d("MainActivity", "book name is " + name);
Log.d("MainActivity", "book author is " + author);
Log.d("MainActivity", "book pages is " + pages);
Log.d("MainActivity", "book price is " + price);
}
cursor.close();
}
}
});
Button updateData = (Button) findViewById(R.id.update_data);
updateData.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// 更新数据
Uri uri = Uri.parse("content://com.example.databasetest. provider/book/" + newId);
ContentValues values = new ContentValues();
values.put("name", "A Storm of Swords");
values.put("pages", 1216);
values.put("price", 24.05);
getContentResolver().update(uri, values, null, null);
}
});
Button deleteData = (Button) findViewById(R.id.delete_data);
deleteData.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// 删除数据
Uri uri = Uri.parse("content://com.example.databasetest. provider/book/" + newId);
getContentResolver().delete(uri, null, null);
}
});
}
}
可以看到,我们分别在这四个按钮的点击事件里面处理了增删改查的逻辑。添加数据的时候,首先调用了 Uri.parse()方法将一个内容URI解析成Uri对象,然后把要添加的数据都存放到ContentValues对象中,接着调用 ContentResolver的 insert()方法执行添加操作就可以了。注意 insert()方法会返回一个 Uri 对象,这个对象中包含了新增数据的id,我们通过getPathSegments()方法将这个 id取出,稍后会用到它。
查询数据的时候,同样是调用了Uri.parse()方法将一个内容URI解析成Uri对象,然后调用 ContentResolver的 query()方法去查询数据,查询的结果当然还是存放在Cursor对象中的。之后对Cursor进行遍历,从中取出查询结果,并一一打印出来。
更新数据的时候,也是先将内容URI解析成 Uri对象,然后把想要更新的数据存放到ContentValues对象中,再调用 ContentResolver的 update()方法执行更新操作就可以了。注意这里我们为了不想让 Book表中其他的行受到影响,在调用 Uri.parse()方法时,给内容URI的尾部增加了一个 id,而这个 id正是添加数据时所返回的。这就表示我们只希望更新刚刚添加的那条数据,Book表中的其他行都不会受影响。
删除数据的时候,也是使用同样的方法解析了一个以 id 结尾的内容 URI,然后调用 ContentResolver的delete()方法执行删除操作就可以了。由于我们在内容 URI里指定了一个 id,因此只会删掉拥有相应id的那行数据,Book表中的其他数据都不会受影响。