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