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