今天咱们来说一下如何利用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;