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 DatabaseWrapper db = DataModel.get().getDatabase(); 99 100 final MessageData message = BugleDatabaseOperations.readMessage(db, messageId); 101 // Check message can be resent 102 if (message != null && message.canSendMessage()) { 103 final boolean isSms = message.getIsSms(); 104 long timestamp = System.currentTimeMillis(); 105 if (!isSms) { 106 // MMS expects timestamp rounded to nearest second 107 timestamp = 1000 * ((timestamp + 500) / 1000); 108 } 109 110 final ParticipantData self = BugleDatabaseOperations.getExistingParticipant( 111 db, message.getSelfId()); 112 final Uri messageUri = message.getSmsMessageUri(); 113 final String conversationId = message.getConversationId(); 114 115 // Update message status 116 if (message.getYetToSend()) { 117 if (message.getReceivedTimeStamp() == message.getRetryStartTimestamp()) { 118 // Initial sending of message 119 message.markMessageSending(timestamp); 120 } else { 121 // Manual resend of message 122 message.markMessageManualResend(timestamp); 123 } 124 } else { 125 // Automatic resend of message 126 message.markMessageResending(timestamp); 127 } 128 if (!updateMessageAndStatus(isSms, message, null /* messageUri */, false /*notify*/)) { 129 // If message is missing in the telephony database we don't need to send it 130 return false; 131 } 132 133 final ArrayList<String> recipients = 134 BugleDatabaseOperations.getRecipientsForConversation(db, conversationId); 135 136 // Update action state with parameters needed for background sending 137 actionParameters.putParcelable(KEY_MESSAGE_URI, messageUri); 138 actionParameters.putParcelable(KEY_MESSAGE, message); 139 actionParameters.putStringArrayList(KEY_RECIPIENTS, recipients); 140 actionParameters.putInt(KEY_SUB_ID, self.getSubId()); 141 actionParameters.putString(KEY_SUB_PHONE_NUMBER, self.getNormalizedDestination()); 142 143 if (isSms) { 144 final String smsc = BugleDatabaseOperations.getSmsServiceCenterForConversation( 145 db, conversationId); 146 actionParameters.putString(KEY_SMS_SERVICE_CENTER, smsc); 147 148 if (recipients.size() == 1) { 149 final String recipient = recipients.get(0); 150 151 actionParameters.putString(KEY_RECIPIENT, recipient); 152 // Queue actual sending for SMS 153 processingAction.requestBackgroundWork(this); 154 155 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { 156 LogUtil.d(TAG, "SendMessageAction: Queued SMS message " + messageId 157 + " for sending"); 158 } 159 return true; 160 } else { 161 LogUtil.wtf(TAG, "Trying to resend a broadcast SMS - not allowed"); 162 } 163 } else { 164 // Queue actual sending for MMS 165 processingAction.requestBackgroundWork(this); 166 167 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { 168 LogUtil.d(TAG, "SendMessageAction: Queued MMS message " + messageId 169 + " for sending"); 170 } 171 return true; 172 } 173 } 174 175 return false; 176 } 177 178 179 /** 180 * Never called 181 */ 182 @Override executeAction()183 protected Object executeAction() { 184 Assert.fail("SendMessageAction must be queued rather than started"); 185 return null; 186 } 187 188 /** 189 * Send message on background worker thread 190 */ 191 @Override doBackgroundWork()192 protected Bundle doBackgroundWork() { 193 final MessageData message = actionParameters.getParcelable(KEY_MESSAGE); 194 final String messageId = actionParameters.getString(KEY_MESSAGE_ID); 195 Uri messageUri = actionParameters.getParcelable(KEY_MESSAGE_URI); 196 Uri updatedMessageUri = null; 197 final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS; 198 final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID); 199 final String subPhoneNumber = actionParameters.getString(KEY_SUB_PHONE_NUMBER); 200 201 LogUtil.i(TAG, "SendMessageAction: Sending " + (isSms ? "SMS" : "MMS") + " message " 202 + messageId + " in conversation " + message.getConversationId()); 203 204 int status; 205 int rawStatus = MessageData.RAW_TELEPHONY_STATUS_UNDEFINED; 206 int resultCode = MessageData.UNKNOWN_RESULT_CODE; 207 if (isSms) { 208 Assert.notNull(messageUri); 209 final String recipient = actionParameters.getString(KEY_RECIPIENT); 210 final String messageText = message.getMessageText(); 211 final String smsServiceCenter = actionParameters.getString(KEY_SMS_SERVICE_CENTER); 212 final boolean deliveryReportRequired = MmsUtils.isDeliveryReportRequired(subId); 213 214 status = MmsUtils.sendSmsMessage(recipient, messageText, messageUri, subId, 215 smsServiceCenter, deliveryReportRequired); 216 } else { 217 final Context context = Factory.get().getApplicationContext(); 218 final ArrayList<String> recipients = 219 actionParameters.getStringArrayList(KEY_RECIPIENTS); 220 if (messageUri == null) { 221 final long timestamp = message.getReceivedTimeStamp(); 222 223 // Inform sync that message has been added at local received timestamp 224 final SyncManager syncManager = DataModel.get().getSyncManager(); 225 syncManager.onNewMessageInserted(timestamp); 226 227 // For MMS messages first need to write to telephony (resizing images if needed) 228 updatedMessageUri = MmsUtils.insertSendingMmsMessage(context, recipients, 229 message, subId, subPhoneNumber, timestamp); 230 if (updatedMessageUri != null) { 231 messageUri = updatedMessageUri; 232 // To prevent Sync seeing inconsistent state must write to DB on this thread 233 updateMessageUri(messageId, updatedMessageUri); 234 235 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 236 LogUtil.v(TAG, "SendMessageAction: Updated message " + messageId 237 + " with new uri " + messageUri); 238 } 239 } 240 } 241 if (messageUri != null) { 242 // Actually send the MMS 243 final Bundle extras = new Bundle(); 244 extras.putString(EXTRA_MESSAGE_ID, messageId); 245 extras.putParcelable(EXTRA_UPDATED_MESSAGE_URI, updatedMessageUri); 246 final MmsUtils.StatusPlusUri result = MmsUtils.sendMmsMessage(context, subId, 247 messageUri, extras); 248 if (result == MmsUtils.STATUS_PENDING) { 249 // Async send, so no status yet 250 LogUtil.d(TAG, "SendMessageAction: Sending MMS message " + messageId 251 + " asynchronously; waiting for callback to finish processing"); 252 return null; 253 } 254 status = result.status; 255 rawStatus = result.rawStatus; 256 resultCode = result.resultCode; 257 } else { 258 status = MmsUtils.MMS_REQUEST_MANUAL_RETRY; 259 } 260 } 261 262 // When we fast-fail before calling the MMS lib APIs (e.g. airplane mode, 263 // sending message is deleted). 264 ProcessSentMessageAction.processMessageSentFastFailed(messageId, messageUri, 265 updatedMessageUri, subId, isSms, status, rawStatus, resultCode); 266 return null; 267 } 268 updateMessageUri(final String messageId, final Uri updatedMessageUri)269 private void updateMessageUri(final String messageId, final Uri updatedMessageUri) { 270 final DatabaseWrapper db = DataModel.get().getDatabase(); 271 db.beginTransaction(); 272 try { 273 final ContentValues values = new ContentValues(); 274 values.put(MessageColumns.SMS_MESSAGE_URI, updatedMessageUri.toString()); 275 BugleDatabaseOperations.updateMessageRow(db, messageId, values); 276 db.setTransactionSuccessful(); 277 } finally { 278 db.endTransaction(); 279 } 280 } 281 282 @Override processBackgroundResponse(final Bundle response)283 protected Object processBackgroundResponse(final Bundle response) { 284 // Nothing to do here, post-send tasks handled by ProcessSentMessageAction 285 return null; 286 } 287 288 /** 289 * Update message status to reflect success or failure 290 */ 291 @Override processBackgroundFailure()292 protected Object processBackgroundFailure() { 293 final String messageId = actionParameters.getString(KEY_MESSAGE_ID); 294 final MessageData message = actionParameters.getParcelable(KEY_MESSAGE); 295 final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS; 296 final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID); 297 final int resultCode = actionParameters.getInt(ProcessSentMessageAction.KEY_RESULT_CODE); 298 final int httpStatusCode = 299 actionParameters.getInt(ProcessSentMessageAction.KEY_HTTP_STATUS_CODE); 300 301 ProcessSentMessageAction.processResult(messageId, null /* updatedMessageUri */, 302 MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED, 303 isSms, this, subId, resultCode, httpStatusCode); 304 305 return null; 306 } 307 308 /** 309 * Update the message status (and message itself if necessary) 310 * @param isSms whether this is an SMS or MMS 311 * @param message message to update 312 * @param updatedMessageUri message uri for newly-inserted messages; null otherwise 313 * @param clearSeen whether the message 'seen' status should be reset if error occurs 314 */ updateMessageAndStatus(final boolean isSms, final MessageData message, final Uri updatedMessageUri, final boolean clearSeen)315 public static boolean updateMessageAndStatus(final boolean isSms, final MessageData message, 316 final Uri updatedMessageUri, final boolean clearSeen) { 317 final Context context = Factory.get().getApplicationContext(); 318 final DatabaseWrapper db = DataModel.get().getDatabase(); 319 320 // TODO: We're optimistically setting the type/box of outgoing messages to 321 // 'SENT' even before they actually are. We should technically be using QUEUED or OUTBOX 322 // instead, but if we do that, it's possible that the Messaging app will try to send them 323 // as part of its clean-up logic that runs when it starts (http://b/18155366). 324 // 325 // We also use the wrong status when inserting queued SMS messages in 326 // InsertNewMessageAction.insertBroadcastSmsMessage and insertSendingSmsMessage (should be 327 // QUEUED or OUTBOX), and in MmsUtils.insertSendReq (should be OUTBOX). 328 329 boolean updatedTelephony = true; 330 int messageBox; 331 int type; 332 switch(message.getStatus()) { 333 case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE: 334 case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED: 335 type = Sms.MESSAGE_TYPE_SENT; 336 messageBox = Mms.MESSAGE_BOX_SENT; 337 break; 338 case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND: 339 case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY: 340 type = Sms.MESSAGE_TYPE_SENT; 341 messageBox = Mms.MESSAGE_BOX_SENT; 342 break; 343 case MessageData.BUGLE_STATUS_OUTGOING_SENDING: 344 case MessageData.BUGLE_STATUS_OUTGOING_RESENDING: 345 type = Sms.MESSAGE_TYPE_SENT; 346 messageBox = Mms.MESSAGE_BOX_SENT; 347 break; 348 case MessageData.BUGLE_STATUS_OUTGOING_FAILED: 349 case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER: 350 type = Sms.MESSAGE_TYPE_FAILED; 351 messageBox = Mms.MESSAGE_BOX_FAILED; 352 break; 353 default: 354 type = Sms.MESSAGE_TYPE_ALL; 355 messageBox = Mms.MESSAGE_BOX_ALL; 356 break; 357 } 358 // First in the telephony DB 359 if (isSms) { 360 // Ignore update message Uri 361 if (type != Sms.MESSAGE_TYPE_ALL) { 362 if (!MmsUtils.updateSmsMessageSendingStatus(context, message.getSmsMessageUri(), 363 type, message.getReceivedTimeStamp())) { 364 message.markMessageFailed(message.getSentTimeStamp()); 365 updatedTelephony = false; 366 } 367 } 368 } else if (message.getSmsMessageUri() != null) { 369 if (messageBox != Mms.MESSAGE_BOX_ALL) { 370 if (!MmsUtils.updateMmsMessageSendingStatus(context, message.getSmsMessageUri(), 371 messageBox, message.getReceivedTimeStamp())) { 372 message.markMessageFailed(message.getSentTimeStamp()); 373 updatedTelephony = false; 374 } 375 } 376 } 377 if (updatedTelephony) { 378 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 379 LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS") 380 + " message " + message.getMessageId() 381 + " in telephony (" + message.getSmsMessageUri() + ")"); 382 } 383 } else { 384 LogUtil.w(TAG, "SendMessageAction: Failed to update " + (isSms ? "SMS" : "MMS") 385 + " message " + message.getMessageId() 386 + " in telephony (" + message.getSmsMessageUri() + "); marking message failed"); 387 } 388 389 // Update the local DB 390 db.beginTransaction(); 391 try { 392 if (updatedMessageUri != null) { 393 // Update all message and part fields 394 BugleDatabaseOperations.updateMessageInTransaction(db, message); 395 BugleDatabaseOperations.refreshConversationMetadataInTransaction( 396 db, message.getConversationId(), false/* shouldAutoSwitchSelfId */, 397 false/*archived*/); 398 } else { 399 final ContentValues values = new ContentValues(); 400 values.put(MessageColumns.STATUS, message.getStatus()); 401 402 if (clearSeen) { 403 // When a message fails to send, the message needs to 404 // be unseen to be selected as an error notification. 405 values.put(MessageColumns.SEEN, 0); 406 } 407 values.put(MessageColumns.RECEIVED_TIMESTAMP, message.getReceivedTimeStamp()); 408 values.put(MessageColumns.RAW_TELEPHONY_STATUS, message.getRawTelephonyStatus()); 409 410 BugleDatabaseOperations.updateMessageRowIfExists(db, message.getMessageId(), 411 values); 412 } 413 db.setTransactionSuccessful(); 414 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 415 LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS") 416 + " message " + message.getMessageId() + " in local db. Timestamp = " 417 + message.getReceivedTimeStamp()); 418 } 419 } finally { 420 db.endTransaction(); 421 } 422 423 MessagingContentProvider.notifyMessagesChanged(message.getConversationId()); 424 if (updatedMessageUri != null) { 425 MessagingContentProvider.notifyPartsChanged(); 426 } 427 428 return updatedTelephony; 429 } 430 SendMessageAction(final Parcel in)431 private SendMessageAction(final Parcel in) { 432 super(in); 433 } 434 435 public static final Parcelable.Creator<SendMessageAction> CREATOR 436 = new Parcelable.Creator<SendMessageAction>() { 437 @Override 438 public SendMessageAction createFromParcel(final Parcel in) { 439 return new SendMessageAction(in); 440 } 441 442 @Override 443 public SendMessageAction[] newArray(final int size) { 444 return new SendMessageAction[size]; 445 } 446 }; 447 448 @Override writeToParcel(final Parcel parcel, final int flags)449 public void writeToParcel(final Parcel parcel, final int flags) { 450 writeActionToParcel(parcel, flags); 451 } 452 } 453