1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.messaging.datamodel.action; 18 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.net.Uri; 22 import android.os.Bundle; 23 import android.os.Parcel; 24 import android.os.Parcelable; 25 import android.provider.Telephony.Mms; 26 import android.provider.Telephony.Sms; 27 28 import com.android.messaging.Factory; 29 import com.android.messaging.datamodel.BugleDatabaseOperations; 30 import com.android.messaging.datamodel.DataModel; 31 import com.android.messaging.datamodel.DatabaseHelper.MessageColumns; 32 import com.android.messaging.datamodel.DatabaseWrapper; 33 import com.android.messaging.datamodel.MessagingContentProvider; 34 import com.android.messaging.datamodel.SyncManager; 35 import com.android.messaging.datamodel.data.MessageData; 36 import com.android.messaging.datamodel.data.ParticipantData; 37 import com.android.messaging.sms.MmsUtils; 38 import com.android.messaging.util.Assert; 39 import com.android.messaging.util.LogUtil; 40 41 import java.util.ArrayList; 42 43 /** 44 * Action used to send an outgoing message. It writes MMS messages to the telephony db 45 * ({@link InsertNewMessageAction}) writes SMS messages to the telephony db). It also 46 * initiates the actual sending. It will all be used for re-sending a failed message. 47 * <p> 48 * This class is public (not package-private) because the SMS/MMS (e.g. MmsUtils) classes need to 49 * access the EXTRA_* fields for setting up the 'sent' pending intent. 50 */ 51 public class SendMessageAction extends Action implements Parcelable { 52 private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG; 53 54 /** 55 * Queue sending of existing message (can only be called during execute of action) 56 */ queueForSendInBackground(final String messageId, final Action processingAction)57 static boolean queueForSendInBackground(final String messageId, 58 final Action processingAction) { 59 final SendMessageAction action = new SendMessageAction(); 60 return action.queueAction(messageId, processingAction); 61 } 62 63 public static final boolean DEFAULT_DELIVERY_REPORT_MODE = false; 64 public static final int MAX_SMS_RETRY = 3; 65 66 // Core parameters needed for all types of message 67 private static final String KEY_MESSAGE_ID = "message_id"; 68 private static final String KEY_MESSAGE = "message"; 69 private static final String KEY_MESSAGE_URI = "message_uri"; 70 private static final String KEY_SUB_PHONE_NUMBER = "sub_phone_number"; 71 72 // For sms messages a few extra values are included in the bundle 73 private static final String KEY_RECIPIENT = "recipient"; 74 private static final String KEY_RECIPIENTS = "recipients"; 75 private static final String KEY_SMS_SERVICE_CENTER = "sms_service_center"; 76 77 // Values we attach to the pending intent that's fired when the message is sent. 78 // Only applicable when sending via the platform APIs on L+. 79 public static final String KEY_SUB_ID = "sub_id"; 80 public static final String EXTRA_MESSAGE_ID = "message_id"; 81 public static final String EXTRA_UPDATED_MESSAGE_URI = "updated_message_uri"; 82 public static final String EXTRA_CONTENT_URI = "content_uri"; 83 public static final String EXTRA_RESPONSE_IMPORTANT = "response_important"; 84 85 /** 86 * Constructor used for retrying sending in the background (only message id available) 87 */ SendMessageAction()88 private SendMessageAction() { 89 super(); 90 } 91 92 /** 93 * Read message from database and queue actual sending 94 */ queueAction(final String messageId, final Action processingAction)95 private boolean queueAction(final String messageId, final Action processingAction) { 96 actionParameters.putString(KEY_MESSAGE_ID, messageId); 97 98 final long timestamp = System.currentTimeMillis(); 99 final DatabaseWrapper db = DataModel.get().getDatabase(); 100 101 final MessageData message = BugleDatabaseOperations.readMessage(db, messageId); 102 // Check message can be resent 103 if (message != null && message.canSendMessage()) { 104 final boolean isSms = (message.getProtocol() == MessageData.PROTOCOL_SMS); 105 106 final ParticipantData self = BugleDatabaseOperations.getExistingParticipant( 107 db, message.getSelfId()); 108 final Uri messageUri = message.getSmsMessageUri(); 109 final String conversationId = message.getConversationId(); 110 111 // Update message status 112 if (message.getYetToSend()) { 113 // Initial sending of message 114 message.markMessageSending(timestamp); 115 } else { 116 // Automatic resend of message 117 message.markMessageResending(timestamp); 118 } 119 if (!updateMessageAndStatus(isSms, message, null /* messageUri */, false /*notify*/)) { 120 // If message is missing in the telephony database we don't need to send it 121 return false; 122 } 123 124 final ArrayList<String> recipients = 125 BugleDatabaseOperations.getRecipientsForConversation(db, conversationId); 126 127 // Update action state with parameters needed for background sending 128 actionParameters.putParcelable(KEY_MESSAGE_URI, messageUri); 129 actionParameters.putParcelable(KEY_MESSAGE, message); 130 actionParameters.putStringArrayList(KEY_RECIPIENTS, recipients); 131 actionParameters.putInt(KEY_SUB_ID, self.getSubId()); 132 actionParameters.putString(KEY_SUB_PHONE_NUMBER, self.getNormalizedDestination()); 133 134 if (isSms) { 135 final String smsc = BugleDatabaseOperations.getSmsServiceCenterForConversation( 136 db, conversationId); 137 actionParameters.putString(KEY_SMS_SERVICE_CENTER, smsc); 138 139 if (recipients.size() == 1) { 140 final String recipient = recipients.get(0); 141 142 actionParameters.putString(KEY_RECIPIENT, recipient); 143 // Queue actual sending for SMS 144 processingAction.requestBackgroundWork(this); 145 146 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { 147 LogUtil.d(TAG, "SendMessageAction: Queued SMS message " + messageId 148 + " for sending"); 149 } 150 return true; 151 } else { 152 LogUtil.wtf(TAG, "Trying to resend a broadcast SMS - not allowed"); 153 } 154 } else { 155 // Queue actual sending for MMS 156 processingAction.requestBackgroundWork(this); 157 158 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { 159 LogUtil.d(TAG, "SendMessageAction: Queued MMS message " + messageId 160 + " for sending"); 161 } 162 return true; 163 } 164 } 165 166 return false; 167 } 168 169 170 /** 171 * Never called 172 */ 173 @Override executeAction()174 protected Object executeAction() { 175 Assert.fail("SendMessageAction must be queued rather than started"); 176 return null; 177 } 178 179 /** 180 * Send message on background worker thread 181 */ 182 @Override doBackgroundWork()183 protected Bundle doBackgroundWork() { 184 final MessageData message = actionParameters.getParcelable(KEY_MESSAGE); 185 final String messageId = actionParameters.getString(KEY_MESSAGE_ID); 186 Uri messageUri = actionParameters.getParcelable(KEY_MESSAGE_URI); 187 Uri updatedMessageUri = null; 188 final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS; 189 final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID); 190 final String subPhoneNumber = actionParameters.getString(KEY_SUB_PHONE_NUMBER); 191 192 LogUtil.i(TAG, "SendMessageAction: Sending " + (isSms ? "SMS" : "MMS") + " message " 193 + messageId + " in conversation " + message.getConversationId()); 194 195 int status; 196 int rawStatus = MessageData.RAW_TELEPHONY_STATUS_UNDEFINED; 197 int resultCode = MessageData.UNKNOWN_RESULT_CODE; 198 if (isSms) { 199 Assert.notNull(messageUri); 200 final String recipient = actionParameters.getString(KEY_RECIPIENT); 201 final String messageText = message.getMessageText(); 202 final String smsServiceCenter = actionParameters.getString(KEY_SMS_SERVICE_CENTER); 203 final boolean deliveryReportRequired = MmsUtils.isDeliveryReportRequired(subId); 204 205 status = MmsUtils.sendSmsMessage(recipient, messageText, messageUri, subId, 206 smsServiceCenter, deliveryReportRequired); 207 } else { 208 final Context context = Factory.get().getApplicationContext(); 209 final ArrayList<String> recipients = 210 actionParameters.getStringArrayList(KEY_RECIPIENTS); 211 if (messageUri == null) { 212 final long timestamp = message.getReceivedTimeStamp(); 213 214 // Inform sync that message has been added at local received timestamp 215 final SyncManager syncManager = DataModel.get().getSyncManager(); 216 syncManager.onNewMessageInserted(timestamp); 217 218 // For MMS messages first need to write to telephony (resizing images if needed) 219 updatedMessageUri = MmsUtils.insertSendingMmsMessage(context, recipients, 220 message, subId, subPhoneNumber, timestamp); 221 if (updatedMessageUri != null) { 222 messageUri = updatedMessageUri; 223 // To prevent Sync seeing inconsistent state must write to DB on this thread 224 updateMessageUri(messageId, updatedMessageUri); 225 226 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 227 LogUtil.v(TAG, "SendMessageAction: Updated message " + messageId 228 + " with new uri " + messageUri); 229 } 230 } 231 } 232 if (messageUri != null) { 233 // Actually send the MMS 234 final Bundle extras = new Bundle(); 235 extras.putString(EXTRA_MESSAGE_ID, messageId); 236 extras.putParcelable(EXTRA_UPDATED_MESSAGE_URI, updatedMessageUri); 237 final MmsUtils.StatusPlusUri result = MmsUtils.sendMmsMessage(context, subId, 238 messageUri, extras); 239 if (result == MmsUtils.STATUS_PENDING) { 240 // Async send, so no status yet 241 LogUtil.d(TAG, "SendMessageAction: Sending MMS message " + messageId 242 + " asynchronously; waiting for callback to finish processing"); 243 return null; 244 } 245 status = result.status; 246 rawStatus = result.rawStatus; 247 resultCode = result.resultCode; 248 } else { 249 status = MmsUtils.MMS_REQUEST_MANUAL_RETRY; 250 } 251 } 252 253 // When we fast-fail before calling the MMS lib APIs (e.g. airplane mode, 254 // sending message is deleted). 255 ProcessSentMessageAction.processMessageSentFastFailed(messageId, messageUri, 256 updatedMessageUri, subId, isSms, status, rawStatus, resultCode); 257 return null; 258 } 259 updateMessageUri(final String messageId, final Uri updatedMessageUri)260 private void updateMessageUri(final String messageId, final Uri updatedMessageUri) { 261 final DatabaseWrapper db = DataModel.get().getDatabase(); 262 db.beginTransaction(); 263 try { 264 final ContentValues values = new ContentValues(); 265 values.put(MessageColumns.SMS_MESSAGE_URI, updatedMessageUri.toString()); 266 BugleDatabaseOperations.updateMessageRow(db, messageId, values); 267 db.setTransactionSuccessful(); 268 } finally { 269 db.endTransaction(); 270 } 271 } 272 273 @Override processBackgroundResponse(final Bundle response)274 protected Object processBackgroundResponse(final Bundle response) { 275 // Nothing to do here, post-send tasks handled by ProcessSentMessageAction 276 return null; 277 } 278 279 /** 280 * Update message status to reflect success or failure 281 */ 282 @Override processBackgroundFailure()283 protected Object processBackgroundFailure() { 284 final String messageId = actionParameters.getString(KEY_MESSAGE_ID); 285 final MessageData message = actionParameters.getParcelable(KEY_MESSAGE); 286 final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS; 287 final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID); 288 final int resultCode = actionParameters.getInt(ProcessSentMessageAction.KEY_RESULT_CODE); 289 final int httpStatusCode = 290 actionParameters.getInt(ProcessSentMessageAction.KEY_HTTP_STATUS_CODE); 291 292 ProcessSentMessageAction.processResult(messageId, null /* updatedMessageUri */, 293 MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED, 294 isSms, this, subId, resultCode, httpStatusCode); 295 296 return null; 297 } 298 299 /** 300 * Update the message status (and message itself if necessary) 301 * @param isSms whether this is an SMS or MMS 302 * @param message message to update 303 * @param updatedMessageUri message uri for newly-inserted messages; null otherwise 304 * @param clearSeen whether the message 'seen' status should be reset if error occurs 305 */ updateMessageAndStatus(final boolean isSms, final MessageData message, final Uri updatedMessageUri, final boolean clearSeen)306 public static boolean updateMessageAndStatus(final boolean isSms, final MessageData message, 307 final Uri updatedMessageUri, final boolean clearSeen) { 308 final Context context = Factory.get().getApplicationContext(); 309 final DatabaseWrapper db = DataModel.get().getDatabase(); 310 311 // TODO: We're optimistically setting the type/box of outgoing messages to 312 // 'SENT' even before they actually are. We should technically be using QUEUED or OUTBOX 313 // instead, but if we do that, it's possible that the Messaging app will try to send them 314 // as part of its clean-up logic that runs when it starts (http://b/18155366). 315 // 316 // We also use the wrong status when inserting queued SMS messages in 317 // InsertNewMessageAction.insertBroadcastSmsMessage and insertSendingSmsMessage (should be 318 // QUEUED or OUTBOX), and in MmsUtils.insertSendReq (should be OUTBOX). 319 320 boolean updatedTelephony = true; 321 int messageBox; 322 int type; 323 switch(message.getStatus()) { 324 case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE: 325 case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED: 326 type = Sms.MESSAGE_TYPE_SENT; 327 messageBox = Mms.MESSAGE_BOX_SENT; 328 break; 329 case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND: 330 case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY: 331 type = Sms.MESSAGE_TYPE_SENT; 332 messageBox = Mms.MESSAGE_BOX_SENT; 333 break; 334 case MessageData.BUGLE_STATUS_OUTGOING_SENDING: 335 case MessageData.BUGLE_STATUS_OUTGOING_RESENDING: 336 type = Sms.MESSAGE_TYPE_SENT; 337 messageBox = Mms.MESSAGE_BOX_SENT; 338 break; 339 case MessageData.BUGLE_STATUS_OUTGOING_FAILED: 340 case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER: 341 type = Sms.MESSAGE_TYPE_FAILED; 342 messageBox = Mms.MESSAGE_BOX_FAILED; 343 break; 344 default: 345 type = Sms.MESSAGE_TYPE_ALL; 346 messageBox = Mms.MESSAGE_BOX_ALL; 347 break; 348 } 349 // First in the telephony DB 350 if (isSms) { 351 // Ignore update message Uri 352 if (type != Sms.MESSAGE_TYPE_ALL) { 353 if (!MmsUtils.updateSmsMessageSendingStatus(context, message.getSmsMessageUri(), 354 type, message.getReceivedTimeStamp())) { 355 message.markMessageFailed(message.getSentTimeStamp()); 356 updatedTelephony = false; 357 } 358 } 359 } else if (message.getSmsMessageUri() != null) { 360 if (messageBox != Mms.MESSAGE_BOX_ALL) { 361 if (!MmsUtils.updateMmsMessageSendingStatus(context, message.getSmsMessageUri(), 362 messageBox, message.getReceivedTimeStamp())) { 363 message.markMessageFailed(message.getSentTimeStamp()); 364 updatedTelephony = false; 365 } 366 } 367 } 368 if (updatedTelephony) { 369 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 370 LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS") 371 + " message " + message.getMessageId() 372 + " in telephony (" + message.getSmsMessageUri() + ")"); 373 } 374 } else { 375 LogUtil.w(TAG, "SendMessageAction: Failed to update " + (isSms ? "SMS" : "MMS") 376 + " message " + message.getMessageId() 377 + " in telephony (" + message.getSmsMessageUri() + "); marking message failed"); 378 } 379 380 // Update the local DB 381 db.beginTransaction(); 382 try { 383 if (updatedMessageUri != null) { 384 // Update all message and part fields 385 BugleDatabaseOperations.updateMessageInTransaction(db, message); 386 BugleDatabaseOperations.refreshConversationMetadataInTransaction( 387 db, message.getConversationId(), false/* shouldAutoSwitchSelfId */, 388 false/*archived*/); 389 } else { 390 final ContentValues values = new ContentValues(); 391 values.put(MessageColumns.STATUS, message.getStatus()); 392 393 if (clearSeen) { 394 // When a message fails to send, the message needs to 395 // be unseen to be selected as an error notification. 396 values.put(MessageColumns.SEEN, 0); 397 } 398 values.put(MessageColumns.RECEIVED_TIMESTAMP, message.getReceivedTimeStamp()); 399 values.put(MessageColumns.RAW_TELEPHONY_STATUS, message.getRawTelephonyStatus()); 400 401 BugleDatabaseOperations.updateMessageRowIfExists(db, message.getMessageId(), 402 values); 403 } 404 db.setTransactionSuccessful(); 405 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 406 LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS") 407 + " message " + message.getMessageId() + " in local db. Timestamp = " 408 + message.getReceivedTimeStamp()); 409 } 410 } finally { 411 db.endTransaction(); 412 } 413 414 MessagingContentProvider.notifyMessagesChanged(message.getConversationId()); 415 if (updatedMessageUri != null) { 416 MessagingContentProvider.notifyPartsChanged(); 417 } 418 419 return updatedTelephony; 420 } 421 SendMessageAction(final Parcel in)422 private SendMessageAction(final Parcel in) { 423 super(in); 424 } 425 426 public static final Parcelable.Creator<SendMessageAction> CREATOR 427 = new Parcelable.Creator<SendMessageAction>() { 428 @Override 429 public SendMessageAction createFromParcel(final Parcel in) { 430 return new SendMessageAction(in); 431 } 432 433 @Override 434 public SendMessageAction[] newArray(final int size) { 435 return new SendMessageAction[size]; 436 } 437 }; 438 439 @Override writeToParcel(final Parcel parcel, final int flags)440 public void writeToParcel(final Parcel parcel, final int flags) { 441 writeActionToParcel(parcel, flags); 442 } 443 } 444