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