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 return; 211 } 212 final String conversationId = message.getConversationId(); 213 if (updatedMessageUri != null) { 214 // Update message if we have newly written final message in the telephony db 215 final MessageData update = MmsUtils.readSendingMmsMessage(updatedMessageUri, 216 conversationId, message.getParticipantId(), message.getSelfId()); 217 if (update != null) { 218 // Set message Id of final message to that of the existing place holder. 219 update.updateMessageId(message.getMessageId()); 220 // Update image sizes. 221 update.updateSizesForImageParts(); 222 // Temp attachments are no longer needed 223 for (final MessagePartData part : message.getParts()) { 224 part.destroySync(); 225 } 226 message = update; 227 // processResult will rewrite the complete message as part of update 228 } else { 229 updatedMessageUri = null; 230 status = MmsUtils.MMS_REQUEST_MANUAL_RETRY; 231 LogUtil.e(TAG, "ProcessSentMessageAction: Unable to read sending message"); 232 } 233 } 234 235 final long timestamp = System.currentTimeMillis(); 236 boolean failed; 237 if (status == MmsUtils.MMS_REQUEST_SUCCEEDED) { 238 message.markMessageSent(timestamp); 239 failed = false; 240 } else if (status == MmsUtils.MMS_REQUEST_AUTO_RETRY 241 && message.getInResendWindow(timestamp)) { 242 message.markMessageNotSent(timestamp); 243 message.setRawTelephonyStatus(rawStatus); 244 failed = false; 245 } else { 246 message.markMessageFailed(timestamp); 247 message.setRawTelephonyStatus(rawStatus); 248 message.setMessageSeen(false); 249 failed = true; 250 } 251 252 // We have special handling for when a message to an emergency number fails. In this case, 253 // we notify immediately of any failure (even if we auto-retry), and instruct the user to 254 // try calling the emergency number instead. 255 if (status != MmsUtils.MMS_REQUEST_SUCCEEDED) { 256 final ArrayList<String> recipients = 257 BugleDatabaseOperations.getRecipientsForConversation(db, conversationId); 258 for (final String recipient : recipients) { 259 if (PhoneNumberUtils.isEmergencyNumber(recipient)) { 260 BugleNotifications.notifyEmergencySmsFailed(recipient, conversationId); 261 message.markMessageFailedEmergencyNumber(timestamp); 262 failed = true; 263 break; 264 } 265 } 266 } 267 268 // Update the message status and optionally refresh the message with final parts/values. 269 if (SendMessageAction.updateMessageAndStatus(isSms, message, updatedMessageUri, failed)) { 270 // We shouldn't show any notifications if we're not allowed to modify Telephony for 271 // this message. 272 if (failed) { 273 BugleNotifications.update(false, BugleNotifications.UPDATE_ERRORS); 274 } 275 BugleActionToasts.onSendMessageOrManualDownloadActionCompleted( 276 conversationId, !failed, status, isSms, subId, true/*isSend*/); 277 } 278 279 LogUtil.i(TAG, "ProcessSentMessageAction: Done sending " + (isSms ? "SMS" : "MMS") 280 + " message " + message.getMessageId() 281 + " in conversation " + conversationId 282 + "; status is " + MmsUtils.getRequestStatusDescription(status)); 283 284 // Whether we succeeded or failed we will check and maybe schedule some more work 285 ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction( 286 status != MmsUtils.MMS_REQUEST_SUCCEEDED, processingAction); 287 } 288 ProcessSentMessageAction(final Parcel in)289 private ProcessSentMessageAction(final Parcel in) { 290 super(in); 291 } 292 293 public static final Parcelable.Creator<ProcessSentMessageAction> CREATOR 294 = new Parcelable.Creator<ProcessSentMessageAction>() { 295 @Override 296 public ProcessSentMessageAction createFromParcel(final Parcel in) { 297 return new ProcessSentMessageAction(in); 298 } 299 300 @Override 301 public ProcessSentMessageAction[] newArray(final int size) { 302 return new ProcessSentMessageAction[size]; 303 } 304 }; 305 306 @Override writeToParcel(final Parcel parcel, final int flags)307 public void writeToParcel(final Parcel parcel, final int flags) { 308 writeActionToParcel(parcel, flags); 309 } 310 } 311