古之成大事者,不惟有超世之才,亦有堅韌不拔之志。北宋.蘇軾《晁錯論》
我們的前輩中那些成就大事的人,不單單有過人的智慧和才能,也須有堅韌不拔的意志。試問沒有堅韌的意志,如何寫得出複雜的系統,如何創造出偉大的產品?作爲程序員的我們,智慧和才能似乎不太欠缺,我們欠缺的也許是正是堅韌的意志,所以從今天起,鍛鍊自己的意志吧,在堅持理想的道路上,讓這種意志給自己力量。
今天我們來講一下如何利用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的單元測試類,用於讀取會話信息和短信內容,代碼如下:
- 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);
- }
- }
- }
我們找到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);
- }
當然,如果想運行上面的測試用例,需要配置兩個權限:讀取短消息權限和讀取聯繫人權限,如下:
- <!-- 讀取短消息 -->
- <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;
- }
- ...
- }
- 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;
我們可以看到,它最終還是和第一種方式走上了相同的道兒,兩者沒什麼本質上的區別。但是從簡單易用性上來講,這一種方式是比較好的,朋友們可以比較一下。
以上就是獲取會話內容和短信內容的全部信息,下面我們介紹一下短信的寫入操作。
發送和寫入短信
在某些場合,我們需要發送短信,並將短信寫入數據源中,這時我們就需要了解一下發送短信機制和寫入短信機制。
我們將試圖發送一條短信到指定的地址,同時將短信的內容寫入到短信數據源中,待短信發送成功後,我們告知用戶發送成功,待對方接收到短信後,我們告知用戶對方接收成功。
要實現這些功能,我們需要了解以下幾個重點內容:
1.使用android.telephony.SmsManager的API發送短信
2.使用ContentProvider機制對“content://sms/sent”這個URI進行寫入操作
3.註冊“SENT_SMS_ACTION”這個廣播地址,待短信發送成功後接收到這條廣播
4.註冊“DELIVERED_SMS_ACTION”這個廣播地址,待對方接收到短信後接收到這條廣播
下面我們就用代碼實現這些功能,創建一個名爲SMSActivity的Activity,如下:
- package com.scott.provider;
- import java.util.List;
- import android.app.Activity;
- import android.app.PendingIntent;
- import android.content.BroadcastReceiver;
- import android.content.ContentValues;
- import android.content.Context;
- import android.content.Intent;
- import android.content.IntentFilter;
- import android.net.Uri;
- import android.os.Bundle;
- import android.telephony.SmsManager;
- import android.view.View;
- import android.widget.EditText;
- import android.widget.Toast;
- public class SMSActivity extends Activity {
- private SendReceiver sendReceiver = new SendReceiver();
- private DeliverReceiver deliverReceiver = new DeliverReceiver();
- private EditText address;
- private EditText body;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.sms);
- address = (EditText) findViewById(R.id.address);
- body = (EditText) findViewById(R.id.body);
- //註冊發送成功的廣播
- registerReceiver(sendReceiver, new IntentFilter("SENT_SMS_ACTION"));
- //註冊接收成功的廣播
- registerReceiver(deliverReceiver, new IntentFilter("DELIVERED_SMS_ACTION"));
- }
- @Override
- protected void onDestroy() {
- super.onDestroy();
- unregisterReceiver(sendReceiver);
- unregisterReceiver(deliverReceiver);
- }
- public void sendSMS(View view) {
- String address = this.address.getText().toString();
- String body = this.body.getText().toString();
- //android.telephony.SmsManager, not [android.telephony.gsm.SmsManager]
- SmsManager smsManager = SmsManager.getDefault();
- //短信發送成功或失敗後會產生一條SENT_SMS_ACTION的廣播
- PendingIntent sendIntent = PendingIntent.getBroadcast(this, 0, new Intent("SENT_SMS_ACTION"), 0);
- //接收方成功收到短信後,發送方會產生一條DELIVERED_SMS_ACTION廣播
- PendingIntent deliveryIntent = PendingIntent.getBroadcast(this, 0, new Intent("DELIVERED_SMS_ACTION"), 0);
- if (body.length() > 70) { //如果字數超過70,需拆分成多條短信發送
- List<String> msgs = smsManager.divideMessage(body);
- for (String msg : msgs) {
- smsManager.sendTextMessage(address, null, msg, sendIntent, deliveryIntent);
- }
- } else {
- smsManager.sendTextMessage(address, null, body, sendIntent, deliveryIntent);
- }
- //寫入到短信數據源
- ContentValues values = new ContentValues();
- values.put("address",address); //發送地址
- values.put("body", body); //消息內容
- values.put("date", System.currentTimeMillis()); //創建時間
- values.put("read", 0); //0:未讀;1:已讀
- values.put("type", 2); //1:接收;2:發送
- getContentResolver().insert(Uri.parse("content://sms/sent"), values); //插入數據
- }
- private class SendReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- switch (getResultCode()) {
- case Activity.RESULT_OK:
- Toast.makeText(context, "Sent Successfully.", Toast.LENGTH_SHORT).show();
- break;
- default:
- Toast.makeText(context, "Failed to Send.", Toast.LENGTH_SHORT).show();
- }
- }
- }
- /**
- * 發送方的短信發送到對方手機上之後,對方手機會返回給運營商一個信號,
- * 運營商再把這個信號發給發送方,發送方此時可確認對方接收成功
- * 模擬器不支持,真機上需等待片刻
- * @author user
- *
- */
- private class DeliverReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- Toast.makeText(context, "Delivered Successfully.", Toast.LENGTH_SHORT).show();
- }
- }
- }
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent">
- <TextView
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:text="address"/>
- <EditText
- android:id="@+id/address"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"/>
- <TextView
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:text="body"/>
- <EditText
- android:id="@+id/body"
- android:layout_width="fill_parent"
- android:layout_height="150dp"
- android:gravity="top"/>
- <Button
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:text="sendSMS"
- android:onClick="sendSMS"/>
- </LinearLayout>
- <!-- 發送短消息 -->
- <uses-permission android:name="android.permission.SEND_SMS"/>
- <!-- 寫入短消息 -->
- <uses-permission android:name="android.permission.WRITE_SMS" />
看來我們的操作成功了,到底Jack能不能泡到Lisa呢,朋友們,發揮你們的想象力吧。
最後需要注意的一件事,代碼裏也提到過,就是在模擬器測試時,是不支持“接收成功”這個功能的,所以朋友們想要看到“Delivered Successfully”,還必須在真機上試,並且需要耐心等上片刻。感興趣的朋友趕緊試一試吧。