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.app.Activity; 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.telephony.PhoneNumberUtils; 26 import android.telephony.SmsManager; 27 28 import com.android.messaging.Factory; 29 import com.android.messaging.datamodel.BugleDatabaseOperations; 30 import com.android.messaging.datamodel.BugleNotifications; 31 import com.android.messaging.datamodel.DataModel; 32 import com.android.messaging.datamodel.DatabaseWrapper; 33 import com.android.messaging.datamodel.MmsFileProvider; 34 import com.android.messaging.datamodel.data.MessageData; 35 import com.android.messaging.datamodel.data.MessagePartData; 36 import com.android.messaging.datamodel.data.ParticipantData; 37 import com.android.messaging.mmslib.pdu.SendConf; 38 import com.android.messaging.sms.MmsConfig; 39 import com.android.messaging.sms.MmsSender; 40 import com.android.messaging.sms.MmsUtils; 41 import com.android.messaging.util.Assert; 42 import com.android.messaging.util.LogUtil; 43 44 import java.io.File; 45 import java.util.ArrayList; 46 47 /** 48 * Update message status to reflect success or failure 49 * Can also update the message itself if a "final" message is now available from telephony db 50 */ 51 public class ProcessSentMessageAction extends Action { 52 private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG; 53 54 // These are always set 55 private static final String KEY_SMS = "is_sms"; 56 private static final String KEY_SENT_BY_PLATFORM = "sent_by_platform"; 57 58 // These are set when we're processing a message sent by the user. They are null for messages 59 // sent automatically (e.g. a NotifyRespInd/AcknowledgeInd sent in response to a download). 60 private static final String KEY_MESSAGE_ID = "message_id"; 61 private static final String KEY_MESSAGE_URI = "message_uri"; 62 private static final String KEY_UPDATED_MESSAGE_URI = "updated_message_uri"; 63 private static final String KEY_SUB_ID = "sub_id"; 64 65 // These are set for messages sent by the platform (L+) 66 public static final String KEY_RESULT_CODE = "result_code"; 67 public static final String KEY_HTTP_STATUS_CODE = "http_status_code"; 68 private static final String KEY_CONTENT_URI = "content_uri"; 69 private static final String KEY_RESPONSE = "response"; 70 private static final String KEY_RESPONSE_IMPORTANT = "response_important"; 71 72 // These are set for messages we sent ourself (legacy), or which we fast-failed before sending. 73 private static final String KEY_STATUS = "status"; 74 private static final String KEY_RAW_STATUS = "raw_status"; 75 76 // This is called when MMS lib API returns via PendingIntent processMmsSent(final int resultCode, final Uri messageUri, final Bundle extras)77 public static void processMmsSent(final int resultCode, final Uri messageUri, 78 final Bundle extras) { 79 final ProcessSentMessageAction action = new ProcessSentMessageAction(); 80 final Bundle params = action.actionParameters; 81 params.putBoolean(KEY_SMS, false); 82 params.putBoolean(KEY_SENT_BY_PLATFORM, true); 83 params.putString(KEY_MESSAGE_ID, extras.getString(SendMessageAction.EXTRA_MESSAGE_ID)); 84 params.putParcelable(KEY_MESSAGE_URI, messageUri); 85 params.putParcelable(KEY_UPDATED_MESSAGE_URI, 86 extras.getParcelable(SendMessageAction.EXTRA_UPDATED_MESSAGE_URI)); 87 params.putInt(KEY_SUB_ID, 88 extras.getInt(SendMessageAction.KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID)); 89 params.putInt(KEY_RESULT_CODE, resultCode); 90 params.putInt(KEY_HTTP_STATUS_CODE, extras.getInt(SmsManager.EXTRA_MMS_HTTP_STATUS, 0)); 91 params.putParcelable(KEY_CONTENT_URI, 92 extras.getParcelable(SendMessageAction.EXTRA_CONTENT_URI)); 93 params.putByteArray(KEY_RESPONSE, extras.getByteArray(SmsManager.EXTRA_MMS_DATA)); 94 params.putBoolean(KEY_RESPONSE_IMPORTANT, 95 extras.getBoolean(SendMessageAction.EXTRA_RESPONSE_IMPORTANT)); 96 action.start(); 97 } 98 processMessageSentFastFailed(final String messageId, final Uri messageUri, final Uri updatedMessageUri, final int subId, final boolean isSms, final int status, final int rawStatus, final int resultCode)99 public static void processMessageSentFastFailed(final String messageId, 100 final Uri messageUri, final Uri updatedMessageUri, final int subId, final boolean isSms, 101 final int status, final int rawStatus, final int resultCode) { 102 final ProcessSentMessageAction action = new ProcessSentMessageAction(); 103 final Bundle params = action.actionParameters; 104 params.putBoolean(KEY_SMS, isSms); 105 params.putBoolean(KEY_SENT_BY_PLATFORM, false); 106 params.putString(KEY_MESSAGE_ID, messageId); 107 params.putParcelable(KEY_MESSAGE_URI, messageUri); 108 params.putParcelable(KEY_UPDATED_MESSAGE_URI, updatedMessageUri); 109 params.putInt(KEY_SUB_ID, subId); 110 params.putInt(KEY_STATUS, status); 111 params.putInt(KEY_RAW_STATUS, rawStatus); 112 params.putInt(KEY_RESULT_CODE, resultCode); 113 action.start(); 114 } 115 ProcessSentMessageAction()116 private ProcessSentMessageAction() { 117 // Callers must use one of the static methods above 118 } 119 120 /** 121 * Update message status to reflect success or failure 122 * Can also update the message itself if a "final" message is now available from telephony db 123 */ 124 @Override executeAction()125 protected Object executeAction() { 126 final Context context = Factory.get().getApplicationContext(); 127 final String messageId = actionParameters.getString(KEY_MESSAGE_ID); 128 final Uri messageUri = actionParameters.getParcelable(KEY_MESSAGE_URI); 129 final Uri updatedMessageUri = actionParameters.getParcelable(KEY_UPDATED_MESSAGE_URI); 130 final boolean isSms = actionParameters.getBoolean(KEY_SMS); 131 final boolean sentByPlatform = actionParameters.getBoolean(KEY_SENT_BY_PLATFORM); 132 133 int status = actionParameters.getInt(KEY_STATUS, MmsUtils.MMS_REQUEST_MANUAL_RETRY); 134 int rawStatus = actionParameters.getInt(KEY_RAW_STATUS, 135 MmsUtils.PDU_HEADER_VALUE_UNDEFINED); 136 final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID); 137 138 if (sentByPlatform) { 139 // Delete temporary file backing the contentUri passed to MMS service 140 final Uri contentUri = actionParameters.getParcelable(KEY_CONTENT_URI); 141 Assert.isTrue(contentUri != null); 142 final File tempFile = MmsFileProvider.getFile(contentUri); 143 long messageSize = 0; 144 if (tempFile.exists()) { 145 messageSize = tempFile.length(); 146 tempFile.delete(); 147 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 148 LogUtil.v(TAG, "ProcessSentMessageAction: Deleted temp file with outgoing " 149 + "MMS pdu: " + contentUri); 150 } 151 } 152 153 final int resultCode = actionParameters.getInt(KEY_RESULT_CODE); 154 final boolean responseImportant = actionParameters.getBoolean(KEY_RESPONSE_IMPORTANT); 155 if (resultCode == Activity.RESULT_OK) { 156 if (responseImportant) { 157 // Get the status from the response PDU and update telephony 158 final byte[] response = actionParameters.getByteArray(KEY_RESPONSE); 159 final SendConf sendConf = MmsSender.parseSendConf(response, subId); 160 if (sendConf != null) { 161 final MmsUtils.StatusPlusUri result = 162 MmsUtils.updateSentMmsMessageStatus(context, messageUri, sendConf); 163 status = result.status; 164 rawStatus = result.rawStatus; 165 } 166 } 167 } else { 168 String errorMsg = "ProcessSentMessageAction: Platform returned error resultCode: " 169 + resultCode; 170 final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE); 171 if (httpStatusCode != 0) { 172 errorMsg += (", HTTP status code: " + httpStatusCode); 173 } 174 LogUtil.w(TAG, errorMsg); 175 status = MmsSender.getErrorResultStatus(resultCode, httpStatusCode); 176 177 // Check for MMS messages that failed because they exceeded the maximum size, 178 // indicated by an I/O error from the platform. 179 if (resultCode == SmsManager.MMS_ERROR_IO_ERROR) { 180 if (messageSize > MmsConfig.get(subId).getMaxMessageSize()) { 181 rawStatus = MessageData.RAW_TELEPHONY_STATUS_MESSAGE_TOO_BIG; 182 } 183 } 184 } 185 } 186 if (messageId != null) { 187 final int resultCode = actionParameters.getInt(KEY_RESULT_CODE); 188 final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE); 189 processResult( 190 messageId, updatedMessageUri, status, rawStatus, isSms, this, subId, 191 resultCode, httpStatusCode); 192 } else { 193 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 194 LogUtil.v(TAG, "ProcessSentMessageAction: No sent message to process (it was " 195 + "probably a notify response for an MMS download)"); 196 } 197 } 198 return null; 199 } 200 processResult(final String messageId, Uri updatedMessageUri, int status, final int rawStatus, final boolean isSms, final Action processingAction, final int subId, final int resultCode, final int httpStatusCode)201 static void processResult(final String messageId, Uri updatedMessageUri, int status, 202 final int rawStatus, final boolean isSms, final Action processingAction, 203 final int subId, final int resultCode, final int httpStatusCode) { 204 final DatabaseWrapper db = DataModel.get().getDatabase(); 205 MessageData message = BugleDatabaseOperations.readMessage(db, messageId); 206 final MessageData originalMessage = message; 207 if (message == null) { 208 LogUtil.w(TAG, "ProcessSentMessageAction: Sent message " + messageId 209 + " missing from local database"); 210 ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction( 211 true /* failed */, processingAction); 212 return; 213 } 214 final String conversationId = message.getConversationId(); 215 if (updatedMessageUri != null) { 216 // Update message if we have newly written final message in the telephony db 217 final MessageData update = MmsUtils.readSendingMmsMessage(updatedMessageUri, 218 conversationId, message.getParticipantId(), message.getSelfId()); 219 if (update != null) { 220 // Set message Id of final message to that of the existing place holder. 221 update.updateMessageId(message.getMessageId()); 222 // Update image sizes. 223 update.updateSizesForImageParts(); 224 // Temp attachments are no longer needed 225 for (final MessagePartData part : message.getParts()) { 226 part.destroySync(); 227 } 228 message = update; 229 // processResult will rewrite the complete message as part of update 230 } else { 231 updatedMessageUri = null; 232 status = MmsUtils.MMS_REQUEST_MANUAL_RETRY; 233 LogUtil.e(TAG, "ProcessSentMessageAction: Unable to read sending message"); 234 } 235 } 236 237 final long timestamp = System.currentTimeMillis(); 238 boolean failed; 239 if (status == MmsUtils.MMS_REQUEST_SUCCEEDED) { 240 message.markMessageSent(timestamp); 241 failed = false; 242 } else if (status == MmsUtils.MMS_REQUEST_AUTO_RETRY 243 && message.getInResendWindow(timestamp)) { 244 message.markMessageNotSent(timestamp); 245 message.setRawTelephonyStatus(rawStatus); 246 failed = false; 247 } else { 248 message.markMessageFailed(timestamp); 249 message.setRawTelephonyStatus(rawStatus); 250 message.setMessageSeen(false); 251 failed = true; 252 } 253 254 // We have special handling for when a message to an emergency number fails. In this case, 255 // we notify immediately of any failure (even if we auto-retry), and instruct the user to 256 // try calling the emergency number instead. 257 if (status != MmsUtils.MMS_REQUEST_SUCCEEDED) { 258 final ArrayList<String> recipients = 259 BugleDatabaseOperations.getRecipientsForConversation(db, conversationId); 260 for (final String recipient : recipients) { 261 if (PhoneNumberUtils.isEmergencyNumber(recipient)) { 262 BugleNotifications.notifyEmergencySmsFailed(recipient, conversationId); 263 message.markMessageFailedEmergencyNumber(timestamp); 264 failed = true; 265 break; 266 } 267 } 268 } 269 270 // Update the message status and optionally refresh the message with final parts/values. 271 if (SendMessageAction.updateMessageAndStatus(isSms, message, updatedMessageUri, failed)) { 272 // We shouldn't show any notifications if we're not allowed to modify Telephony for 273 // this message. 274 if (failed) { 275 BugleNotifications.update(false, BugleNotifications.UPDATE_ERRORS); 276 } 277 BugleActionToasts.onSendMessageOrManualDownloadActionCompleted( 278 conversationId, !failed, status, isSms, subId, true/*isSend*/); 279 } 280 281 LogUtil.i(TAG, "ProcessSentMessageAction: Done sending " + (isSms ? "SMS" : "MMS") 282 + " message " + message.getMessageId() 283 + " in conversation " + conversationId 284 + "; status is " + MmsUtils.getRequestStatusDescription(status)); 285 286 // Whether we succeeded or failed we will check and maybe schedule some more work 287 ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction( 288 status != MmsUtils.MMS_REQUEST_SUCCEEDED, processingAction); 289 } 290 ProcessSentMessageAction(final Parcel in)291 private ProcessSentMessageAction(final Parcel in) { 292 super(in); 293 } 294 295 public static final Parcelable.Creator<ProcessSentMessageAction> CREATOR 296 = new Parcelable.Creator<ProcessSentMessageAction>() { 297 @Override 298 public ProcessSentMessageAction createFromParcel(final Parcel in) { 299 return new ProcessSentMessageAction(in); 300 } 301 302 @Override 303 public ProcessSentMessageAction[] newArray(final int size) { 304 return new ProcessSentMessageAction[size]; 305 } 306 }; 307 308 @Override writeToParcel(final Parcel parcel, final int flags)309 public void writeToParcel(final Parcel parcel, final int flags) { 310 writeActionToParcel(parcel, flags); 311 } 312 } 313