四、使用内容供应器

到目前为止,我们在这本书里已经完成了很多!在短短的三章中,我们研究了数据存储机制,从简单、不起眼的 SharedPreferences类,到强大而复杂的 SQLite 数据库,这些数据库配备了各种查询方法和利用同样强大的 SQL 语言的类。

但是,假设您已经掌握了最后三章,并且已经成功地为您的应用从头构建了一个数据库模式,该模式现在已经上市。现在,假设您想要创建第二个应用,该应用扩展了第一个应用的功能,并且需要访问原始应用的数据库。或者,您可能不需要创建第二个应用,但您只是想通过使您的数据库可供外部应用访问并集成到它们自己的数据库中来更好地营销您的应用。

或者,也许你甚至从未想过建立自己的数据库,而是只想利用每台安卓设备上已经存在的丰富数据,这些数据随时可供查询!在本章中,我们将学习如何使用 ContentProvider类完成所有这些事情,最后,我们将花一些时间集思广益,讨论为什么通过 ContentProvider公开您的数据库模式会让您受益的实际用例。

内容提供者

让我们从问题开始:到底是什么ContentProvider?为什么我需要和这个 ContentProvider? 互动

一个 ContentProvider本质上是一个接口,位于开发者和数据库模式之间,期望的数据位于其中。为什么需要这个中介接口?考虑以下(真实的)场景:

在安卓操作系统中,用户的联系人列表(包括电话号码、地址、生日和与联系人相关的许多其他数据字段)存储在用户设备上相当复杂的数据库模式中。考虑这样一个场景,作为一名开发人员,我希望在这个模式中查询用户的联系人电话号码。

想想为了访问一两个字段,我必须学习整个数据库的模式,这有多不方便?或者,如果每次谷歌更新安卓操作系统并调整联系人模式(相信我,这已经发生了几次),我都必须重新学习模式并随后重新构建查询,这将会有多不方便?

正是因为这些原因,这样一个中介才存在——因此,人们不需要直接与模式交互,只需要通过内容供应器进行查询。现在,在这一点上,每次谷歌更新其联系模式,他们需要确保他们重新调整 Contacts内容供应器的实现;否则我们通过内容供应器的查询可能会失败。

换句话说,本章的大部分内容及其对 ContentProvider类的实现将提醒您我们之前在为数据库编写便利方法时所做的工作。如果您选择通过内容供应器公开您的数据,您将需要定义外部应用如何查询您的数据,外部应用如何插入新数据或更新现有数据,等等。这些都是您需要重写和实现的方法。

但现在让我们更谨慎一点。从开始到结束,实现内容提供者有许多部分和片段,因此,首先,让我们从布局这一部分开始,看看所有这些片段:

  • 定义数据模型(通常是一个 SQLite 数据库,然后扩展 ContentProvider类)
  • 定义其统一资源标识符(URI)
  • 在清单文件中声明内容提供程序
  • 实现抽象方法(query(), insert(), update(), delete(), getType()ContentProvideronCreate())

现在,让我们从定义数据模型开始。通常情况下,数据模型类似于 SQLite 数据库的模型(尽管不一定是这样),然后它只是扩展了 ContentProvider类。对于我的例子,我选择实现一个非常简单的数据库模式,它只包含一个表——公民表,意在复制一个标准数据库,该数据库跟踪一个所有人的列表,这些人都有一个唯一的 ID(想想社保 ID)、一个姓名、一个注册的州,在我的例子中还有一个报告的收入。让我们首先定义这个 CitizensTable类及其模式:

public class CitizenTable {
public static final String TABLE_NAME = "citizen_table";
/**
* DEFINE THE TABLE
*/
// ID COLUMN MUST LOOK LIKE THIS
public static final String ID = "_id";
public static final String NAME = "name";
public static final String STATE = "state";
public static final String INCOME = "income";
/**
* DEFINE THE CONTENT TYPE AND URI
*/
// TO BE DISCUSSED LATER. . .
}

很简单。现在让我们创建一个扩展 SQLiteOpenHelper类的类(就像我们在前一章中所做的那样),但是这次我们将把它声明为一个内部类,其中外部类扩展了 ContentProvider类:

public class CitizenContentProvider extends ContentProvider {
private static final String DATABASE_NAME = "citizens.db";
private static final int DATABASE_VERSION = 1;
public static final String AUTHORITY =
"jwei.apps.dataforandroid.ch4.CitizenContentProvider";
// OVERRIDE AND IMPLEMENT OUR DATABASE SCHEMA
private static class DatabaseHelper extends SQLiteOpenHelper{
DatabaseHelper(Context context) {
super(context,DATABASE_NAME,null,DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// CREATE INCOME TABLE
db.execSQL("CREATE TABLE " + CitizenTable.TABLE_NAME +
" (" + CitizenTable.ID + " INTEGER PRIMARY KEY
AUTOINCREMENT," + CitizenTable.NAME + " TEXT," +
CitizenTable.STATE + " TEXT," + CitizenTable.INCOME +
" INTEGER);");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion,
int newVersion) {
Log.w("LOG_TAG", "Upgrading database from version " +
oldVersion + " to " + newVersion +
", which will destroy all old data");
// KILL PREVIOUS TABLES IF UPGRADED
db.execSQL("DROP TABLE IF EXISTS " +
CitizenTable.TABLE_NAME);
// CREATE NEW INSTANCE OF SCHEMA
onCreate(db);
}
}
private DatabaseHelper dbHelper;
// NOTE THE DIFFERENT METHODS THAT NEED TO BE IMPLEMENTED
@Override
public boolean onCreate() {
// . . .
}
@Override
public int delete(Uri uri, String where, String[] whereArgs){
// . . .
}
@Override
public String getType(Uri uri) {
// . . .
}
@Override
public Uri insert(Uri uri, ContentValues initialValues) {
// . . .
ContentProviderContentProviderabout}
@Override
public Cursor query(Uri uri, String[] projection, String
selection, String[] selectionArgs, String sortOrder) {
// . . .
}
@Override
public int update(Uri uri, ContentValues values, String where,
String[] whereArgs) {
// . . .
}
}

您不必将您的 SQLite 数据库声明为内部类——对我来说,它只是让实现变得简单了一点,并且一切都很好地集中在一个地方。无论如何,您会注意到数据模型本身的实现与之前完全相同——覆盖 onCreate()方法并创建您的表,然后覆盖 onUpdate()方法并删除/重新创建该表。在我们刚刚看到的框架中,您还将看到扩展 ContentProvider类所需要实现的各种方法(这将在下一节中讨论)。

我们刚才看到的代码唯一不同的地方是包含了字符串:

public static final String AUTHORITY =
"jwei.apps.dataforandroid.ch4.CitizenContentProvider";

这个权限是识别提供者-不一定是路径。我的意思是,稍后我们将看到您如何定义整个路径(这被称为 URI)来将查询指向数据库模式中的正确位置。

在我们的内容供应器中,我们将允许开发人员以两种方式之一查询我们的数据库:

content://jwei.apps.dataforandroid.ch4.CitizenContentProvider/citizen

content://jwei.apps.dataforandroid.ch4.CitizenContentProvider/citizen/#

这是我们将在内容供应器中注册的两个完全指定的路径,根据开发人员请求的路径,内容供应器将知道如何查询我们的数据库。那么这些是什么意思呢——请注意,两者都以前缀 content://开头,这只是告诉对象这是指向内容供应器的 URI 的标准前缀(就像 http://告诉浏览器路径指向网页一样)。

在前缀之后,我们指定权限,以便对象知道要去哪个内容供应器,然后我们有后缀 /citizen/citizen/#。前者我们将简单地定义为基本查询——开发人员只是发布一个标准查询,并将通过 query()方法中的任何过滤器。第二种是针对开发人员已经知道公民的 ID(即社保 ID)并且只想获取表中特定行的情况。我们可以简化事情,允许开发人员以路径的形式指定 WHERE过滤器,而不是强迫开发人员通过带有 ID 的 WHERE过滤器。

现在,如果所有这些听起来仍然令人困惑,最直观的类比可能是:当您注册一个互联网域时,您必须指定一个基本网址,一旦注册,浏览器将知道如何找到其他文件相对于该基本网址的位置。同样,在我们的例子中,我们在安卓清单(我们应用的主板)中指定我们想要公开一个内容供应器,并定义了它的路径。一旦注册,任何时候开发者想要联系我们的内容供应器,他/她必须指定这个基地 URI(也就是权威),此外,他/她将需要通过完成 URI 的路径来指定他们正在进行什么样的查询。关于如何定义 ContentProvider URI 的更多信息,我邀请您查看:

http://developer . Android . com/guide/topics/providers/content-providers . html # uri sum

但是现在,让我们快速了解一下如何在安卓清单文件中声明您的提供者,然后让我们继续讨论实现的核心,即重写抽象方法:

<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="jwei.apps.dataforandroid"
android:versionCode="1"
android:versionName="1.0">
<application android:icon="@drawable/icon"
android:label="@string/app_name">
<provider
android:name=
"jwei.apps.dataforandroid.ch4.CitizenContentProvider"
android:authorities=
"jwei.apps.dataforandroid.ch4.CitizenContentProvider"/>
</application>
</manifest>

同样,非常简单。你所需要做的就是为你的内容供应器定义一个名称和权限——事实上,如果你给一个不适当的基地 URI 作为你的权限,清单文件会抱怨,所以只要它编译你知道你可以走了!现在,让我们继续讨论内容供应器更复杂的实现。

实现查询方法

现在我们已经构建了数据模型,定义了表的权限和 URI,并成功地在我们的安卓清单文件中声明了它,现在是时候编写这个类的大部分并实现它的六个抽象方法了。我们将从 onCreate()query()方法开始:

public class CitizenContentProvider extends ContentProvider {
private static final String DATABASE_NAME = "citizens.db";
private static final int DATABASE_VERSION = 1;
public static final String AUTHORITY =
"jwei.apps.dataforandroid.ch4.CitizenContentProvider";
private static final UriMatcher sUriMatcher;
private static HashMap<String, String> projectionMap;
// URI MATCH OF A GENERAL CITIZENS QUERY
private static final int CITIZENS = 1;
// URI MATCH OF A SPECIFIC CITIZEN QUERY
private static final int SSID = 2;
private static class DatabaseHelper extends SQLiteOpenHelper {
// . . .
}
private DatabaseHelper dbHelper;
@Override
public boolean onCreate() {
// HELPER DATABASE IS INITIALIZED
dbHelper = new DatabaseHelper(getContext());
return true;
}
@Override
public int delete(Uri uri, String where, String[] whereArgs){
// . . .
}
@Override
public String getType(Uri uri) {
// . . .
}
@Override
public Uri insert(Uri uri, ContentValues initialValues) {
// . . .
}
@Override
public Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(CitizenTable.TABLE_NAME);
switch (sUriMatcher.match(uri)) {
case CITIZENS:
qb.setProjectionMap(projectionMap);
break;
case SSID:
String ssid =
uri.getPathSegments(). get(CitizenTable.SSID_PATH_POSITION);
qb.setProjectionMap(projectionMap);
// FOR QUERYING BY SPECIFIC SSID
qb.appendWhere(CitizenTable.ID + "=" + ssid);
break;
default:
throw new IllegalArgumentException ("Unknown URI " + uri);
}
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor c = qb.query(db, projection, selection,
selectionArgs, null, null, sortOrder);
// REGISTERS NOTIFICATION LISTENER WITH GIVEN CURSOR
// CURSOR KNOWS WHEN UNDERLYING DATA HAS CHANGED
c.setNotificationUri(getContext().getContentResolver(),
uri);
return c;
ContentProviderContentProviderquery method, implementing}
@Override
public int update(Uri uri, ContentValues values, String where,
String[] whereArgs) {
// . . .
}
// INSTANTIATE AND SET STATIC VARIABLES
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(AUTHORITY, "citizen", CITIZENS);
sUriMatcher.addURI(AUTHORITY, "citizen/#", SSID);
// PROJECTION MAP USED FOR ROW ALIAS
projectionMap = new HashMap<String, String>();
projectionMap.put(CitizenTable.ID, CitizenTable.ID);
projectionMap.put(CitizenTable.NAME, CitizenTable.NAME);
projectionMap.put(CitizenTable.STATE, CitizenTable.STATE);
projectionMap.put(CitizenTable.INCOME,
CitizenTable.INCOME);
}
}

所以让我们先把简单的事情解决掉。首先,您会注意到,在我们定义了 SQLite 数据库之后(通过扩展 SQLiteOpenHelper类),我们声明了一个全局 DatabaseHelper变量,并在我们的 onCreate()方法中初始化了它。在某个活动发出打开我们特定内容供应器的请求后,会自动调用 onCreate()方法(通过使用 ContentResolver对象,我们稍后也会谈到这一点)。当然,任何其他初始化都应该在这里进行,但是在我们的例子中,我们所要做的就是初始化到数据库的连接。

完成后,让我们看看最后声明的静态变量。 projectionMap所做的是它允许你为你的列取别名。在大多数内容供应器中,这种映射似乎有点没有意义,因为您只是告诉内容供应器将您的表的列映射到它们自己上(正如我们在实现 onCreate()query()方法时所做的,我们刚刚看到的)。但是,在某些情况下,对于更复杂的模式(即具有联合表的模式),能够重命名和别名表的列可以使访问内容供应器的数据更加直观。

现在,请记住我们之前谈到的两条路径(即 /citizen/citizen/#)?),我们在这里所做的就是实例化一个 UriMatcher对象,它允许我们通过方法 addURI()定义这些路径。

在高层次上,这个方法所做的是定义一组映射——它告诉我们的 ContentProvider类,任何带有路径 /citizen的查询都应该映射到用 CITIZENS标志指定的任何行为。同样,任何路径为 /citizen/#的查询都应该映射到由 SSID标志指定的那些行为(这些标志都是在类的顶部定义的)。拥有这一功能对开发人员来说非常有用,因为它允许他有效地查询公民的身份是否提前知道。

这些标志通常出现在 switch语句中,所以现在我们将注意力集中在 query()方法上。它从启动一个 SqliteQueryBuilder类开始(我们在上一章花了大量的时间来研究它),并从那里使用我们的 UriMatcher对象来匹配传入的 URI。换句话说, UriMatcher正在做的是查看请求的路径,并首先弄清楚它是否是有效的路径(如果不是,我们抛出一个错误异常 unknown URI)。一旦它看到开发人员提交了一个有效的 URI,它就返回该路径的相关标志(也就是说,在我们的例子中是 CITIZENSSSID,此时我们可以使用 switch语句来导航到正确的功能。

一旦你在高层次上理解了正在发生的事情,剩下的就应该非常简单和熟悉了。如果用户刚刚提交了一个通用查询(即带有 CITIZENS标志),那么我们需要做的就是定义将要查询的投影图和表名。同样,如果用户想直接进入到我们表中的一行,那么通过在路径中指定社会保障 ID,我们可以解析出该公民的行:

String ssid =
uri.getPathSegments().get(CitizenTable.SSID_PATH_POSITION);

不要太担心 SSID_PATH_POSITION变量——我们在这里所做的就是把传入的 URI 分解成它的路径段。一旦我们有了路径段,我们将得到第一个(随后 SSID_PATH_POSITION被设置为 1,我们很快就会看到),因为在我们的例子中,我们只有一个路径段被传入。

现在,一旦我们有了被传递到查询中的期望的社会保障 ID,我们所需要做的就是将它附加到一个 WHERE过滤器,剩下的只是我们之前看到的东西——获得可读的数据库,并填写 SQLiteDatabasequery()方法。

我要提到的最后一件事是,在成功进行查询并返回指向数据的 Cursor后,由于我们将内容供应器暴露给设备上的所有外部应用,因此有可能多个应用同时访问我们的数据库,在这种情况下,我们的数据可能会发生变化。正因为如此,我们告诉返回的 Cursor监听对其底层数据所做的任何更改,这样当发生更改时, Cursor将知道更新它自己以及随后可能使用我们的 Cursor的任何用户界面组件。

执行删除和更新方法

希望此时一切都有意义,所以让我们继续讨论 delete()update()方法,它们在结构上看起来非常类似于 query()方法:

public class CitizenContentProvider extends ContentProvider {
private static final String DATABASE_NAME = "citizens.db";
private static final int DATABASE_VERSION = 1;
public static final String AUTHORITY =
"jwei.apps.dataforandroid.ch4.CitizenContentProvider";
private static final UriMatcher sUriMatcher;
private static HashMap<String, String> projectionMap;
// URI MATCH OF A GENERAL CITIZENS QUERY
private static final int CITIZENS = 1;
// URI MATCH OF A SPECIFIC CITIZEN QUERY
private static final int SSID = 2;
private static class DatabaseHelper extends SQLiteOpenHelper {
// . . .
}
private DatabaseHelper dbHelper;
@Override
public boolean onCreate() {
// HELPER DATABASE IS INITIALIZED
dbHelper = new DatabaseHelper(getContext());
return true;
}
@Override
public int delete(Uri uri, String where, String[] whereArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int count;
switch (sUriMatcher.match(uri)) {
case CITIZENS:
// PERFORM REGULAR DELETE
count = db.delete(CitizenTable.TABLE_NAME, where,
whereArgs);
break;
case SSID:
// FROM INCOMING URI GET SSID
String ssid =
uri.getPathSegments(). get(CitizenTable.SSID_PATH_POSITION);
// USER WANTS TO DELETE A SPECIFIC CITIZEN
String finalWhere = CitizenTable.ID+"="+ssid;
// IF USER SPECIFIES WHERE FILTER THEN APPEND
if (where != null) {
finalWhere = finalWhere + " AND " + where;
}
count = db.delete(CitizenTable.TABLE_NAME,
finalWhere, whereArgs);
break;
default:
throw new IllegalArgumentException ("Unknown URI " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
ContentProviderContentProviderupdate() methods, implementing@Override
public String getType(Uri uri) {
// . . .
}
@Override
public Uri insert(Uri uri, ContentValues initialValues) {
// . . .
}
@Override
public Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder) {
// . . .
}
@Override
public int update(Uri uri, ContentValues values, String where,
String[] whereArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int count;
switch (sUriMatcher.match(uri)) {
case CITIZENS:
// GENERAL UPDATE ON ALL CITIZENS
count = db.update(CitizenTable.TABLE_NAME, values,
where, whereArgs);
break;
case SSID:
// FROM INCOMING URI GET SSID
String ssid =
uri.getPathSegments(). get(CitizenTable.SSID_PATH_POSITION);
// THE USER WANTS TO UPDATE A SPECIFIC CITIZEN
String finalWhere = CitizenTable.ID+"="+ssid;
if (where != null) {
finalWhere = finalWhere + " AND " + where;
}
// PERFORM THE UPDATE ON THE SPECIFIC CITIZEN
count = db.update(CitizenTable.TABLE_NAME, values,
finalWhere, whereArgs);
break;
default:
throw new IllegalArgumentException ("Unknown URI " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
// INSTANTIATE AND SET STATIC VARIABLES
static {
// . . .
}
}

所以我们看到这两种说法背后的逻辑非常符合 query()方法。我们看到在 delete()方法中,我们首先获得我们的可写数据库(注意,在这种情况下,我们不需要 SQLiteQueryBuilder的帮助,因为我们正在删除一些东西并且没有查询任何东西),然后我们将传入的 URI 指向我们的 UriMatcher。一旦 UriMatcher验证了路径,它就将路径指向适当的标志,在这一点上,我们可以相应地改变功能。

在我们的例子中,任何带有 CITIZEN路径规范的查询都变成了一个标准的 delete()语句,而那些带有 SSID路径规范的查询变成了一个 delete()语句,在表的 ID 列上有一个附加的 WHERE过滤器。同样,这里的直觉是我们正在从我们的数据库中删除一个特定的公民。请看下面的代码片段:

String finalWhere = CitizenTable.ID+"="+ssid;
// IF USER SPECIFIES WHERE FILTER THEN APPEND
if (where != null) {
finalWhere = finalWhere + " AND " + where;
}

注意我们是如何将身份过滤器附加到用户可能指定的任何原始 WHERE过滤器上的。在您的实现中记住像这样的细节是很重要的——也就是说,开发人员可能已经在路径规范中传递了额外的参数和标识,所以您的最终 WHERE过滤器应该考虑所有这些。剩下的唯一细节在这行:

getContext().getContentResolver().notifyChange(uri, null);

这里我们所做的是请求进行这个调用的 ContextContentResolver,并通知它对其底层数据的更改已经成功。当我们讨论如何将 Cursors绑定到用户界面时,为什么这很重要将变得更加清楚,但是现在考虑一种情况,在您的活动中,您将数据的行显示为列表。很自然,每当有东西改变了底层数据库中的一行数据时,您会希望您的列表反映这些变化,所以这就是为什么我们需要在方法结束时通知这些变化。

现在,我不会对 update()方法说太多,因为其逻辑与 delete()方法相同——唯一的区别在于您得到的可写 SQLite 数据库所做的调用。所以,让我们继续前进,用 getType()insert()方法完成我们的实现!

实现插入和获取类型方法

是时候实现我们最后的两个方法,完成我们的 ContentProvider实现了。我们来看看:

public class CitizenContentProvider extends ContentProvider {
private static final String DATABASE_NAME = "citizens.db";
private static final int DATABASE_VERSION = 1;
public static final String AUTHORITY =
"jwei.apps.dataforandroid.ch4.CitizenContentProvider";
private static final UriMatcher sUriMatcher;
private static HashMap<String, String> projectionMap;
// URI MATCH OF A GENERAL CITIZENS QUERY
private static final int CITIZENS = 1;
// URI MATCH OF A SPECIFIC CITIZEN QUERY
private static final int SSID = 2;
private static class DatabaseHelper extends SQLiteOpenHelper {
// . . .
}
private DatabaseHelper dbHelper;
@Override
public boolean onCreate() {
// . . .
}
@Override
public int delete(Uri uri, String where, String[] whereArgs) {
// . . .
}
@Override
public String getType(Uri uri) {
switch (sUriMatcher.match(uri)) {
case CITIZENS:
return CitizenTable.CONTENT_TYPE;
case SSID:
return CitizenTable.CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
@Override
public Uri insert(Uri uri, ContentValues initialValues) {
// ONLY GENERAL CITIZENS URI IS ALLOWED FOR INSERTS
// DOESN'T MAKE SENSE TO SPECIFY A SINGLE CITIZEN
if (sUriMatcher.match(uri) != CITIZENS) { throw new IllegalArgumentException("Unknown URI " + uri); }
// PACKAGE DESIRED VALUES AS A CONTENTVALUE OBJECT
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
SQLiteDatabase db = dbHelper.getWritableDatabase();
long rowId = db.insert(CitizenTable.TABLE_NAME,
CitizenTable.NAME, values);
if (rowId > 0) {
Uri citizenUri = ContentUris.withAppendedId(CitizenTable.CONTENT_URI, rowId);
// NOTIFY CONTEXT OF THE CHANGE
getContext().getContentResolver().notifyChange(citizenUri,
null);
ContentProviderContentProvidergetType() method, implementingreturn citizenUri;
}
throw new SQLException("Failed to insert row into " + uri);
}
@Override
public Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder) {
// . . .
}
@Override
public int update(Uri uri, ContentValues values, String where,
String[] whereArgs) {
// . . .
}
// INSTANTIATE AND SET STATIC VARIABLES
static {
// . . .
}
}

首先,我们来解决 getType()法。这个方法只是返回给定 URI 请求的数据对象的多用途互联网邮件扩展(MIME) 类型,这实际上只是意味着您给数据的每一行(或多行)一个可区分的数据类型。这样,如果需要,开发人员就能够识别指向您的表的 Cursor是否确实在检索有效的公民对象。为数据指定 MIME 类型的规则是:

  • vnd.android.cursor.item/为单个记录
  • vnd.android.cursor.dir/为多个记录

随后,我们将在 CitizenTable类中定义我们的 MIME 类型(这也是我们定义列和模式的地方):

public class CitizenTable {
public static final String TABLE_NAME = "citizen_table";
/**
* DEFINE THE TABLE
*/
// . . .
/**
* DEFINE THE CONTENT TYPE AND URI
*/
// THE CONTENT URI TO OUR PROVIDER
public static final Uri CONTENT_URI = Uri.parse("content://" +
CitizenContentProvider.AUTHORITY + "/citizen");
// MIME TYPE FOR GROUP OF CITIZENS
public static final String CONTENT_TYPE =
"vnd.android.cursor.dir/vnd.jwei512.citizen";
// MIME TYPE FOR SINGLE CITIZEN
public static final String CONTENT_ITEM_TYPE =
"vnd.android.cursor.item/vnd.jwei512.citizen";
// RELATIVE POSITION OF CITIZEN SSID IN URI
public static final int SSID_PATH_POSITION = 1;
}

所以现在我们已经定义了我们的 MIME 类型,剩下的就是简单地传递 UriMatcher中的 URI(再次)并返回相应的 MIME 类型。

最后但同样重要的是,我们有我们的 insert()方法。这种方法略有不同,但并不明显。唯一不同的是,当插入一些东西时,通过一个 SSID URI 路径是没有意义的(想想看——如果你正在插入一个新的公民,你怎么可能已经有了一个想要进入 URI 的社会保障身份证)。所以在这种情况下,如果一个 URI 认为没有CITIZEN路径规范传入,我们抛出一个错误。否则,我们继续并简单地检索我们的可写数据库,并将这些值插入到我们的内容提供者中(这也是我们之前看到的)。

就这样!目标是在看到完整的实现后,所有的部分联系在一起,你开始理解,至少是直观地理解,我们的 ContentProvider类正在发生什么。只要这在直觉上有意义,当你自己实际编程和实现内容提供者时,剩下的就会随之而来!

现在,在继续讨论通过内容供应器公开您的数据的实际原因之前,让我们快速了解一下您将如何与内容供应器进行交互(让我们暂时使用我们的),并随后介绍 ContentResolver类,到目前为止,我们已经看到它出现了几次。这看起来很快,但不用担心——很快我们将花一整章的时间来查询最常用的内容供应器: Contacts内容供应器。

与内容供应器互动

至此,我们已经成功实现了自己的内容提供者,现在可以由外部应用读取、查询和更新(假设授予了适当的权限)!要与内容供应器互动,第一步是从您的 Context获取相关的 ContentResolver。这个类的行为非常像一个 SQLiteDatabase类,因为它有你的标准 insert(), query(), update()delete()方法(事实上,这两个类的语法和参数也非常相似),但是它是专门为通过 URIs 与内容供应器交互而设计的,这些内容是由开发人员传入的。

让我们看看如何在 Activity类中实例化 ContentResolver,然后使用两种路径规范插入和查询数据:

public class ContentProviderActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
ContentResolver cr = getContentResolver();
ContentValues contentValue = new ContentValues();
contentValue.put(CitizenTable.NAME, "Jason Wei");
contentValue.put(CitizenTable.STATE, "CA");
contentValue.put(CitizenTable.INCOME, 100000);
cr.insert(CitizenTable.CONTENT_URI, contentValue);
contentValue = new ContentValues();
contentValue.put(CitizenTable.NAME, "James Lee");
contentValue.put(CitizenTable.STATE, "NY");
contentValue.put(CitizenTable.INCOME, 120000);
cr.insert(CitizenTable.CONTENT_URI, contentValue);
contentValue = new ContentValues();
contentValue.put(CitizenTable.NAME, "Daniel Lee");
contentValue.put(CitizenTable.STATE, "NY");
contentValue.put(CitizenTable.INCOME, 80000);
cr.insert(CitizenTable.CONTENT_URI, contentValue);
// QUERY TABLE FOR ALL COLUMNS AND ROWS
Cursor c = cr.query(CitizenTable.CONTENT_URI, null, null,
null, CitizenTable.INCOME + " ASC");
// LET THE ACTIVITY MANAGE THE CURSOR
startManagingCursor(c);
int idCol = c.getColumnIndex(CitizenTable.ID);
int nameCol = c.getColumnIndex(CitizenTable.NAME);
int stateCol = c.getColumnIndex(CitizenTable.STATE);
int incomeCol = c.getColumnIndex(CitizenTable.INCOME);
while (c.moveToNext()) {
int id = c.getInt(idCol);
String name = c.getString(nameCol);
String state = c.getString(stateCol);
int income = c.getInt(incomeCol);
System.out.println("RETRIEVED ||" + id + "||" + name +
"||" + state + "||" + income);
}
System.out.println("-------------------------------");
// QUERY BY A SPECIFIC ID
Uri myC = Uri.withAppendedPath(CitizenTable.CONTENT_URI,
"2");
Cursor c1 = cr.query(myC, null, null, null, null);
// LET THE ACTIVITY MANAGE THE CURSOR
startManagingCursor(c1);
while (c1.moveToNext()) {
int id = c1.getInt(idCol);
String name = c1.getString(nameCol);
String state = c1.getString(stateCol);
int income = c1.getInt(incomeCol);
System.out.println("RETRIEVED ||" + id + "||" + name +
"||" + state + "||" + income);
}
}
}

这里的情况是,我们首先在数据库中插入三行,这样公民表现在看起来就像:

|

身份证明

|

名字

|

状态

|

收入

| | --- | --- | --- | --- | | one | 贾森·魏 | 加拿大 | One hundred thousand | | Two | 李中清 | 纽约州 | One hundred and twenty thousand | | three | 李仁港 | 纽约州 | Eighty thousand |

从这里,我们使用内容解析器对表进行一般查询(也就是说,只是传入基本的 URI 路径规范),顺序是增加收入。然后,我们使用内容解析器使用 SSID路径规范进行特定的查询。为此,我们使用静态方法:

Uri myC = Uri.withAppendedPath(CitizenTable.CONTENT_URI, "2");

这将 URI 的基本内容从:

content://jwei.apps.dataforandroid.ch4.CitizenContentProvider/citizen

至以下:

content://jwei.apps.dataforandroid.ch4.CitizenContentProvider/citizen/2

为了验证我们的结果,让我们看看输出了什么:

Interacting with a ContentProvider

从前面的截图中,我们可以看到这两个查询确实输出了正确的数据行!

现在,关于前面的例子,我要说的唯一剩下的事情(因为大多数语法和 Cursor处理与前面章节的例子相同)是关于方法 startManagingCursor()。在前面的章节中,你会注意到每次我通过一个 query()打开一个 Cursor,我必须确保在 Activity的末尾关闭它;否则,OS 会抛出各种挂机 Cursor警告。不过有了 startManagingCursor()方便法, Activity会为你管理 Cursor的生命周期,确保在 Activity自毁之前关闭,等等。总的来说,让 Activity替你管理你的 Cursors是个好主意。

实际使用案例

所以,既然你知道如何实现和访问内容供应器,你可能会挠头,心想:我为什么要这么做?

对于内容供应器来说,有哪些实际的用例可以激励你去经历构建内容供应器的额外麻烦,而不仅仅是扩展一个 SQLiteOpenHelper和编写一些方便的方法?

嗯, ContentProvider的一个独特之处在于,它允许您将数据公开给所有外部应用,因此我们可以从那里开始头脑风暴。假设您正在运行一个小型(或大型)初创公司,并且您已经开发了一个允许用户查找餐馆和预订的应用。

现在,明智的做法是,您的应用很可能将这些预订的预订存储在某种类型的数据库中,以便用户每次打开应用时都能看到他们以前做过什么预订。但是,假设您公开了您的内容供应器,并将其转换为本地的API(对于某些人来说,最简单的方法就是将内容供应器想象成这样)——在这种情况下,其他应用,可能是日历应用或任务列表应用,可以开发一些特殊的功能,允许他们将他们的日历和/或任务与该用户的餐厅预订同步!*

*在这个例子中,您有两个应用,它们都有自己特定的功能,利用内容供应器的力量为用户提供出色的体验(快乐的用户意味着对您的应用的快乐评论)!

在我们结束这一章并进入下一章之前,让我们集思广益再举一个例子。安卓操作系统(以及整个谷歌)的一大优点是搜索功能!因此,在安卓操作系统中,有一个本地的快速搜索应用,它通常作为一个小部件出现在设备的主屏幕上(更多信息请参见http://developer.android.com/resources/articles/qsb.html)。

这个快速搜索小部件特别酷,因为它允许你搜索所有宣布自己可搜索的数据库。让数据库变得可搜索有哪些先决条件?你猜对了——必须通过内容供应器。同样,只有通过向内容供应器公开您的数据,任何应用(无论是本地的还是第三方的)才能读取和访问您的数据库。

因此,假设您正在编写一个短信应用,结果您维护了一个内容供应器,该供应器存储了您与朋友的所有最新短信。您可以添加的一个整洁的特性是将您的内容供应器声明为可搜索的,然后在您的内容供应器中指定要搜索的字段(在这种情况下,可能是包含正文的字段)。一旦你做到了这一点,用户可以快速使用主屏幕的搜索小部件,并与他们的朋友无缝切换他们的文本!

说到底,内容供应器背后的原则和概念很简单,实施只是工作的一半——另一半是发挥创造力,为您的内容供应器考虑创新和有用的应用。

总结

在这一章中,我们详细讨论了什么是 ContentProvider以及如何实现它,结果我们看到了很多代码。然而,从概念上来说, ContentProvider是相当简单的,首先定义一个扩展了 SQLiteOpenHelper的内部类,然后根据传递到每个方法中的指令指定如何查询和/或修改该 SQLite 数据库。这些指令以 URIs 的形式出现,因此在每种方法中,您将解析 URI 的不同路径并执行适当的功能。

*然后,我们很快看到您如何通过使用从 Context获得的 ContentResolver与您的新内容供应器(或者实际上是任何内容供应器)进行交互,然后将其用于 query(), insert(), delete()update()相应的内容供应器。

最后,我们花了一些时间远离代码,考虑我们可以使用内容供应器的实际方法。这始终是开发应用时要做的一项重要工作,也是我写这本书的目标之一——让您掌握这些技术的低级实现细节,以及它们的高级动机和用例。

现在,我前面提到安卓操作系统充满了预先存在的内容供应器,任何开发人员都可以自由查询和更新。事实上,这是真的,系统中内置的一些更常见的内容供应器是媒体和日历内容供应器。然而,到目前为止最重要和最常用的 ContentProviderContacts内容提供者——内置于操作系统中的数据库模式,其中包含用户的联系人列表。

在下一章中,我们将把全部注意力投入到学习和理解这个 Contacts内容提供者、它的模式,以及如何与它交互来完成标准的查询和更新。****