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 26 import com.android.messaging.Factory; 27 import com.android.messaging.datamodel.BugleDatabaseOperations; 28 import com.android.messaging.datamodel.DataModel; 29 import com.android.messaging.datamodel.DatabaseHelper.MessageColumns; 30 import com.android.messaging.datamodel.DatabaseWrapper; 31 import com.android.messaging.datamodel.MessagingContentProvider; 32 import com.android.messaging.datamodel.SyncManager; 33 import com.android.messaging.datamodel.data.MessageData; 34 import com.android.messaging.datamodel.data.ParticipantData; 35 import com.android.messaging.sms.MmsUtils; 36 import com.android.messaging.util.Assert; 37 import com.android.messaging.util.Assert.RunsOnMainThread; 38 import com.android.messaging.util.LogUtil; 39 40 /** 41 * Downloads an MMS message. 42 * <p> 43 * This class is public (not package-private) because the SMS/MMS (e.g. MmsUtils) classes need to 44 * access the EXTRA_* fields for setting up the 'downloaded' pending intent. 45 */ 46 public class DownloadMmsAction extends Action implements Parcelable { 47 private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG; 48 49 /** 50 * Interface for DownloadMmsAction listeners 51 */ 52 public interface DownloadMmsActionListener { 53 @RunsOnMainThread onDownloadMessageStarting(final ActionMonitor monitor, final Object data, final MessageData message)54 abstract void onDownloadMessageStarting(final ActionMonitor monitor, 55 final Object data, final MessageData message); 56 @RunsOnMainThread onDownloadMessageSucceeded(final ActionMonitor monitor, final Object data, final MessageData message)57 abstract void onDownloadMessageSucceeded(final ActionMonitor monitor, 58 final Object data, final MessageData message); 59 @RunsOnMainThread onDownloadMessageFailed(final ActionMonitor monitor, final Object data, final MessageData message)60 abstract void onDownloadMessageFailed(final ActionMonitor monitor, 61 final Object data, final MessageData message); 62 } 63 64 /** 65 * Queue download of an mms notification message (can only be called during execute of action) 66 */ queueMmsForDownloadInBackground(final String messageId, final Action processingAction)67 static boolean queueMmsForDownloadInBackground(final String messageId, 68 final Action processingAction) { 69 // When this method is being called, it is always from auto download 70 final DownloadMmsAction action = new DownloadMmsAction(); 71 // This could queue nothing 72 return action.queueAction(messageId, processingAction); 73 } 74 75 private static final String KEY_MESSAGE_ID = "message_id"; 76 private static final String KEY_CONVERSATION_ID = "conversation_id"; 77 private static final String KEY_PARTICIPANT_ID = "participant_id"; 78 private static final String KEY_CONTENT_LOCATION = "content_location"; 79 private static final String KEY_TRANSACTION_ID = "transaction_id"; 80 private static final String KEY_NOTIFICATION_URI = "notification_uri"; 81 private static final String KEY_SUB_ID = "sub_id"; 82 private static final String KEY_SUB_PHONE_NUMBER = "sub_phone_number"; 83 private static final String KEY_AUTO_DOWNLOAD = "auto_download"; 84 private static final String KEY_FAILURE_STATUS = "failure_status"; 85 86 // Values we attach to the pending intent that's fired when the message is downloaded. 87 // Only applicable when downloading via the platform APIs on L+. 88 public static final String EXTRA_MESSAGE_ID = "message_id"; 89 public static final String EXTRA_CONTENT_URI = "content_uri"; 90 public static final String EXTRA_NOTIFICATION_URI = "notification_uri"; 91 public static final String EXTRA_SUB_ID = "sub_id"; 92 public static final String EXTRA_SUB_PHONE_NUMBER = "sub_phone_number"; 93 public static final String EXTRA_TRANSACTION_ID = "transaction_id"; 94 public static final String EXTRA_CONTENT_LOCATION = "content_location"; 95 public static final String EXTRA_AUTO_DOWNLOAD = "auto_download"; 96 public static final String EXTRA_RECEIVED_TIMESTAMP = "received_timestamp"; 97 public static final String EXTRA_CONVERSATION_ID = "conversation_id"; 98 public static final String EXTRA_PARTICIPANT_ID = "participant_id"; 99 public static final String EXTRA_STATUS_IF_FAILED = "status_if_failed"; 100 DownloadMmsAction()101 private DownloadMmsAction() { 102 super(); 103 } 104 105 @Override executeAction()106 protected Object executeAction() { 107 Assert.fail("DownloadMmsAction must be queued rather than started"); 108 return null; 109 } 110 queueAction(final String messageId, final Action processingAction)111 protected boolean queueAction(final String messageId, final Action processingAction) { 112 actionParameters.putString(KEY_MESSAGE_ID, messageId); 113 114 final DatabaseWrapper db = DataModel.get().getDatabase(); 115 // Read the message from local db 116 final MessageData message = BugleDatabaseOperations.readMessage(db, messageId); 117 if (message != null && message.canDownloadMessage()) { 118 final Uri notificationUri = message.getSmsMessageUri(); 119 final String conversationId = message.getConversationId(); 120 final int status = message.getStatus(); 121 122 final String selfId = message.getSelfId(); 123 final ParticipantData self = BugleDatabaseOperations 124 .getExistingParticipant(db, selfId); 125 final int subId = self.getSubId(); 126 actionParameters.putInt(KEY_SUB_ID, subId); 127 actionParameters.putString(KEY_CONVERSATION_ID, conversationId); 128 actionParameters.putString(KEY_PARTICIPANT_ID, message.getParticipantId()); 129 actionParameters.putString(KEY_CONTENT_LOCATION, message.getMmsContentLocation()); 130 actionParameters.putString(KEY_TRANSACTION_ID, message.getMmsTransactionId()); 131 actionParameters.putParcelable(KEY_NOTIFICATION_URI, notificationUri); 132 actionParameters.putBoolean(KEY_AUTO_DOWNLOAD, isAutoDownload(status)); 133 134 final long now = System.currentTimeMillis(); 135 if (message.getInDownloadWindow(now)) { 136 // We can still retry 137 actionParameters.putString(KEY_SUB_PHONE_NUMBER, self.getNormalizedDestination()); 138 139 final int downloadingStatus = getDownloadingStatus(status); 140 // Update message status to indicate downloading. 141 updateMessageStatus(notificationUri, messageId, conversationId, 142 downloadingStatus, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED); 143 // Pre-compute the next status when failed so we don't have to load from db again 144 actionParameters.putInt(KEY_FAILURE_STATUS, getFailureStatus(downloadingStatus)); 145 146 // Actual download happens in background 147 processingAction.requestBackgroundWork(this); 148 149 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { 150 LogUtil.d(TAG, 151 "DownloadMmsAction: Queued download of MMS message " + messageId); 152 } 153 return true; 154 } else { 155 LogUtil.w(TAG, "DownloadMmsAction: Download of MMS message " + messageId 156 + " failed (outside download window)"); 157 158 // Retries depleted and we failed. Update the message status so we won't retry again 159 updateMessageStatus(notificationUri, messageId, conversationId, 160 MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED, 161 MessageData.RAW_TELEPHONY_STATUS_UNDEFINED); 162 if (status == MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD) { 163 // For auto download failure, we should send a DEFERRED NotifyRespInd 164 // to carrier to indicate we will manual download later 165 ProcessDownloadedMmsAction.sendDeferredRespStatus( 166 messageId, message.getMmsTransactionId(), 167 message.getMmsContentLocation(), subId); 168 return true; 169 } 170 } 171 } 172 return false; 173 } 174 175 /** 176 * Find out the auto download state of this message based on its starting status 177 * 178 * @param status The starting status of the message. 179 * @return True if this is a message doing auto downloading, false otherwise 180 */ isAutoDownload(final int status)181 private static boolean isAutoDownload(final int status) { 182 switch (status) { 183 case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD: 184 return false; 185 case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD: 186 return true; 187 default: 188 Assert.fail("isAutoDownload: invalid input status " + status); 189 return false; 190 } 191 } 192 193 /** 194 * Get the corresponding downloading status based on the starting status of the message 195 * 196 * @param status The starting status of the message. 197 * @return The downloading status 198 */ getDownloadingStatus(final int status)199 private static int getDownloadingStatus(final int status) { 200 switch (status) { 201 case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD: 202 return MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING; 203 case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD: 204 return MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING; 205 default: 206 Assert.fail("isAutoDownload: invalid input status " + status); 207 return MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING; 208 } 209 } 210 211 /** 212 * Get the corresponding failed status based on the current downloading status 213 * 214 * @param status The downloading status 215 * @return The status the message should have if downloading failed 216 */ getFailureStatus(final int status)217 private static int getFailureStatus(final int status) { 218 switch (status) { 219 case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING: 220 return MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD; 221 case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING: 222 return MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD; 223 default: 224 Assert.fail("isAutoDownload: invalid input status " + status); 225 return MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD; 226 } 227 } 228 229 @Override doBackgroundWork()230 protected Bundle doBackgroundWork() { 231 final Context context = Factory.get().getApplicationContext(); 232 final int subId = actionParameters.getInt(KEY_SUB_ID); 233 final String messageId = actionParameters.getString(KEY_MESSAGE_ID); 234 final Uri notificationUri = actionParameters.getParcelable(KEY_NOTIFICATION_URI); 235 final String subPhoneNumber = actionParameters.getString(KEY_SUB_PHONE_NUMBER); 236 final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID); 237 final String contentLocation = actionParameters.getString(KEY_CONTENT_LOCATION); 238 final boolean autoDownload = actionParameters.getBoolean(KEY_AUTO_DOWNLOAD); 239 final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID); 240 final String participantId = actionParameters.getString(KEY_PARTICIPANT_ID); 241 final int statusIfFailed = actionParameters.getInt(KEY_FAILURE_STATUS); 242 243 final long receivedTimestampRoundedToSecond = 244 1000 * ((System.currentTimeMillis() + 500) / 1000); 245 246 LogUtil.i(TAG, "DownloadMmsAction: Downloading MMS message " + messageId 247 + " (" + (autoDownload ? "auto" : "manual") + ")"); 248 249 // Bundle some values we'll need after the message is downloaded (via platform APIs) 250 final Bundle extras = new Bundle(); 251 extras.putString(EXTRA_MESSAGE_ID, messageId); 252 extras.putString(EXTRA_CONVERSATION_ID, conversationId); 253 extras.putString(EXTRA_PARTICIPANT_ID, participantId); 254 extras.putInt(EXTRA_STATUS_IF_FAILED, statusIfFailed); 255 256 // Start the download 257 final MmsUtils.StatusPlusUri status = MmsUtils.downloadMmsMessage(context, 258 notificationUri, subId, subPhoneNumber, transactionId, contentLocation, 259 autoDownload, receivedTimestampRoundedToSecond / 1000L, extras); 260 if (status == MmsUtils.STATUS_PENDING) { 261 // Async download; no status yet 262 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { 263 LogUtil.d(TAG, "DownloadMmsAction: Downloading MMS message " + messageId 264 + " asynchronously; waiting for pending intent to signal completion"); 265 } 266 } else { 267 // Inform sync that message has been added at local received timestamp 268 final SyncManager syncManager = DataModel.get().getSyncManager(); 269 syncManager.onNewMessageInserted(receivedTimestampRoundedToSecond); 270 // Handle downloaded message 271 ProcessDownloadedMmsAction.processMessageDownloadFastFailed(messageId, 272 notificationUri, conversationId, participantId, contentLocation, subId, 273 subPhoneNumber, statusIfFailed, autoDownload, transactionId, 274 status.resultCode); 275 } 276 return null; 277 } 278 279 @Override processBackgroundResponse(final Bundle response)280 protected Object processBackgroundResponse(final Bundle response) { 281 // Nothing to do here; post-download actions handled by ProcessDownloadedMmsAction 282 return null; 283 } 284 285 @Override processBackgroundFailure()286 protected Object processBackgroundFailure() { 287 final String messageId = actionParameters.getString(KEY_MESSAGE_ID); 288 final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID); 289 final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID); 290 final String participantId = actionParameters.getString(KEY_PARTICIPANT_ID); 291 final int statusIfFailed = actionParameters.getInt(KEY_FAILURE_STATUS); 292 final int subId = actionParameters.getInt(KEY_SUB_ID); 293 294 ProcessDownloadedMmsAction.processDownloadActionFailure(messageId, 295 MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED, 296 conversationId, participantId, statusIfFailed, subId, transactionId); 297 298 return null; 299 } 300 updateMessageStatus(final Uri messageUri, final String messageId, final String conversationId, final int status, final int rawStatus)301 static void updateMessageStatus(final Uri messageUri, final String messageId, 302 final String conversationId, final int status, final int rawStatus) { 303 final Context context = Factory.get().getApplicationContext(); 304 // Downloading status just kept in local DB but need to fix up telephony DB first 305 if (status == MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING || 306 status == MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING) { 307 MmsUtils.clearMmsStatus(context, messageUri); 308 } 309 // Then mark downloading status in our local DB 310 final ContentValues values = new ContentValues(); 311 values.put(MessageColumns.STATUS, status); 312 values.put(MessageColumns.RAW_TELEPHONY_STATUS, rawStatus); 313 final DatabaseWrapper db = DataModel.get().getDatabase(); 314 BugleDatabaseOperations.updateMessageRowIfExists(db, messageId, values); 315 316 MessagingContentProvider.notifyMessagesChanged(conversationId); 317 } 318 DownloadMmsAction(final Parcel in)319 private DownloadMmsAction(final Parcel in) { 320 super(in); 321 } 322 323 public static final Parcelable.Creator<DownloadMmsAction> CREATOR 324 = new Parcelable.Creator<DownloadMmsAction>() { 325 @Override 326 public DownloadMmsAction createFromParcel(final Parcel in) { 327 return new DownloadMmsAction(in); 328 } 329 330 @Override 331 public DownloadMmsAction[] newArray(final int size) { 332 return new DownloadMmsAction[size]; 333 } 334 }; 335 336 @Override writeToParcel(final Parcel parcel, final int flags)337 public void writeToParcel(final Parcel parcel, final int flags) { 338 writeActionToParcel(parcel, flags); 339 } 340 } 341