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