-- 做者 謝恩銘 轉載請註明出處「程序員聯盟」ProgrammerLeague
原文 : www.jianshu.com/p/e85884989…android
這篇文章算是一個小小的 Android 開發經驗總結,也是拋磚引玉。若有錯謬,歡迎指正。git
最近在 Github 上一個開源的 Android Messages app 中,修正了一個需求的實現。程序員
這個開源的 Android app 是 QKSMS 。還不錯的一個遵循 Material Design 的開源免費的 Android 消息應用,不過貌似做者不怎麼維護了,比較惋惜。github
以前我也爲這個開源項目貢獻過一些補丁,我還寫了一篇文章專門講如何爲 Github 的開源項目提交補丁:
Github | 如何貢獻Android開源項目和提交補丁bash
需求以下:微信
在退出 Airplane mode(飛行模式)以後自動發送以前失敗的全部 SMS(Short Message Service,就是「短信」)和 MMS(Multimedia Message Service,就是「彩信」,例如 圖片,視頻,音頻,VCard 等等)。網絡
以前做者其實已經寫了這一塊代碼,可是沒有知足需求。app
他是這麼處理的:ide
既然要在退出飛行模式時從新發送,那麼就用一個 BroadcastReceiver 來接收系統的飛行模式狀態改變的廣播,一旦接收到廣播,即從新發送全部失敗的 SMS和 MMS。測試
關鍵代碼以下:
// 類定義
public class AirplaneModeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!intent.getAction().equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) {
return;
}
// 若是是進入飛行模式,那麼什麼也不作
if (intent.getBooleanExtra("state", true)) {
return;
}
// 找出全部包含未成功的消息的對話(conversation),返回 Cursor
Cursor conversationCursor = context.getContentResolver().query(
SmsHelper.CONVERSATIONS_CONTENT_PROVIDER,
Conversation.ALL_THREADS_PROJECTION,
Conversation.FAILED_SELECTION, null,
SmsHelper.sortDateDesc
);
// 遍歷每一個這樣的對話
while (conversationCursor.moveToNext()) {
Uri uri = ContentUris.withAppendedId(SmsHelper.MMS_SMS_CONTENT_PROVIDER, conversationCursor.getLong(Conversation.ID));
// 找出對話中全部未成功的消息,返回 Cursor
Cursor cursor = context.getContentResolver().query(uri, MessageColumns.PROJECTION,
SmsHelper.FAILED_SELECTION, null, SmsHelper.sortDateAsc);
// 把 Cursor 映射到一個 MessageItem 對象,而後從新發送它
MessageColumns.ColumnsMap columnsMap = new MessageColumns.ColumnsMap(cursor);
while (cursor.moveToNext()) {
try {
MessageItem message = new MessageItem(context, cursor.getString(columnsMap.mColumnMsgType),
cursor, columnsMap, null, true);
sendMessage(context, message);
} catch (MmsException e) {
e.printStackTrace();
}
}
cursor.close();
}
conversationCursor.close();
}
// 發送消息
private void sendMessage(Context context, MessageItem messageItem) {
Transaction sendTransaction = new Transaction(context, SmsHelper.getSendSettings(context));
Message message = new Message(messageItem.mBody, messageItem.mAddress);
message.setType(Message.TYPE_SMSMMS);
context.getContentResolver().delete(messageItem.mMessageUri, null, null);
sendTransaction.sendNewMessage(message, 0);
}
}複製代碼
<!-- 在 AndroidManifest.xml 中註冊這個 BroadcastReceiver -->
<receiver android:name=".AirplaneModeReceiver">
<intent-filter>
<action android:name="android.intent.action.AIRPLANE_MODE" />
</intent-filter>
</receiver>複製代碼
上面的代碼中有些內容,好比從新發送消息的代碼,包含一些 QKSMS 中的定義,不過很多代碼都是用的 Android 源碼中的定義。
例如:
public static final Uri CONVERSATIONS_CONTENT_PROVIDER = Uri.parse("content://mms-sms/conversations?simple=true");複製代碼
SmsHelper.CONVERSATIONS_CONTENT_PROVIDER 這個 Uri 是表示全部的對話。
而 Conversation.ALL_THREADS_PROJECTION 的定義以下:
public static final String[] ALL_THREADS_PROJECTION = {
Threads._ID, Threads.DATE, Threads.MESSAGE_COUNT, Threads.RECIPIENT_IDS,
Threads.SNIPPET, Threads.SNIPPET_CHARSET, Threads.READ, Threads.ERROR,
Threads.HAS_ATTACHMENT
};複製代碼
SmsHelper.MMS_SMS_CONTENT_PROVIDER 也是標準的 Android 的 Uri:
public static final Uri MMS_SMS_CONTENT_PROVIDER = Uri.parse("content://mms-sms/conversations/");複製代碼
而 MessageItem 這樣的類基本使用了 Android 的 AOSP(Android Open Source Project)中的代碼。
QKSMS 這個開源項目大量使用了 AOSP 中的代碼,這也是它必須開源的緣由。
其餘的一些定義則是 QKSMS 中自定義的類或變量,你們能夠自行去看 Github 上的 源碼,或者 git clone 下來用 Android Studio 查看。上面的從新發送 SMS 和 MMS 的代碼是一個能夠借鑑的實現。
上面的代碼看似能夠實現需求。實際測試時發現,一旦退出飛行模式,確實會從新發送失敗的 SMS 和 MMS,可是仍是會失敗。
這是爲何呢?
緣由很簡單:
剛退出飛行模式時,系統還須要一些時間去從新鏈接 Cellular network(蜂窩網絡,又稱 移動網絡(mobile network))。若是在檢測到關閉飛行模式時當即發送未成功的消息,系統尚未鏈接完畢,不免會失敗。
既然上面的實現不可行,那麼應該如何來實現呢?
首先咱們要知道:
MMS 須要移動數據,SMS 不須要移動數據。
就是說:MMS 的發送和接收須要 Mobile data,而 SMS 則並不須要。固然了,SMS 和 MMS 都須要有運營商(Network Operator)的網絡,也就是須要有 SIM 卡。
開啓和關閉 Mobile data 的操做通常能夠在 Android 手機裏的「儀表盤」上這樣實現:
如上圖所示,左邊的 Mobile data 是用於開啓或關閉 Mobile data(移動數據)。右邊的 Airplane mode 是用於開啓或關閉飛行模式。
所以,有兩種狀況:
開啓飛行模式前,並無開啓 Mobile data。這樣的話,在關閉飛行模式後,MMS 也不能發送,只有 SMS 能被髮送,由於只有 Cellular network 會從新鏈接。
開啓飛行模式前,已經開啓 Mobile data。這樣的話,在關閉飛行模式後,MMS 和 SMS 均可以被髮送。由於 Cellular network 和 Mobile data 都會從新鏈接。
咱們的代碼也就須要分兩部分來監聽:
負責從新發送 SMS 的:只須要監聽 Cellular network(移動網絡)的狀態改變。若是狀態爲已鏈接,則嘗試從新發送未成功的 SMS。
負責從新發送 MMS 的:由於 Cellular network 鏈接還不夠,還須要 Mobile data 鏈接。所以須要監聽 Mobile data(移動數據)狀態改變。若是狀態爲已鏈接,則嘗試從新發送未成功的 MMS。
咱們須要監聽 "android.intent.action.SERVICE_STATE" 這個 actioin 對應的廣播:
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals("android.intent.action.SERVICE_STATE")) {
if (isCellularNetworkOn(context)) {
// 當 Cellular network 鏈接上以後,主要用於從新發送未成功的 SMS
// 從新發送的實現 ...
}
}
}複製代碼
咱們須要監聽 ConnectivityManager.CONNECTIVITY_ACTION (也就是 "android.net.conn.CONNECTIVITY_CHANGE" )這個 actioin 對應的廣播:
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
NetworkInfo mNetworkInfo = intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
if ((mNetworkInfo == null) || (mNetworkInfo.getType() != ConnectivityManager.TYPE_MOBILE)) {
return;
}
if (mNetworkInfo.isConnected()) {
// 當 Mobile data 鏈接上時,主要用於從新發送未成功的 MMS
// 從新發送的實現 ...
}
}
}複製代碼
咱們能夠把兩個監聽的部分合並在一個 BroadcastReceiver 中實現,合併後的代碼以下 :
/**
* 監聽移動網絡狀態改變,以便可以從新發送未成功的消息(SMS 和 MMS)
* MMS 須要 Mobile data,而 SMS不須要
*/
public class MobileNetworkReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals("android.intent.action.SERVICE_STATE")) {
if (isCellularNetworkOn(context)) {
// 當 Cellular network 鏈接上時,主要用於從新發送未成功的 SMS
resendFailedMessages(context);
}
} else if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
NetworkInfo mNetworkInfo = intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
if ((mNetworkInfo == null) || (mNetworkInfo.getType() != ConnectivityManager.TYPE_MOBILE)) {
return;
}
if (mNetworkInfo.isConnected()) {
// 當 Mobile data 鏈接上時,主要用於從新發送未成功的 MMS
resendFailedMessages(context);
}
}
}
// 從新發送失敗的消息(SMS 或 MMS)
private void resendFailedMessages(Context context) {
// 找出全部包含未成功的消息的對話(conversation),返回 Cursor
Cursor conversationCursor = context.getContentResolver().query(
SmsHelper.CONVERSATIONS_CONTENT_PROVIDER,
Conversation.ALL_THREADS_PROJECTION,
Conversation.FAILED_SELECTION, null,
SmsHelper.sortDateDesc
);
// 遍歷每一個這樣的對話
while (conversationCursor.moveToNext()) {
Uri uri = ContentUris.withAppendedId(SmsHelper.MMS_SMS_CONTENT_PROVIDER, conversationCursor.getLong(Conversation.ID));
// 找出對話中全部未成功的消息,返回 Cursor
Cursor cursor = context.getContentResolver().query(uri, MessageColumns.PROJECTION,
SmsHelper.FAILED_SELECTION, null, SmsHelper.sortDateAsc);
// 把 Cursor 映射到一個 MessageItem 對象,而後從新發送它
MessageColumns.ColumnsMap columnsMap = new MessageColumns.ColumnsMap(cursor);
while (cursor.moveToNext()) {
try {
MessageItem message = new MessageItem(context, cursor.getString(columnsMap.mColumnMsgType),
cursor, columnsMap, null, true);
sendMessage(context, message);
} catch (MmsException e) {
e.printStackTrace();
}
}
cursor.close();
}
conversationCursor.close();
}
// 發送消息
private void sendMessage(Context context, MessageItem messageItem) {
Transaction sendTransaction = new Transaction(context, SmsHelper.getSendSettings(context));
Message message = new Message(messageItem.mBody, messageItem.mAddress);
message.setType(Message.TYPE_SMSMMS);
sendTransaction.sendNewMessage(message, 0);
context.getContentResolver().delete(messageItem.mMessageUri, null, null);
}
// 判斷 Cellular network 是否已鏈接
public static boolean isCellularNetworkOn(Context context) {
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
return telephonyManager.getNetworkOperator() != null && !telephonyManager.getNetworkOperator().isEmpty();
}
}複製代碼
<!-- 在 AndroidManifest.xml 中添加權限 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 在 AndroidManifest.xml 中註冊這個 BroadcastReceiver -->
<receiver android:name=".MobileNetworkReceiver">
<intent-filter>
<action android:name="android.intent.action.SERVICE_STATE" />
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>複製代碼
微信公衆號「程序員聯盟」ProgrammerLeague
我是謝恩銘,在巴黎奮鬥的軟件工程師。
關於我熱愛生活,喜歡游泳,略懂烹飪。人生格言:「向着標杆直跑」