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

古之成大事者,不惟有超世之才,亦有堅韌不拔之志。北宋.蘇軾《晁錯論》

我們的前輩中那些成就大事的人,不單單有過人的智慧和才能,也須有堅韌不拔的意志。試問沒有堅韌的意志,如何寫得出複雜的系統,如何創造出偉大的產品?作爲程序員的我們,智慧和才能似乎不太欠缺,我們欠缺的也許是正是堅韌的意志,所以從今天起,鍛鍊自己的意志吧,在堅持理想的道路上,讓這種意志給自己力量。

今天我們來講一下如何利用ContentProvider讀寫短消息。

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

相對於聯繫人來說,短消息不是公開的,所以沒有專門的API供我們調用,這就要求我們根據源代碼進行分析研究,制定出一定的操作方案。

我們需要先找到短消息的數據源,打開/data/data/com.android.providers.telephony可以看到:


其中的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表示短信的內容。

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


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


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

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

  1. package com.scott.provider;  
  2.   
  3. import java.text.SimpleDateFormat;  
  4.   
  5. import android.content.ContentResolver;  
  6. import android.database.Cursor;  
  7. import android.database.CursorWrapper;  
  8. import android.net.Uri;  
  9. import android.test.AndroidTestCase;  
  10. import android.util.Log;  
  11.   
  12. public class SMSTest extends AndroidTestCase {  
  13.       
  14.     private static final String TAG = "SMSTest";  
  15.       
  16.     //會話  
  17.     private static final String CONVERSATIONS = "content://sms/conversations/";  
  18.     //查詢聯繫人  
  19.     private static final String CONTACTS_LOOKUP = "content://com.android.contacts/phone_lookup/";  
  20.     //全部短信  
  21.     private static final String SMS_ALL   = "content://sms/";  
  22.     //收件箱  
  23. //  private static final String SMS_INBOX = "content://sms/inbox";  
  24.     //已發送  
  25. //  private static final String SMS_SENT  = "content://sms/sent";  
  26.     //草稿箱  
  27. //  private static final String SMS_DRAFT = "content://sms/draft";  
  28.       
  29.     private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
  30.       
  31.     /** 
  32.      * 讀取會話信息 
  33.      */  
  34.     public void testReadConversation() {  
  35.         ContentResolver resolver = getContext().getContentResolver();  
  36.         Uri uri = Uri.parse(CONVERSATIONS);  
  37.         String[] projection = new String[]{"groups.group_thread_id AS group_id""groups.msg_count AS msg_count",  
  38.                         "groups.group_date AS last_date""sms.body AS last_msg""sms.address AS contact"};  
  39.         Cursor thinc = resolver.query(uri, projection, nullnull"groups.group_date DESC");   //查詢並按日期倒序  
  40.         Cursor richc = new CursorWrapper(thinc) {   //對Cursor進行處理,遇到號碼後獲取對應的聯繫人名稱  
  41.             @Override  
  42.             public String getString(int columnIndex) {  
  43.                 if(super.getColumnIndex("contact") == columnIndex){  
  44.                     String contact = super.getString(columnIndex);  
  45.                     //讀取聯繫人,查詢對應的名稱  
  46.                     Uri uri = Uri.parse(CONTACTS_LOOKUP + contact);  
  47.                     Cursor cursor = getContext().getContentResolver().query(uri, nullnullnullnull);  
  48.                     if(cursor.moveToFirst()){  
  49.                         String contactName = cursor.getString(cursor.getColumnIndex("display_name"));  
  50.                         return contactName;  
  51.                     }  
  52.                     return contact;  
  53.                 }  
  54.                 return super.getString(columnIndex);  
  55.             }  
  56.         };  
  57.         while (richc.moveToNext()) {  
  58.             String groupId = "groupId: " + richc.getInt(richc.getColumnIndex("group_id"));  
  59.             String msgCount = "msgCount: " + richc.getLong(richc.getColumnIndex("msg_count"));  
  60.             String lastMsg = "lastMsg: " + richc.getString(richc.getColumnIndex("last_msg"));  
  61.             String contact = "contact: " + richc.getString(richc.getColumnIndex("contact"));  
  62.             String lastDate = "lastDate: " + dateFormat.format(richc.getLong(richc.getColumnIndex("last_date")));  
  63.   
  64.             printLog(groupId, contact, msgCount, lastMsg, lastDate, "---------------END---------------");  
  65.         }  
  66.         richc.close();  
  67.     }  
  68.       
  69.     /** 
  70.      * 讀取短信 
  71.      */  
  72.     public void testReadSMS() {  
  73.         ContentResolver resolver = getContext().getContentResolver();  
  74.         Uri uri = Uri.parse(SMS_ALL);  
  75.         String[] projection = {"thread_id AS group_id""address AS contact""body AS msg_content""date""type"};  
  76.         Cursor c = resolver.query(uri, projection, nullnull"date DESC");    //查詢並按日期倒序  
  77.         while (c.moveToNext()) {  
  78.             String groupId = "groupId: " + c.getInt(c.getColumnIndex("group_id"));  
  79.             String contact = "contact: " + c.getString(c.getColumnIndex("contact"));  
  80.             String msgContent = "msgContent: " + c.getString(c.getColumnIndex("msg_content"));  
  81.             String date = "date: " + dateFormat.format(c.getLong(c.getColumnIndex("date")));  
  82.             String type = "type: " + getTypeById(c.getInt(c.getColumnIndex("type")));  
  83.   
  84.             printLog(groupId, contact, msgContent, date, type, "---------------END---------------");  
  85.         }  
  86.         c.close();  
  87.     }  
  88.       
  89.     private String getTypeById(int typeId) {  
  90.         switch (typeId) {  
  91.         case 1return "receive";  
  92.         case 2return "send";  
  93.         case 3return "draft";  
  94.         defaultreturn "none";  
  95.         }  
  96.     }  
  97.       
  98.     private void printLog(String...strings) {  
  99.         for (String s : strings) {  
  100.             Log.i(TAG, s == null ? "NULL" : s);  
  101.         }  
  102.     }  
  103. }  
我們先分析一下testReadConversation()方法,它是用來讀取所有的會話信息的,根據“content://sms/conversations/”這個URI進行會話數據的讀取操作,當取到數據後,對數據進一步的包裝,具體做法是遇到號碼時再根據“content://com.android.contacts/phone_lookup/”到聯繫人中查找對應的名稱,如果存在則顯示名稱而不是號碼。我們注意到在查詢會話時使用到了projection,這些都是根據什麼制定的呢?這就需要我們去看一下源代碼了。

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

  1. @Override  
  2. public Cursor query(Uri url, String[] projectionIn, String selection,  
  3.         String[] selectionArgs, String sort) {  
  4.     SQLiteQueryBuilder qb = new SQLiteQueryBuilder();  
  5.   
  6.     // Generate the body of the query.  
  7.     int match = sURLMatcher.match(url);  
  8.     switch (match) {  
  9.     ...  
  10.     case SMS_CONVERSATIONS:  
  11.         qb.setTables("sms, (SELECT thread_id AS group_thread_id, MAX(date)AS group_date,"  
  12.                + "COUNT(*) AS msg_count FROM sms GROUP BY thread_id) AS groups");  
  13.         qb.appendWhere("sms.thread_id = groups.group_thread_id AND sms.date ="  
  14.                + "groups.group_date");  
  15.         qb.setProjectionMap(sConversationProjectionMap);  
  16.         break;  
  17.         ...  
  18.     }  
  19.   
  20.     String orderBy = null;  
  21.   
  22.     if (!TextUtils.isEmpty(sort)) {  
  23.         orderBy = sort;  
  24.     } else if (qb.getTables().equals(TABLE_SMS)) {  
  25.         orderBy = Sms.DEFAULT_SORT_ORDER;  
  26.     }  
  27.   
  28.     SQLiteDatabase db = mOpenHelper.getReadableDatabase();  
  29.     Cursor ret = qb.query(db, projectionIn, selection, selectionArgs,  
  30.                           nullnull, orderBy);  
  31.   
  32.     // TODO: Since the URLs are a mess, always use content://sms  
  33.     ret.setNotificationUri(getContext().getContentResolver(),  
  34.             NOTIFICATION_URI);  
  35.     return ret;  
  36. }  

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

  1. static {  
  2.     ...  
  3.     sURLMatcher.addURI("sms""conversations", SMS_CONVERSATIONS);  
  4.     sURLMatcher.addURI("sms""conversations/*", SMS_CONVERSATIONS_ID);  
  5.     ...  
  6.   
  7.     sConversationProjectionMap.put(Sms.Conversations.SNIPPET,  
  8.         "sms.body AS snippet");  
  9.     sConversationProjectionMap.put(Sms.Conversations.THREAD_ID,  
  10.         "sms.thread_id AS thread_id");  
  11.     sConversationProjectionMap.put(Sms.Conversations.MESSAGE_COUNT,  
  12.         "groups.msg_count AS msg_count");  
  13.     sConversationProjectionMap.put("delta"null);  
  14. }  
這幾對數據有什麼用處呢?如果我們查詢時的projection爲null的話,sConversationProjectionMap就將轉換爲默認的projection,最後查詢結果中僅包含這三個最基本的字段:snippet、thread_id、msg_count,可以代表一個會話的最簡明的信息,朋友們可以親自試一試。

當然,如果想運行上面的測試用例,需要配置兩個權限:讀取短消息權限和讀取聯繫人權限,如下:

  1. <!-- 讀取短消息 -->  
  2. <uses-permission android:name="android.permission.READ_SMS" />  
  3. <!-- 讀取聯繫人 -->  
  4. <uses-permission android:name="android.permission.READ_CONTACTS"/>  
然後我們運行一下測試用例,結果如下:


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

  1.   @Override  
  2.   public Cursor query(Uri url, String[] projectionIn, String selection,  
  3.           String[] selectionArgs, String sort) {  
  4.       SQLiteQueryBuilder qb = new SQLiteQueryBuilder();  
  5.   
  6.       // Generate the body of the query.  
  7.       int match = sURLMatcher.match(url);  
  8.       switch (match) {  
  9.         
  10.       case SMS_ALL:  
  11.           constructQueryForBox(qb, Sms.MESSAGE_TYPE_ALL);  
  12.           break;  
  13.       case SMS_INBOX:  
  14.           constructQueryForBox(qb, Sms.MESSAGE_TYPE_INBOX);  
  15.           break;  
  16.   
  17.       case SMS_SENT:  
  18.           constructQueryForBox(qb, Sms.MESSAGE_TYPE_SENT);  
  19.           break;  
  20.   
  21.       case SMS_DRAFT:  
  22.           constructQueryForBox(qb, Sms.MESSAGE_TYPE_DRAFT);  
  23.           break;  
  24.       }  
  25. ...  
  26.   }  
可以看到,他們都調用了constructQueryForBox方法,這個方法是幹什麼的呢?
  1. private void constructQueryForBox(SQLiteQueryBuilder qb, int type) {  
  2.     qb.setTables(TABLE_SMS);  
  3.   
  4.     if (type != Sms.MESSAGE_TYPE_ALL) {  
  5.         qb.appendWhere("type=" + type);  
  6.     }  
  7. }  
我們發現它其實是添加過濾條件的,如果不是查詢全部,則添加類型過濾信息,因此查詢出不同的短信集合。朋友們也可以親自試一試不同類型的查詢。

另外,如果我們想根據會話來查詢對應的短信集合的話,我們可以用以下兩種方式來完成:

1.“content://sms/”(selection:“thread_id=3”)

2.“content://sms/conversations/3”

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

  1. case SMS_CONVERSATIONS_ID:  
  2. int threadID;  
  3. try {  
  4.     threadID = Integer.parseInt(url.getPathSegments().get(1));  
  5.     if (Log.isLoggable(TAG, Log.VERBOSE)) {  
  6.         Log.d(TAG, "query conversations: threadID=" + threadID);  
  7.     }  
  8. }  
  9. catch (Exception ex) {  
  10.     Log.e(TAG,  
  11.           "Bad conversation thread id: "  
  12.           + url.getPathSegments().get(1));  
  13.     return null;  
  14. }  
  15. qb.setTables(TABLE_SMS);  
  16. qb.appendWhere("thread_id = " + threadID);  
  17. break;  

我們可以看到,它最終還是和第一種方式走上了相同的道兒,兩者沒什麼本質上的區別。但是從簡單易用性上來講,這一種方式是比較好的,朋友們可以比較一下。

以上就是獲取會話內容和短信內容的全部信息,下面我們介紹一下短信的寫入操作。

發送和寫入短信

在某些場合,我們需要發送短信,並將短信寫入數據源中,這時我們就需要了解一下發送短信機制和寫入短信機制。

我們將試圖發送一條短信到指定的地址,同時將短信的內容寫入到短信數據源中,待短信發送成功後,我們告知用戶發送成功,待對方接收到短信後,我們告知用戶對方接收成功。

要實現這些功能,我們需要了解以下幾個重點內容:

1.使用android.telephony.SmsManager的API發送短信

2.使用ContentProvider機制對“content://sms/sent”這個URI進行寫入操作

3.註冊“SENT_SMS_ACTION”這個廣播地址,待短信發送成功後接收到這條廣播

4.註冊“DELIVERED_SMS_ACTION”這個廣播地址,待對方接收到短信後接收到這條廣播

下面我們就用代碼實現這些功能,創建一個名爲SMSActivity的Activity,如下:

  1. package com.scott.provider;  
  2.   
  3. import java.util.List;  
  4.   
  5. import android.app.Activity;  
  6. import android.app.PendingIntent;  
  7. import android.content.BroadcastReceiver;  
  8. import android.content.ContentValues;  
  9. import android.content.Context;  
  10. import android.content.Intent;  
  11. import android.content.IntentFilter;  
  12. import android.net.Uri;  
  13. import android.os.Bundle;  
  14. import android.telephony.SmsManager;  
  15. import android.view.View;  
  16. import android.widget.EditText;  
  17. import android.widget.Toast;  
  18.   
  19. public class SMSActivity extends Activity {  
  20.       
  21.     private SendReceiver sendReceiver = new SendReceiver();  
  22.     private DeliverReceiver deliverReceiver = new DeliverReceiver();  
  23.       
  24.     private EditText address;  
  25.     private EditText body;  
  26.       
  27.     @Override  
  28.     protected void onCreate(Bundle savedInstanceState) {  
  29.         super.onCreate(savedInstanceState);  
  30.         setContentView(R.layout.sms);  
  31.           
  32.         address = (EditText) findViewById(R.id.address);  
  33.         body = (EditText) findViewById(R.id.body);  
  34.           
  35.         //註冊發送成功的廣播  
  36.         registerReceiver(sendReceiver, new IntentFilter("SENT_SMS_ACTION"));  
  37.         //註冊接收成功的廣播  
  38.         registerReceiver(deliverReceiver, new IntentFilter("DELIVERED_SMS_ACTION"));  
  39.     }  
  40.   
  41.     @Override  
  42.     protected void onDestroy() {  
  43.         super.onDestroy();  
  44.           
  45.         unregisterReceiver(sendReceiver);  
  46.         unregisterReceiver(deliverReceiver);  
  47.     }  
  48.       
  49.     public void sendSMS(View view) {  
  50.         String address = this.address.getText().toString();  
  51.         String body = this.body.getText().toString();  
  52.         //android.telephony.SmsManager, not [android.telephony.gsm.SmsManager]  
  53.         SmsManager smsManager = SmsManager.getDefault();  
  54.         //短信發送成功或失敗後會產生一條SENT_SMS_ACTION的廣播  
  55.         PendingIntent sendIntent = PendingIntent.getBroadcast(this0new Intent("SENT_SMS_ACTION"), 0);  
  56.         //接收方成功收到短信後,發送方會產生一條DELIVERED_SMS_ACTION廣播  
  57.         PendingIntent deliveryIntent = PendingIntent.getBroadcast(this0new Intent("DELIVERED_SMS_ACTION"), 0);  
  58.         if (body.length() > 70) {    //如果字數超過70,需拆分成多條短信發送  
  59.             List<String> msgs = smsManager.divideMessage(body);  
  60.             for (String msg : msgs) {  
  61.                 smsManager.sendTextMessage(address, null, msg, sendIntent, deliveryIntent);                          
  62.             }  
  63.         } else {  
  64.             smsManager.sendTextMessage(address, null, body, sendIntent, deliveryIntent);  
  65.         }  
  66.           
  67.         //寫入到短信數據源  
  68.         ContentValues values = new ContentValues();  
  69.         values.put("address",address);  //發送地址  
  70.         values.put("body", body);   //消息內容  
  71.         values.put("date", System.currentTimeMillis()); //創建時間  
  72.         values.put("read"0);  //0:未讀;1:已讀  
  73.         values.put("type"2);  //1:接收;2:發送  
  74.         getContentResolver().insert(Uri.parse("content://sms/sent"), values);   //插入數據  
  75.     }  
  76.       
  77.     private class SendReceiver extends BroadcastReceiver {  
  78.   
  79.         @Override  
  80.         public void onReceive(Context context, Intent intent) {  
  81.             switch (getResultCode()) {  
  82.             case Activity.RESULT_OK:  
  83.                 Toast.makeText(context, "Sent Successfully.", Toast.LENGTH_SHORT).show();  
  84.                 break;  
  85.             default:  
  86.                 Toast.makeText(context, "Failed to Send.", Toast.LENGTH_SHORT).show();  
  87.             }  
  88.         }  
  89.     }  
  90.   
  91.     /** 
  92.      * 發送方的短信發送到對方手機上之後,對方手機會返回給運營商一個信號, 
  93.      * 運營商再把這個信號發給發送方,發送方此時可確認對方接收成功 
  94.      * 模擬器不支持,真機上需等待片刻 
  95.      * @author user 
  96.      * 
  97.      */  
  98.     private class DeliverReceiver extends BroadcastReceiver {  
  99.   
  100.         @Override  
  101.         public void onReceive(Context context, Intent intent) {  
  102.             Toast.makeText(context, "Delivered Successfully.", Toast.LENGTH_SHORT).show();  
  103.         }  
  104.     }  
  105. }  
佈局文件如下:
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:orientation="vertical"  
  4.     android:layout_width="fill_parent"  
  5.     android:layout_height="fill_parent">  
  6.     <TextView  
  7.         android:layout_width="fill_parent"  
  8.         android:layout_height="wrap_content"  
  9.         android:text="address"/>  
  10.     <EditText  
  11.         android:id="@+id/address"  
  12.         android:layout_width="fill_parent"  
  13.         android:layout_height="wrap_content"/>  
  14.     <TextView  
  15.         android:layout_width="fill_parent"  
  16.         android:layout_height="wrap_content"  
  17.         android:text="body"/>  
  18.     <EditText  
  19.         android:id="@+id/body"  
  20.         android:layout_width="fill_parent"  
  21.         android:layout_height="150dp"  
  22.         android:gravity="top"/>  
  23.     <Button  
  24.         android:layout_width="fill_parent"  
  25.         android:layout_height="wrap_content"  
  26.         android:text="sendSMS"  
  27.         android:onClick="sendSMS"/>  
  28. </LinearLayout>  
需要注意的是,這個過程要聲明發送短信的權限和寫入短信的權限,我們在AndroidManifest.xml的聲明如下:
  1. <!-- 發送短消息 -->  
  2.    <uses-permission android:name="android.permission.SEND_SMS"/>  
  3.    <!-- 寫入短消息 -->  
  4.    <uses-permission android:name="android.permission.WRITE_SMS" />  
然後,運行該程序,我們讓Jack給Lisa發送一條短信,看看結果如何:


看來我們的操作成功了,到底Jack能不能泡到Lisa呢,朋友們,發揮你們的想象力吧。

最後需要注意的一件事,代碼裏也提到過,就是在模擬器測試時,是不支持“接收成功”這個功能的,所以朋友們想要看到“Delivered Successfully”,還必須在真機上試,並且需要耐心等上片刻。感興趣的朋友趕緊試一試吧。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章