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 54 abstract void onDownloadMessageStarting(final ActionMonitor monitor, 55 final Object data, final MessageData message); 56 @RunsOnMainThread 57 abstract void onDownloadMessageSucceeded(final ActionMonitor monitor, 58 final Object data, final MessageData message); 59 @RunsOnMainThread 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 */ 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 101 private DownloadMmsAction() { 102 super(); 103 } 104 105 @Override 106 protected Object executeAction() { 107 Assert.fail("DownloadMmsAction must be queued rather than started"); 108 return null; 109 } 110 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 */ 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 */ 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 */ 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 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 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 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 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 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 337 public void writeToParcel(final Parcel parcel, final int flags) { 338 writeActionToParcel(parcel, flags); 339 } 340 } 341