1. 程式人生 > >基礎總結篇之六:ContentProvider之讀寫聯絡人

基礎總結篇之六:ContentProvider之讀寫聯絡人

靡不有初,鮮克有終。《詩經》

很多事情,絕大多數人都會在開始的時候滿懷熱情,而能堅持到底的卻是寥寥無幾。對待自己的目標,虎頭蛇尾絕不可取,半途而廢只會一無所成,我們必須持之以恆的做下去,堅持到底才能摘取勝利的果實。最近也忙了起來,忙著給自己充電,深知這項任務的艱鉅,不是一天兩天的事,所以也借用這句警言來告誡自己,堅持不懈的走下去。

今天我們來講解一下如何利用ContentProvider機制讀寫聯絡人資訊。

在Android中,ContentProvider是一種資料包裝器,適合在不同程序間實現資訊的共享。例如,在Android中SQLite資料庫是一個典型的資料來源,我們可以把它封裝到ContentProvider中,這樣就可以很好的為其他應用提供資訊共享服務。其他應用在訪問ContentProvider時,可以使用一組類似REST的URI的方式進行資料操作,大大簡化了讀寫資訊的複雜度。例如,如果要從封裝圖書資料庫的ContentProvider獲取一組圖書,需要使用類似以下形式的URI:

content://com.scott.book.BookProvider/books

而要從圖書資料庫中獲取指定圖書(比如23號圖書),需要使用類似以下形式的URI:

content://com.scott.book.BookProvider/books/23

注:ContentProvider是一個抽象類,定義了一系列操作資料的方法模板,BookProvider需要實現這些方法,實現圖書資訊的各種操作。

那麼,現在知道了具體的URI之後,我們又如何操作進而取得資料呢?

此時,我們就要了解ContentResolver這個類,它跟ContentProvider是對應的關係,我們正是通過它來與ContentProvider進行資料交換的。android.content.Context類為我們定義了getContentResolver()方法,用於獲取一個ContentResolver物件,如果我們在執行期可以通過getContext()獲取當前Context例項物件,就可以通過這個例項物件所提供的getContentResolver()方法獲取到ContentResolver型別的例項物件,進而可以操作對應的資料。

下面我們就通過聯絡人例項對這種機制進行演示。

在Android中,聯絡人的操作都是通過一個統一的途徑來讀寫資料的,我們開啟/data/data/com.android.providers.contacts可以看到聯絡人的資料來源:


有興趣的朋友可以匯出這個檔案,用專業的工具軟體開啟看一下表結構。

對這個SQLite型別的資料來源的封裝後,聯絡人就以ContentProvider的形式為其他應用程序提供聯絡人的讀寫服務,我們就可以順利成章的操作自己的聯絡人資訊了。

為了方便測試,我們先新增兩個聯絡人到資料來源中,如圖所示:



我們看到,每個聯絡人都有兩個電話號碼和兩個郵箱賬號,分別為家庭座機號碼、移動手機號碼、家庭郵箱賬號和工作郵箱賬號。當然在新增聯絡人時有很多其他資訊,我們這裡都沒有填寫,只選擇了最常用的電話和郵箱,主要是方便演示這個過程。

在演示程式碼之前,我們需要了解一下android.provider.ContactsContract這個類(注:在較早的版本中是android.provider.Contacts這個類,不過現在已被廢棄,不建議使用),它定義了各種聯絡人相關的URI和每一種型別資訊的屬性資訊:


有興趣的朋友還可以讀一下原始碼,不過比較多,而且內部類使用的特別多,讀起來有一定的困難,還是要做好心理準備。

下面我們通過一個專案,來演示一下聯絡人操作的具體過程。新建一個名為provider的專案,建立一個名為ContactsReadTest的測試用例,如下:

package com.scott.provider;

import java.util.ArrayList;

import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.test.AndroidTestCase;
import android.util.Log;

public class ContactsReadTest extends AndroidTestCase {
	
	private static final String TAG = "ContactsReadTest";
	
	//[content://com.android.contacts/contacts]
	private static final Uri CONTACTS_URI = ContactsContract.Contacts.CONTENT_URI;
	//[content://com.android.contacts/data/phones]
	private static final Uri PHONES_URI = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
	//[content://com.android.contacts/data/emails]
	private static final Uri EMAIL_URI = ContactsContract.CommonDataKinds.Email.CONTENT_URI;
	
	private static final String _ID = ContactsContract.Contacts._ID;
	private static final String DISPLAY_NAME = ContactsContract.Contacts.DISPLAY_NAME;
	private static final String HAS_PHONE_NUMBER = ContactsContract.Contacts.HAS_PHONE_NUMBER;
	private static final String CONTACT_ID = ContactsContract.Data.CONTACT_ID;
	
	private static final String PHONE_NUMBER = ContactsContract.CommonDataKinds.Phone.NUMBER;
	private static final String PHONE_TYPE = ContactsContract.CommonDataKinds.Phone.TYPE;
	private static final String EMAIL_DATA = ContactsContract.CommonDataKinds.Email.DATA;
	private static final String EMAIL_TYPE = ContactsContract.CommonDataKinds.Email.TYPE;
	
	public void testReadContacts() {
		ContentResolver resolver = getContext().getContentResolver();
		Cursor c = resolver.query(CONTACTS_URI, null, null, null, null);
		while (c.moveToNext()) {
			int _id = c.getInt(c.getColumnIndex(_ID));
			String displayName = c.getString(c.getColumnIndex(DISPLAY_NAME));
			
			Log.i(TAG, displayName);
			
			ArrayList<String> phones = new ArrayList<String>();
			ArrayList<String> emails = new ArrayList<String>();
			
			String selection = CONTACT_ID + "=" + _id;	//the 'where' clause
			
			//獲取手機號
			int hasPhoneNumber = c.getInt(c.getColumnIndex(HAS_PHONE_NUMBER));
			if (hasPhoneNumber > 0) {
				Cursor 	phc = resolver.query(PHONES_URI, null, selection, null, null);
				while (phc.moveToNext()) {
					String phoneNumber = phc.getString(phc.getColumnIndex(PHONE_NUMBER));
					int phoneType = phc.getInt(phc.getColumnIndex(PHONE_TYPE));
					phones.add(getPhoneTypeNameById(phoneType) + " : " + phoneNumber);
				}
				phc.close();
			}
			
			Log.i(TAG, "phones: " + phones);
			
			//獲取郵箱
			Cursor emc = resolver.query(EMAIL_URI,null, selection, null, null);
			while (emc.moveToNext()) {
				String emailData = emc.getString(emc.getColumnIndex(EMAIL_DATA));
				int emailType = emc.getInt(emc.getColumnIndex(EMAIL_TYPE));
				emails.add(getEmailTypeNameById(emailType) + " : " + emailData);
			}
			emc.close();
			
			Log.i(TAG, "emails: " + emails);
		}
		c.close();
	}
	
	private String getPhoneTypeNameById(int typeId) {
		switch (typeId) {
		case ContactsContract.CommonDataKinds.Phone.TYPE_HOME: return "home";
		case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE: return "mobile";
		case ContactsContract.CommonDataKinds.Phone.TYPE_WORK: return "work";
		default: return "none";
		}
	}
	
	private String getEmailTypeNameById(int typeId) {
		switch (typeId) {
		case ContactsContract.CommonDataKinds.Email.TYPE_HOME: return "home";
		case ContactsContract.CommonDataKinds.Email.TYPE_WORK: return "work";
		case ContactsContract.CommonDataKinds.Email.TYPE_OTHER: return "other";
		default: return "none";
		}
	}
}
為了使這個測試用例執行起來,我們需要在AndroidManifest.xml中配置一下測試裝置的宣告,它與<application>元素處於同一級別位置:
<!-- 配置測試裝置的主類和目標包 -->
    <instrumentation android:name="android.test.InstrumentationTestRunner"
					 android:targetPackage="com.scott.provider"/>
然後再配置使用測試類庫宣告,它與<activity>元素處於同一級別位置:
    	<!-- 配置測試要使用的類庫 -->
        <uses-library android:name="android.test.runner"/>
最後,還有一個重要的宣告需要配置,就是讀取聯絡人許可權,宣告如下:
    <!-- 讀取聯絡人 -->
    <uses-permission android:name="android.permission.READ_CONTACTS"/>
經過以上準備工作,這個測試用例就可以運轉起來了,我們執行一下testReadContacts()方法,列印結果如下:


看來聯絡人裡的資訊都被我們準確無誤的讀取出來了。

如果我們在一個Activity裡執行讀取聯絡人的程式碼,不僅可以使用ContentResolver直接進行讀取操作(即查詢),還可以使用Activity提供的managedQuery方法方便的實現同樣的效果,我們來看一下這個方法的具體程式碼:

    public final Cursor managedQuery(Uri uri,
                                     String[] projection,
                                     String selection,
                                     String[] selectionArgs,
                                     String sortOrder)
    {
        Cursor c = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
        if (c != null) {
            startManagingCursor(c);
        }
        return c;
    }
我們發現,其實它還是使用了ContentResolver進行查詢操作,但是多了一步startManagingCursor的操作,它會根據Activity的生命週期對Cursor物件進行管理,避免了一些因Cursor是否釋放引起的問題,所以非常方便,大大簡化了我們的工作量。

接下來我們將要嘗試將一個聯絡人資訊新增到系統聯絡人的資料來源中,實現對聯絡人的寫入操作。我們新建一個名為ContactsWriteTest的測試用例,如下:

package com.scott.provider;

import java.util.ArrayList;

import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.net.Uri;
import android.provider.ContactsContract;
import android.test.AndroidTestCase;
import android.util.Log;

public class ContactsWriteTest extends AndroidTestCase {

	private static final String TAG = "ContactsWriteTest";

	//[content://com.android.contacts/raw_contacts]
	private static final Uri RAW_CONTACTS_URI = ContactsContract.RawContacts.CONTENT_URI;
	//[content://com.android.contacts/data]
	private static final Uri DATA_URI = ContactsContract.Data.CONTENT_URI;
	
	private static final String ACCOUNT_TYPE = ContactsContract.RawContacts.ACCOUNT_TYPE;
	private static final String ACCOUNT_NAME = ContactsContract.RawContacts.ACCOUNT_NAME;
	
	private static final String RAW_CONTACT_ID = ContactsContract.Data.RAW_CONTACT_ID;
	private static final String MIMETYPE = ContactsContract.Data.MIMETYPE;
	
	private static final String NAME_ITEM_TYPE = ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE;
	private static final String DISPLAY_NAME = ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME;
	
	private static final String PHONE_ITEM_TYPE = ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE;
	private static final String PHONE_NUMBER = ContactsContract.CommonDataKinds.Phone.NUMBER;
	private static final String PHONE_TYPE = ContactsContract.CommonDataKinds.Phone.TYPE;
	private static final int PHONE_TYPE_HOME = ContactsContract.CommonDataKinds.Phone.TYPE_HOME;
	private static final int PHONE_TYPE_MOBILE = ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE;
	
	private static final String EMAIL_ITEM_TYPE = ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE;
	private static final String EMAIL_DATA = ContactsContract.CommonDataKinds.Email.DATA;
	private static final String EMAIL_TYPE = ContactsContract.CommonDataKinds.Email.TYPE;
	private static final int EMAIL_TYPE_HOME = ContactsContract.CommonDataKinds.Email.TYPE_HOME;
	private static final int EMAIL_TYPE_WORK = ContactsContract.CommonDataKinds.Email.TYPE_WORK;

	private static final String AUTHORITY = ContactsContract.AUTHORITY;
	
	public void testWriteContacts() throws Exception {
		ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();

		ContentProviderOperation operation = ContentProviderOperation.newInsert(RAW_CONTACTS_URI)
									.withValue(ACCOUNT_TYPE, null)
									.withValue(ACCOUNT_NAME, null)
									.build();
		operations.add(operation);

		//新增聯絡人名稱操作
		operation = ContentProviderOperation.newInsert(DATA_URI)
									.withValueBackReference(RAW_CONTACT_ID, 0)
									.withValue(MIMETYPE, NAME_ITEM_TYPE)
									.withValue(DISPLAY_NAME, "Scott Liu")
									.build();
		operations.add(operation);

		//新增家庭座機號碼
		operation = ContentProviderOperation.newInsert(DATA_URI)
									.withValueBackReference(RAW_CONTACT_ID, 0)
									.withValue(MIMETYPE, PHONE_ITEM_TYPE)
									.withValue(PHONE_TYPE, PHONE_TYPE_HOME)
									.withValue(PHONE_NUMBER, "01034567890")
									.build();
		operations.add(operation);
		
		//新增移動手機號碼
		operation = ContentProviderOperation.newInsert(DATA_URI)
									.withValueBackReference(RAW_CONTACT_ID, 0)
									.withValue(MIMETYPE, PHONE_ITEM_TYPE)
									.withValue(PHONE_TYPE, PHONE_TYPE_MOBILE)
									.withValue(PHONE_NUMBER, "13034567890")
									.build();
		operations.add(operation);

		//新增家庭郵箱
		operation = ContentProviderOperation.newInsert(DATA_URI)
									.withValueBackReference(RAW_CONTACT_ID, 0)
									.withValue(MIMETYPE, EMAIL_ITEM_TYPE)
									.withValue(EMAIL_TYPE, EMAIL_TYPE_HOME)
									.withValue(EMAIL_DATA, "[email protected]")
									.build();
		operations.add(operation);

		//新增工作郵箱
		operation = ContentProviderOperation.newInsert(DATA_URI)
									.withValueBackReference(RAW_CONTACT_ID, 0)
									.withValue(MIMETYPE, EMAIL_ITEM_TYPE)
									.withValue(EMAIL_TYPE, EMAIL_TYPE_WORK)
									.withValue(EMAIL_DATA, "[email protected]")
									.build();
		operations.add(operation);
		
		ContentResolver resolver = getContext().getContentResolver();
		//批量執行,返回執行結果集
		ContentProviderResult[] results = resolver.applyBatch(AUTHORITY, operations);
		for (ContentProviderResult result : results) {
			Log.i(TAG, result.uri.toString());
		}
	}
}
在上面的程式碼中,我們把整個操作分為幾個ContentProviderOperation操作,並將他們做批處理操作,我們也許注意到,從第二個操作開始,每一項都有一個withValueBackReference(RAW_CONTACT_ID, 0)步驟,它參照了第一項操作新新增的聯絡人的id,因為是批處理,我們插入資料前並不知道id的值,不過這個不用擔心,在進行批處理插入資料時,它會重新引用新的id值,不會影響最終的結果。

當然,這個也不能忘了配置寫入聯絡人的許可權宣告:

    <!-- 寫入聯絡人 -->
    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
經過以上步驟之後,我們執行一下testWriteContacts()方法,看看聯絡人是否新增進去了: