Contacts Provider Access
这一章节讲述了如何从Contacts Provider访问数据。主要关注以下几点:
- Entity queries.实体查询
- Batch modification.批量修改
- Retrieval and modification with intents.使用intents检索和修改
- Data integrity.数据完整性
Querying entities
ContactsContract.RawContacts关联到ContactsContract.Contacts的数据,或者所有管理到 ContactsContract.RawContacts的
ContactsContract.CommonDataKinds.Email数据。为了便于实现这个功能,Contacts Provider 提供了实体(entity)的数据结构,它像一个连接各个数据表的数据结构。
注意:一个实体通常没有包含父表和子表所有的列。如果访问到实体没有的列,就会得到一个异常。
下面的代码片段展示了如何检索一个contact的所有raw contact。这个代码片段是一个拥有两个activity的大程序的一部分,这两个activity是“主要”和“详细信息”的activity。“主要”的activity展示一列联系人的数据,当用户选择一个联系人时,把它的ID传给“详细信息”的activity。详细信息的activity使用ContactsContract.Contacts.Entity
展示了改contact的所有Data数据。
下面的代码是“详细信息”activity的代码片段:
...
/*
* Appends the entity path to the URI. In the case of the Contacts Provider, the
* expected URI is content://com.google.contacts/#/entity (# is the ID value).
*/
mContactUri = Uri.withAppendedPath(
mContactUri,
ContactsContract.Contacts.Entity.CONTENT_DIRECTORY);
// Initializes the loader identified by LOADER_ID.
getLoaderManager().initLoader(
LOADER_ID, // The identifier of the loader to initialize
null, // Arguments for the loader (in this case, none)
this); // The context of the activity
// Creates a new cursor adapter to attach to the list view
mCursorAdapter = new SimpleCursorAdapter(
this, // the context of the activity
R.layout.detail_list_item, // the view item containing the detail widgets
mCursor, // the backing cursor
mFromColumns, // the columns in the cursor that provide the data
mToViews, // the views in the view item that display the data
0); // flags
// Sets the ListView's backing adapter.
mRawContactList.setAdapter(mCursorAdapter);
...
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
/*
* Sets the columns to retrieve.
* RAW_CONTACT_ID is included to identify the raw contact associated with the data row.
* DATA1 contains the first column in the data row (usually the most important one).
* MIMETYPE indicates the type of data in the data row.
*/
String[] projection =
{
ContactsContract.Contacts.Entity.RAW_CONTACT_ID,
ContactsContract.Contacts.Entity.DATA1,
ContactsContract.Contacts.Entity.MIMETYPE
};
/*
* Sorts the retrieved cursor by raw contact id, to keep all data rows for a single raw
* contact collated together.
*/
String sortOrder =
ContactsContract.Contacts.Entity.RAW_CONTACT_ID +
" ASC";
/*
* Returns a new CursorLoader. The arguments are similar to
* ContentResolver.query(), except for the Context argument, which supplies the location of
* the ContentResolver to use.
*/
return new CursorLoader(
getApplicationContext(), // The activity's context
mContactUri, // The entity content URI for a single contact
projection, // The columns to retrieve
null, // Retrieve all the raw contacts and their data rows.
null, //
sortOrder); // Sort by the raw contact ID.
}
数据查询完成后,
LoaderManager
回调
onLoadFinished()。查询的结果会由一个Cursor参数传递。你可以从这个Cursor获取数据用来显示或其他操作。
Batch modification
尽可能使用批量修改来插入、更新和删除Contacts Provider中的数据。通过创建存储ContentProviderOperation的 ArrayList,再调用applyBatch()的方法。因为Contacts Provider在一个transaction中执行applyBatch()的所有操作,所以能够保持数据的一致性。批量操作也可以方便插入一个联系人数据及其详细数据。
注意:如果只是要改一行raw contact的数据,首先考虑用发intent给联系人应用的方式,而不是自己去操作数据。使用intent的方式在Retrieval and modification with intents有更详细的说明。
Yield points
批量修改包含大量的操作可能会阻塞进程,导致用户体验不好。为了把修改操作放在尽可能少的操作列表里面,并且要防止阻塞进程,可以为一个或多个操作设置yield points。 yield point 就是
isYieldAllowed()为true的
ContentProviderOperation对象。当Contacts Provider执行到yield point的时候,它暂停操作,让其他进程先执行,并关闭当前的操作。当provider重新开始工作后,它继续之前的工作,从操作列表中取出下一个操作,开启一个新的transaction。
Yield point导致调用applyBatch()的时候,会有多个transaction。yield point要设置在一系列相关行操作的最后一个操作。例如,在添加联系人的时候,yield point要设置在添加raw contact和它的data 的最后一个操作,或者添加在一系列跟同一个联系人相关的操作中的最后一个操作。
yield point也是一个原子操作。所有在两个yield point直接操作,在一次操作中要么全部成功,要不全部无效。如果没有设置yield point,整个批量处理就是一个最小的原子操作。如果设置了原子操作,你避免降低系统性能作,而在同一时间,确保操作的子集是原子。
Modification back references
添加一个新的raw contact和它的data数据,使用一系列的ContentProviderOperation对象,你必须把data数据的RAW_CONTACT_ID设置成新添加的raw contact的ID。然而,当创建data数据行的时候,raw contact的ID还没生成。因为这一系列的
ContentProviderOperation对象是一个原子操作,还不能得到创建raw contact的结果。为了解决这个问题,ContentProviderOperation.Builder有一个
withValueBackReference()的方法。这个方法可以让你使用前面操作的结果来插入数据。
withValueBackReference()有两个参数:
key
key-value键值对的键。值应该是所修改的表的列名。
previousResult
(简单来说,就是操作的索引值)
applyBatch()返回值ContentProviderResult数组的索引。开始批量操作后,每个操作的结果都存在一个数组里。previousResult操作结果数组的一个索引,通过key值检索和存储。这样子就可以在插入一个新的raw contact联系人后,得到它的_ID,然后再你插入Data数据的时候,就可以通过“后引用”去使用这个ID。
在调用applyBatch()的时候,整个结果数组就创建好了,结果数组的大小和ContentProviderOperation的数组大小一样。但是,结果数组的初始值都是null,如果在操作结束前,通过“后引用”来使用操作的结果,就会抛出异常。
下面的代码片段显示如何使用batch插入一个raw contact和数据。这些代码包括使用yield points和“后引用”。这个代码片段是createContacEntry()方法的扩展,是Contact Manager 例子源码中的ContactAdder的一部分。
第一个源码片在UI中获取联系人的data数据。用户已经选择了用来添加联系人的账户:
// Creates a contact entry from the current UI values, using the currently-selected account.
protected void createContactEntry() {
/*
* Gets values from the UI
*/
String name = mContactNameEditText.getText().toString();
String phone = mContactPhoneEditText.getText().toString();
String email = mContactEmailEditText.getText().toString();
int phoneType = mContactPhoneTypes.get(
mContactPhoneTypeSpinner.getSelectedItemPosition());
int emailType = mContactEmailTypes.get(
mContactEmailTypeSpinner.getSelectedItemPosition());
下一个源码片段创建一个operation去插入一个raw contact数据到 ContactsContract.RawContacts:
/* * Prepares the batch operation for inserting a new raw contact and its data. Even if * the Contacts Provider does not have any data for this person, you can't add a Contact, * only a raw contact. The Contacts Provider will then add a Contact automatically. */ // Creates a new array of ContentProviderOperation objects. ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); /* * Creates a new raw contact with its account type (server type) and account name * (user's account). Remember that the display name is not stored in this row, but in a * StructuredName data row. No other data is required. */ ContentProviderOperation.Builder op = ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, mSelectedAccount.getType()) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, mSelectedAccount.getName()); // Builds the operation and adds it to the array of operations ops.add(op.build());接下来的代码,创建了显示名字,电话号码和邮箱的data数据。 每一个操作使用
withValueBackReference()
去获取
RAW_CONTACT_ID。这个引用指向 第一个操作的
ContentProviderResult,第一个操作是创建raw contact并返回它的_ID。这样,每个新建的data数据,就自动连接它的
RAW_CONTACT_ID到新插入的
ContactsContract.RawContacts
。
ContentProviderOperation.Builder在添加email的时候,使用
withYieldAllowed()添加了一个yield point:
// Creates the display name for the new raw contact, as a StructuredName data row.
op =
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
/*
* withValueBackReference sets the value of the first argument to the value of
* the ContentProviderResult indexed by the second argument. In this particular
* call, the raw contact ID column of the StructuredName data row is set to the
* value of the result returned by the first operation, which is the one that
* actually adds the raw contact row.
*/
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
// Sets the data row's MIME type to StructuredName
.withValue(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
// Sets the data row's display name to the name in the UI.
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name);
// Builds the operation and adds it to the array of operations
ops.add(op.build());
// Inserts the specified phone number and type as a Phone data row
op =
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
/*
* Sets the value of the raw contact id column to the new raw contact ID returned
* by the first operation in the batch.
*/
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
// Sets the data row's MIME type to Phone
.withValue(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
// Sets the phone number and type
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
.withValue(ContactsContract.CommonDataKinds.Phone.TYPE, phoneType);
// Builds the operation and adds it to the array of operations
ops.add(op.build());
// Inserts the specified email and type as a Phone data row
op =
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
/*
* Sets the value of the raw contact id column to the new raw contact ID returned
* by the first operation in the batch.
*/
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
// Sets the data row's MIME type to Email
.withValue(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
// Sets the email address and type
.withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email)
.withValue(ContactsContract.CommonDataKinds.Email.TYPE, emailType);
/*
* Demonstrates a yield point. At the end of this insert, the batch operation's thread
* will yield priority to other threads. Use after every set of operations that affect a
* single contact, to avoid degrading performance.
*/
op.withYieldAllowed(true);
// Builds the operation and adds it to the array of operations
ops.add(op.build());
最后一个代码片段调用
applyBatch()去插入一个新的raw contact 和它的data数据行。
// Ask the Contacts Provider to create a new contact
Log.d(TAG,"Selected account: " + mSelectedAccount.getName() + " (" +
mSelectedAccount.getType() + ")");
Log.d(TAG,"Creating contact: " + name);
/*
* Applies the array of ContentProviderOperation objects in batch. The results are
* discarded.
*/
try {
getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
} catch (Exception e) {
// Display a warning
Context ctx = getApplicationContext();
CharSequence txt = getString(R.string.contactCreationFailure);
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(ctx, txt, duration);
toast.show();
// Log exception
Log.e(TAG, "Exception encountered while inserting contact: " + e);
}
}
批量操作允许实现一个优化多线程控制( optimistic concurrency control),一个无需锁定底层数据库的方法。使用这个方法,在调用transaction 的时候,检查一下是否有其他的修改会同时发生。如果发现同时修改,就取消这次修改。
Optimistic concurrency control 对手机设备很有用,手机设备只有一个用户的,并且很少同时访问数据库。因为数据库没有加锁,就不用花费时间去加锁和解锁。
以下是使用ContactsContract.RawContacts
的步骤:
- 在检索数据的时候,同时检索raw contact的VERSION列。
- 创建一个ContentProviderOperation.Builder,调用newAssertQuery(Uri)方法,content URI是
RawContacts.CONTENT_URI
和raw contact的_ID. - ContentProviderOperation.Builder调用
withValue()的方法去对比刚刚检索到的
VERSION值
- 在同一个
ContentProviderOperation.Builder中调用
withExpectedCount()
去确保在这次断言中只有一个修改。 - 调用Call
build()
创建ContentProviderOperation
,然后把这个ContentProviderOperation加入到调用applyBatch()操作的第一个操作。
- 调用批量操作。
如果raw contact在你读取数据,然后去修改它的期间被更新, ContentProviderOperation
将会失败,然后整个力量操作都会失效。这是需要重新尝试这次操作。
下面的代码片段展示如何在使用CursorLoader查询一个raw contact后,创建一个断言ContentProviderOperation
/*
* The application uses CursorLoader to query the raw contacts table. The system calls this method
* when the load is finished.
*/
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
// Gets the raw contact's _ID and VERSION values
mRawContactID = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID));
mVersion = cursor.getInt(cursor.getColumnIndex(SyncColumns.VERSION));
}
...
// Sets up a Uri for the assert operation
Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, mRawContactID);
// Creates a builder for the assert operation
ContentProviderOperation.Builder assertOp = ContentProviderOperation.netAssertQuery(rawContactUri);
// Adds the assertions to the assert operation: checks the version and count of rows tested
assertOp.withValue(SyncColumns.VERSION, mVersion);
assertOp.withExpectedCount(1);
// Creates an ArrayList to hold the ContentProviderOperation objects
ArrayList ops = new ArrayList<ContentProviderOperationg>;
ops.add(assertOp.build());
// You would add the rest of your batch operations to "ops" here
...
// Applies the batch. If the assert fails, an Exception is thrown
try
{
ContentProviderResult[] results =
getContentResolver().applyBatch(AUTHORITY, ops);
} catch (OperationApplicationException e) {
// Actions you want to take if the assert operation fails go here
}