基礎總結篇之七:ContentProvider之讀寫短消息(一)

今天咱們來說一下如何利用ContentProvider讀寫短消息。java

上次咱們講了如何經過ContentProvider機制讀寫聯繫人,經過讀取聯繫人信息和添加聯繫人這兩種方式對聯繫人進行操做,相信你們對ContentProvider的基本使用方法也有所瞭解了。在Android中ContentProvider應用場合還不少,讀寫短消息就是其中一個,今天咱們就來探討一下利用ContentProvider操做短消息的問題。android

相對於聯繫人來講,短消息不是公開的,因此沒有專門的API供咱們調用,這就要求咱們根據源代碼進行分析研究,制定出必定的操做方案。app

咱們須要先找到短消息的數據源,打開/data/data/com.android.providers.telephony能夠看到:ide


其中的mmssms.db就是短消息的數據源,朋友們能夠導出一下這個文件,用專業工具軟件查看一下表結構。爲了方便你們理解,我簡單介紹一下今天涉及到的兩張表以及表中的經常使用字段:工具

如圖所示,兩張表分別是threads表和sms表,前者表明全部會話信息,每一個會話表明和一個聯繫人之間短信的羣組;後者表明短信的具體信息。在sms表中的thread_id指向了threads表中的_id,指定每條短信的會話id,以便對短信進行分組。下面介紹一下表中的每一個字段的意義:單元測試

threads表:_id字段表示該會話id;date表示該會話最後一條短信的日期,通常用來對多個會話排序顯示;message_count表示該會話所包含的短信數量;snippet表示該會話中最後一條短信的內容;read表示該會話是否已讀(0:未讀,1:已讀),通常來講該會話中有了新短信但沒查看時,該會話read變爲未讀狀態,當查看過新短信後read就變爲已讀狀態。測試

sms表:_id表示該短信的id;thread_id表示該短信所屬的會話的id;date表示該短信的日期;read表示該短信是否已讀;type表示該短信的類型,例如1表示接收類型,2表示發送類型,3表示草稿類型;body表示短信的內容。ui

下面咱們會經過單元測試的方式演示一下讀取會話信息和短信內容。在寫代碼以前,咱們先初始化一些數據,具體過程是啓動三個模擬器555四、555六、5558,讓5554分別與5556和5558互發短信,以下:url


咱們看到5554這小子名叫Jack;5556名叫Lucy,可能認識有幾天了,手機上存了她的號碼;5558名叫Lisa,可能剛認識,還沒來得及存號碼。Jack這小子真狠啊,想同時泡兩個妞,難道名字叫Jack的長得都很帥?下面是以上的兩個會話信息:spa


能夠看到,由於在聯繫人裏存了Lucy,因此顯示時並再也不直接顯示陌生的數字,而是其名字;括號內顯示了該會話的短信數;下面文字顯示了最後一條短信的內容和日期。

下面咱們建立一個名爲SMSTest的單元測試類,用於讀取會話信息和短信內容,代碼以下:

package com.scott.provider;
import java.text.SimpleDateFormat;
import android.content.ContentResolver;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.net.Uri;
import android.test.AndroidTestCase;
import android.util.Log;
public class SMSTest extends AndroidTestCase {
	
	private static final String TAG = "SMSTest";
	
	//會話
	private static final String CONVERSATIONS = "content://sms/conversations/";
	//查詢聯繫人
	private static final String CONTACTS_LOOKUP = "content://com.android.contacts/phone_lookup/";
	//所有短信
	private static final String SMS_ALL   = "content://sms/";
	//收件箱
//	private static final String SMS_INBOX = "content://sms/inbox";
	//已發送
//	private static final String SMS_SENT  = "content://sms/sent";
	//草稿箱
//	private static final String SMS_DRAFT = "content://sms/draft";
	
	private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	
	/**
	 * 讀取會話信息
	 */
	public void testReadConversation() {
		ContentResolver resolver = getContext().getContentResolver();
		Uri uri = Uri.parse(CONVERSATIONS);
		String[] projection = new String[]{"groups.group_thread_id AS group_id", "groups.msg_count AS msg_count",
						"groups.group_date AS last_date", "sms.body AS last_msg", "sms.address AS contact"};
		Cursor thinc = resolver.query(uri, projection, null, null, "groups.group_date DESC");	//查詢並按日期倒序
		Cursor richc = new CursorWrapper(thinc) {	//對Cursor進行處理,遇到號碼後獲取對應的聯繫人名稱
			@Override
        	public String getString(int columnIndex) {
				if(super.getColumnIndex("contact") == columnIndex){
					String contact = super.getString(columnIndex);
					//讀取聯繫人,查詢對應的名稱
					Uri uri = Uri.parse(CONTACTS_LOOKUP + contact);
					Cursor cursor = getContext().getContentResolver().query(uri, null, null, null, null);
					if(cursor.moveToFirst()){
        				String contactName = cursor.getString(cursor.getColumnIndex("display_name"));
        				return contactName;
        			}
					return contact;
				}
				return super.getString(columnIndex);
			}
		};
		while (richc.moveToNext()) {
			String groupId = "groupId: " + richc.getInt(richc.getColumnIndex("group_id"));
			String msgCount = "msgCount: " + richc.getLong(richc.getColumnIndex("msg_count"));
			String lastMsg = "lastMsg: " + richc.getString(richc.getColumnIndex("last_msg"));
			String contact = "contact: " + richc.getString(richc.getColumnIndex("contact"));
			String lastDate = "lastDate: " + dateFormat.format(richc.getLong(richc.getColumnIndex("last_date")));
			printLog(groupId, contact, msgCount, lastMsg, lastDate, "---------------END---------------");
		}
		richc.close();
	}
	
	/**
	 * 讀取短信
	 */
	public void testReadSMS() {
		ContentResolver resolver = getContext().getContentResolver();
		Uri uri = Uri.parse(SMS_ALL);
		String[] projection = {"thread_id AS group_id", "address AS contact", "body AS msg_content", "date", "type"};
		Cursor c = resolver.query(uri, projection, null, null, "date DESC");	//查詢並按日期倒序
		while (c.moveToNext()) {
			String groupId = "groupId: " + c.getInt(c.getColumnIndex("group_id"));
			String contact = "contact: " + c.getString(c.getColumnIndex("contact"));
			String msgContent = "msgContent: " + c.getString(c.getColumnIndex("msg_content"));
			String date = "date: " + dateFormat.format(c.getLong(c.getColumnIndex("date")));
			String type = "type: " + getTypeById(c.getInt(c.getColumnIndex("type")));
			printLog(groupId, contact, msgContent, date, type, "---------------END---------------");
		}
		c.close();
	}
	
	private String getTypeById(int typeId) {
		switch (typeId) {
		case 1: return "receive";
		case 2: return "send";
		case 3: return "draft";
		default: return "none";
		}
	}
	
	private void printLog(String...strings) {
		for (String s : strings) {
			Log.i(TAG, s == null ? "NULL" : s);
		}
	}
}

咱們先分析一下testReadConversation()方法,它是用來讀取全部的會話信息的,根據「content://sms/conversations/」這個URI進行會話數據的讀取操做,當取到數據後,對數據進一步的包裝,具體作法是遇到號碼時再根據「content://com.android.contacts/phone_lookup/」到聯繫人中查找對應的名稱,若是存在則顯示名稱而不是號碼。咱們注意到在查詢會話時使用到了projection,這些都是根據什麼制定的呢?這就須要咱們去看一下源代碼了。

咱們找到TelephonyProvider中的com/android/providers/telephony/SmsProvider.java文件,看一看究竟:

    @Override
    public Cursor query(Uri url, String[] projectionIn, String selection,
            String[] selectionArgs, String sort) {
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        // Generate the body of the query.
        int match = sURLMatcher.match(url);
        switch (match) {
        ...
        case SMS_CONVERSATIONS:
            qb.setTables("sms, (SELECT thread_id AS group_thread_id, MAX(date)AS group_date,"
                   + "COUNT(*) AS msg_count FROM sms GROUP BY thread_id) AS groups");
            qb.appendWhere("sms.thread_id = groups.group_thread_id AND sms.date ="
                   + "groups.group_date");
            qb.setProjectionMap(sConversationProjectionMap);
            break;
            ...
        }
		
        String orderBy = null;
        if (!TextUtils.isEmpty(sort)) {
            orderBy = sort;
        } else if (qb.getTables().equals(TABLE_SMS)) {
            orderBy = Sms.DEFAULT_SORT_ORDER;
        }
        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
        Cursor ret = qb.query(db, projectionIn, selection, selectionArgs,
                              null, null, orderBy);
        // TODO: Since the URLs are a mess, always use content://sms
        ret.setNotificationUri(getContext().getContentResolver(),
                NOTIFICATION_URI);
        return ret;
    }

咱們看到,在query方法的case語句中,若是是SMS_CONVERSATIONS類型的話,就爲SQLiteQueryBuilder實例對象qb設置對應的查詢表和where語句,另外還會爲其設置一個基本的查詢映射map即sConversationProjectionMap,這個變量在下面代碼中體現:

static {  
    ...  
    sURLMatcher.addURI("sms", "conversations", SMS_CONVERSATIONS);  
    sURLMatcher.addURI("sms", "conversations/*", SMS_CONVERSATIONS_ID);  
    ...  
  
    sConversationProjectionMap.put(Sms.Conversations.SNIPPET,  
        "sms.body AS snippet");  
    sConversationProjectionMap.put(Sms.Conversations.THREAD_ID,  
        "sms.thread_id AS thread_id");  
    sConversationProjectionMap.put(Sms.Conversations.MESSAGE_COUNT,  
        "groups.msg_count AS msg_count");  
    sConversationProjectionMap.put("delta", null);  
}

這幾對數據有什麼用處呢?若是咱們查詢時的projection爲null的話,sConversationProjectionMap就將轉換爲默認的projection,最後查詢結果中僅包含這三個最基本的字段:snippet、thread_id、msg_count,能夠表明一個會話的最簡明的信息,朋友們能夠親自試一試。

固然,若是想運行上面的測試用例,須要配置兩個權限:讀取短消息權限和讀取聯繫人權限,以下:

<!-- 讀取短消息 -->  
<uses-permission android:name="android.permission.READ_SMS" />  
<!-- 讀取聯繫人 -->  
<uses-permission android:name="android.permission.READ_CONTACTS"/>

而後咱們運行一下測試用例,結果以下:


以上就是讀取會話的所有內容,下面咱們再介紹其中的testReadSMS()方法。在這個方法中咱們試圖將全部的短消息都獲取到,使用了「content://sms/」進行查詢,這個查詢相對簡單了許多。另外代碼中也有幾個沒使用到的URI,他們分別是收件箱、已發送和草稿箱,這幾個查詢是「content://sms/」的子集,分別用了不一樣的選擇條件對短信表進行查詢,咱們看一下具體的源代碼:

    @Override
    public Cursor query(Uri url, String[] projectionIn, String selection,
            String[] selectionArgs, String sort) {
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        // Generate the body of the query.
        int match = sURLMatcher.match(url);
        switch (match) {
        
        case SMS_ALL:
            constructQueryForBox(qb, Sms.MESSAGE_TYPE_ALL);
            break;
        case SMS_INBOX:
            constructQueryForBox(qb, Sms.MESSAGE_TYPE_INBOX);
            break;
        case SMS_SENT:
            constructQueryForBox(qb, Sms.MESSAGE_TYPE_SENT);
            break;
        case SMS_DRAFT:
            constructQueryForBox(qb, Sms.MESSAGE_TYPE_DRAFT);
            break;
        }
		...
    }

能夠看到,他們都調用了constructQueryForBox方法,這個方法是幹什麼的呢?

private void constructQueryForBox(SQLiteQueryBuilder qb, int type) {  
    qb.setTables(TABLE_SMS);  
  
    if (type != Sms.MESSAGE_TYPE_ALL) {  
        qb.appendWhere("type=" + type);  
    }  
}

咱們發現它實際上是添加過濾條件的,若是不是查詢所有,則添加類型過濾信息,所以查詢出不一樣的短信集合。朋友們也能夠親自試一試不一樣類型的查詢。

另外,若是咱們想根據會話來查詢對應的短信集合的話,咱們能夠用如下兩種方式來完成:

1.「content://sms/」(selection:「thread_id=3」)

2.「content://sms/conversations/3」

第一種比較容易想到查詢的過程,即在上面的基礎上加上「thread_id=3」這條where語句便可;第二種是在會話path後面跟上會話id便可,具體的邏輯以下:

            case SMS_CONVERSATIONS_ID:
            int threadID;
            try {
                threadID = Integer.parseInt(url.getPathSegments().get(1));
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.d(TAG, "query conversations: threadID=" + threadID);
                }
            }
            catch (Exception ex) {
                Log.e(TAG,
                      "Bad conversation thread id: "
                      + url.getPathSegments().get(1));
                return null;
            }
            qb.setTables(TABLE_SMS);
            qb.appendWhere("thread_id = " + threadID);
            break;
相關文章
相關標籤/搜索