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.app.Activity;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.net.Uri;
23 import android.os.Bundle;
24 import android.os.Parcel;
25 import android.os.Parcelable;
26 import android.provider.Telephony.Mms;
27 import android.telephony.SmsManager;
28 import android.text.TextUtils;
29 
30 import com.android.messaging.Factory;
31 import com.android.messaging.datamodel.BugleDatabaseOperations;
32 import com.android.messaging.datamodel.BugleNotifications;
33 import com.android.messaging.datamodel.DataModel;
34 import com.android.messaging.datamodel.DataModelException;
35 import com.android.messaging.datamodel.DatabaseWrapper;
36 import com.android.messaging.datamodel.MessagingContentProvider;
37 import com.android.messaging.datamodel.MmsFileProvider;
38 import com.android.messaging.datamodel.SyncManager;
39 import com.android.messaging.datamodel.data.MessageData;
40 import com.android.messaging.datamodel.data.ParticipantData;
41 import com.android.messaging.mmslib.SqliteWrapper;
42 import com.android.messaging.mmslib.pdu.PduHeaders;
43 import com.android.messaging.mmslib.pdu.RetrieveConf;
44 import com.android.messaging.sms.DatabaseMessages;
45 import com.android.messaging.sms.MmsSender;
46 import com.android.messaging.sms.MmsUtils;
47 import com.android.messaging.util.Assert;
48 import com.android.messaging.util.LogUtil;
49 import com.google.common.io.Files;
50 
51 import java.io.File;
52 import java.io.FileNotFoundException;
53 import java.io.IOException;
54 import java.util.List;
55 
56 /**
57  * Processes an MMS message after it has been downloaded.
58  * NOTE: This action must queue a ProcessPendingMessagesAction when it is done (success or failure).
59  */
60 public class ProcessDownloadedMmsAction extends Action {
61     private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
62 
63     // Always set when message downloaded
64     private static final String KEY_DOWNLOADED_BY_PLATFORM = "downloaded_by_platform";
65     private static final String KEY_MESSAGE_ID = "message_id";
66     private static final String KEY_NOTIFICATION_URI = "notification_uri";
67     private static final String KEY_CONVERSATION_ID = "conversation_id";
68     private static final String KEY_PARTICIPANT_ID = "participant_id";
69     private static final String KEY_STATUS_IF_FAILED = "status_if_failed";
70 
71     // Set when message downloaded by platform (L+)
72     private static final String KEY_RESULT_CODE = "result_code";
73     private static final String KEY_HTTP_STATUS_CODE = "http_status_code";
74     private static final String KEY_CONTENT_URI = "content_uri";
75     private static final String KEY_SUB_ID = "sub_id";
76     private static final String KEY_SUB_PHONE_NUMBER = "sub_phone_number";
77     private static final String KEY_TRANSACTION_ID = "transaction_id";
78     private static final String KEY_CONTENT_LOCATION = "content_location";
79     private static final String KEY_AUTO_DOWNLOAD = "auto_download";
80     private static final String KEY_RECEIVED_TIMESTAMP = "received_timestamp";
81     private static final String KEY_EXPIRY = "expiry";
82 
83     // Set when message downloaded by us (legacy)
84     private static final String KEY_STATUS = "status";
85     private static final String KEY_RAW_STATUS = "raw_status";
86     private static final String KEY_MMS_URI =  "mms_uri";
87 
88     // Used to send a deferred response in response to auto-download failure
89     private static final String KEY_SEND_DEFERRED_RESP_STATUS = "send_deferred_resp_status";
90 
91     // Results passed from background worker to processCompletion
92     private static final String BUNDLE_REQUEST_STATUS = "request_status";
93     private static final String BUNDLE_RAW_TELEPHONY_STATUS = "raw_status";
94     private static final String BUNDLE_MMS_URI = "mms_uri";
95 
96     // This is called when MMS lib API returns via PendingIntent
processMessageDownloaded(final int resultCode, final Bundle extras)97     public static void processMessageDownloaded(final int resultCode, final Bundle extras) {
98         final String messageId = extras.getString(DownloadMmsAction.EXTRA_MESSAGE_ID);
99         final Uri contentUri = extras.getParcelable(DownloadMmsAction.EXTRA_CONTENT_URI);
100         final Uri notificationUri = extras.getParcelable(DownloadMmsAction.EXTRA_NOTIFICATION_URI);
101         final String conversationId = extras.getString(DownloadMmsAction.EXTRA_CONVERSATION_ID);
102         final String participantId = extras.getString(DownloadMmsAction.EXTRA_PARTICIPANT_ID);
103         Assert.notNull(messageId);
104         Assert.notNull(contentUri);
105         Assert.notNull(notificationUri);
106         Assert.notNull(conversationId);
107         Assert.notNull(participantId);
108 
109         final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
110         final Bundle params = action.actionParameters;
111         params.putBoolean(KEY_DOWNLOADED_BY_PLATFORM, true);
112         params.putString(KEY_MESSAGE_ID, messageId);
113         params.putInt(KEY_RESULT_CODE, resultCode);
114         params.putInt(KEY_HTTP_STATUS_CODE,
115                 extras.getInt(SmsManager.EXTRA_MMS_HTTP_STATUS, 0));
116         params.putParcelable(KEY_CONTENT_URI, contentUri);
117         params.putParcelable(KEY_NOTIFICATION_URI, notificationUri);
118         params.putInt(KEY_SUB_ID,
119                 extras.getInt(DownloadMmsAction.EXTRA_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID));
120         params.putString(KEY_SUB_PHONE_NUMBER,
121                 extras.getString(DownloadMmsAction.EXTRA_SUB_PHONE_NUMBER));
122         params.putString(KEY_TRANSACTION_ID,
123                 extras.getString(DownloadMmsAction.EXTRA_TRANSACTION_ID));
124         params.putString(KEY_CONTENT_LOCATION,
125                 extras.getString(DownloadMmsAction.EXTRA_CONTENT_LOCATION));
126         params.putBoolean(KEY_AUTO_DOWNLOAD,
127                 extras.getBoolean(DownloadMmsAction.EXTRA_AUTO_DOWNLOAD));
128         params.putLong(KEY_RECEIVED_TIMESTAMP,
129                 extras.getLong(DownloadMmsAction.EXTRA_RECEIVED_TIMESTAMP));
130         params.putString(KEY_CONVERSATION_ID, conversationId);
131         params.putString(KEY_PARTICIPANT_ID, participantId);
132         params.putInt(KEY_STATUS_IF_FAILED,
133                 extras.getInt(DownloadMmsAction.EXTRA_STATUS_IF_FAILED));
134         params.putLong(KEY_EXPIRY, extras.getLong(DownloadMmsAction.EXTRA_EXPIRY));
135         action.start();
136     }
137 
138     // This is called for fast failing downloading (due to airplane mode or mobile data )
processMessageDownloadFastFailed(final String messageId, final Uri notificationUri, final String conversationId, final String participantId, final String contentLocation, final int subId, final String subPhoneNumber, final int statusIfFailed, final boolean autoDownload, final String transactionId, final int resultCode)139     public static void processMessageDownloadFastFailed(final String messageId,
140             final Uri notificationUri, final String conversationId, final String participantId,
141             final String contentLocation, final int subId, final String subPhoneNumber,
142             final int statusIfFailed, final boolean autoDownload, final String transactionId,
143             final int resultCode) {
144         Assert.notNull(messageId);
145         Assert.notNull(notificationUri);
146         Assert.notNull(conversationId);
147         Assert.notNull(participantId);
148 
149         final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
150         final Bundle params = action.actionParameters;
151         params.putBoolean(KEY_DOWNLOADED_BY_PLATFORM, true);
152         params.putString(KEY_MESSAGE_ID, messageId);
153         params.putInt(KEY_RESULT_CODE, resultCode);
154         params.putParcelable(KEY_NOTIFICATION_URI, notificationUri);
155         params.putInt(KEY_SUB_ID, subId);
156         params.putString(KEY_SUB_PHONE_NUMBER, subPhoneNumber);
157         params.putString(KEY_CONTENT_LOCATION, contentLocation);
158         params.putBoolean(KEY_AUTO_DOWNLOAD, autoDownload);
159         params.putString(KEY_CONVERSATION_ID, conversationId);
160         params.putString(KEY_PARTICIPANT_ID, participantId);
161         params.putInt(KEY_STATUS_IF_FAILED, statusIfFailed);
162         params.putString(KEY_TRANSACTION_ID, transactionId);
163         action.start();
164     }
165 
processDownloadActionFailure(final String messageId, final int status, final int rawStatus, final String conversationId, final String participantId, final int statusIfFailed, final int subId, final String transactionId)166     public static void processDownloadActionFailure(final String messageId, final int status,
167             final int rawStatus, final String conversationId, final String participantId,
168             final int statusIfFailed, final int subId, final String transactionId) {
169         Assert.notNull(messageId);
170         Assert.notNull(conversationId);
171         Assert.notNull(participantId);
172 
173         final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
174         final Bundle params = action.actionParameters;
175         params.putBoolean(KEY_DOWNLOADED_BY_PLATFORM, false);
176         params.putString(KEY_MESSAGE_ID, messageId);
177         params.putInt(KEY_STATUS, status);
178         params.putInt(KEY_RAW_STATUS, rawStatus);
179         params.putString(KEY_CONVERSATION_ID, conversationId);
180         params.putString(KEY_PARTICIPANT_ID, participantId);
181         params.putInt(KEY_STATUS_IF_FAILED, statusIfFailed);
182         params.putInt(KEY_SUB_ID, subId);
183         params.putString(KEY_TRANSACTION_ID, transactionId);
184         action.start();
185     }
186 
sendDeferredRespStatus(final String messageId, final String transactionId, final String contentLocation, final int subId)187     public static void sendDeferredRespStatus(final String messageId, final String transactionId,
188             final String contentLocation, final int subId) {
189         final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
190         final Bundle params = action.actionParameters;
191         params.putString(KEY_MESSAGE_ID, messageId);
192         params.putString(KEY_TRANSACTION_ID, transactionId);
193         params.putString(KEY_CONTENT_LOCATION, contentLocation);
194         params.putBoolean(KEY_SEND_DEFERRED_RESP_STATUS, true);
195         params.putInt(KEY_SUB_ID, subId);
196         action.start();
197     }
198 
ProcessDownloadedMmsAction()199     private ProcessDownloadedMmsAction() {
200         // Callers must use one of the static methods above
201     }
202 
203     @Override
executeAction()204     protected Object executeAction() {
205         // Fire up the background worker
206         requestBackgroundWork();
207         return null;
208     }
209 
210     @Override
doBackgroundWork()211     protected Bundle doBackgroundWork() throws DataModelException {
212         final Context context = Factory.get().getApplicationContext();
213         final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
214         final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
215         final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
216         final String contentLocation = actionParameters.getString(KEY_CONTENT_LOCATION);
217         final boolean sendDeferredRespStatus =
218                 actionParameters.getBoolean(KEY_SEND_DEFERRED_RESP_STATUS, false);
219 
220         // Send a response indicating that auto-download failed
221         if (sendDeferredRespStatus) {
222             if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
223                 LogUtil.v(TAG, "DownloadMmsAction: Auto-download of message " + messageId
224                         + " failed; sending DEFERRED NotifyRespInd");
225             }
226             MmsUtils.sendNotifyResponseForMmsDownload(
227                     context,
228                     subId,
229                     MmsUtils.stringToBytes(transactionId, "UTF-8"),
230                     contentLocation,
231                     PduHeaders.STATUS_DEFERRED);
232             return null;
233         }
234 
235         // Processing a real MMS download
236         final boolean downloadedByPlatform = actionParameters.getBoolean(
237                 KEY_DOWNLOADED_BY_PLATFORM);
238 
239         final int status;
240         int rawStatus = MmsUtils.PDU_HEADER_VALUE_UNDEFINED;
241         Uri mmsUri = null;
242 
243         if (downloadedByPlatform) {
244             final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
245             if (resultCode == Activity.RESULT_OK) {
246                 final Uri contentUri = actionParameters.getParcelable(KEY_CONTENT_URI);
247                 final File downloadedFile = MmsFileProvider.getFile(contentUri);
248                 byte[] downloadedData = null;
249                 try {
250                     downloadedData = Files.toByteArray(downloadedFile);
251                 } catch (final FileNotFoundException e) {
252                     LogUtil.e(TAG, "ProcessDownloadedMmsAction: MMS download file not found: "
253                             + downloadedFile.getAbsolutePath());
254                 } catch (final IOException e) {
255                     LogUtil.e(TAG, "ProcessDownloadedMmsAction: Error reading MMS download file: "
256                             + downloadedFile.getAbsolutePath(), e);
257                 }
258 
259                 // Can delete the temp file now
260                 if (downloadedFile.exists()) {
261                     downloadedFile.delete();
262                     if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
263                         LogUtil.d(TAG, "ProcessDownloadedMmsAction: Deleted temp file with "
264                                 + "downloaded MMS pdu: " + downloadedFile.getAbsolutePath());
265                     }
266                 }
267 
268                 if (downloadedData != null) {
269                     final RetrieveConf retrieveConf =
270                             MmsSender.parseRetrieveConf(downloadedData, subId);
271                     if (MmsUtils.isDumpMmsEnabled()) {
272                         MmsUtils.dumpPdu(downloadedData, retrieveConf);
273                     }
274                     if (retrieveConf != null) {
275                         // Insert the downloaded MMS into telephony
276                         final Uri notificationUri = actionParameters.getParcelable(
277                                 KEY_NOTIFICATION_URI);
278                         final String subPhoneNumber = actionParameters.getString(
279                                 KEY_SUB_PHONE_NUMBER);
280                         final boolean autoDownload = actionParameters.getBoolean(
281                                 KEY_AUTO_DOWNLOAD);
282                         final long receivedTimestampInSeconds =
283                                 actionParameters.getLong(KEY_RECEIVED_TIMESTAMP);
284                         final long expiry = actionParameters.getLong(KEY_EXPIRY);
285 
286                         // Inform sync we're adding a message to telephony
287                         final SyncManager syncManager = DataModel.get().getSyncManager();
288                         syncManager.onNewMessageInserted(receivedTimestampInSeconds * 1000L);
289 
290                         final MmsUtils.StatusPlusUri result =
291                                 MmsUtils.insertDownloadedMessageAndSendResponse(context,
292                                         notificationUri, subId, subPhoneNumber, transactionId,
293                                         contentLocation, autoDownload, receivedTimestampInSeconds,
294                                         expiry, retrieveConf);
295                         status = result.status;
296                         rawStatus = result.rawStatus;
297                         mmsUri = result.uri;
298                     } else {
299                         // Invalid response PDU
300                         status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
301                     }
302                 } else {
303                     // Failed to read download file
304                     status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
305                 }
306             } else {
307                 LogUtil.w(TAG, "ProcessDownloadedMmsAction: Platform returned error resultCode: "
308                         + resultCode);
309                 final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
310                 status = MmsSender.getErrorResultStatus(resultCode, httpStatusCode);
311             }
312         } else {
313             // Message was already processed by the internal API, or the download action failed.
314             // In either case, we just need to copy the status to the response bundle.
315             status = actionParameters.getInt(KEY_STATUS);
316             rawStatus = actionParameters.getInt(KEY_RAW_STATUS);
317             mmsUri = actionParameters.getParcelable(KEY_MMS_URI);
318         }
319 
320         final Bundle response = new Bundle();
321         response.putInt(BUNDLE_REQUEST_STATUS, status);
322         response.putInt(BUNDLE_RAW_TELEPHONY_STATUS, rawStatus);
323         response.putParcelable(BUNDLE_MMS_URI, mmsUri);
324         return response;
325     }
326 
327     @Override
processBackgroundResponse(final Bundle response)328     protected Object processBackgroundResponse(final Bundle response) {
329         if (response == null) {
330             // No message download to process; doBackgroundWork sent a notify deferred response
331             Assert.isTrue(actionParameters.getBoolean(KEY_SEND_DEFERRED_RESP_STATUS));
332             return null;
333         }
334 
335         final int status = response.getInt(BUNDLE_REQUEST_STATUS);
336         final int rawStatus = response.getInt(BUNDLE_RAW_TELEPHONY_STATUS);
337         final Uri messageUri = response.getParcelable(BUNDLE_MMS_URI);
338         final boolean autoDownload = actionParameters.getBoolean(KEY_AUTO_DOWNLOAD);
339         final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
340 
341         // Do post-processing on downloaded message
342         final MessageData message = processResult(status, rawStatus, messageUri);
343 
344         final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
345         // If we were trying to auto-download but have failed need to send the deferred response
346         if (autoDownload && message == null && status == MmsUtils.MMS_REQUEST_MANUAL_RETRY) {
347             final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
348             final String contentLocation = actionParameters.getString(KEY_CONTENT_LOCATION);
349             sendDeferredRespStatus(messageId, transactionId, contentLocation, subId);
350         }
351 
352         if (autoDownload) {
353             final DatabaseWrapper db = DataModel.get().getDatabase();
354             MessageData toastMessage = message;
355             if (toastMessage == null) {
356                 // If the downloaded failed (message is null), then we should announce the
357                 // receiving of the wap push message. Load the wap push message here instead.
358                 toastMessage = BugleDatabaseOperations.readMessageData(db, messageId);
359             }
360             if (toastMessage != null) {
361                 final ParticipantData sender = ParticipantData.getFromId(
362                         db, toastMessage.getParticipantId());
363                 BugleActionToasts.onMessageReceived(
364                         toastMessage.getConversationId(), sender, toastMessage);
365             }
366         } else {
367             final boolean success = message != null && status == MmsUtils.MMS_REQUEST_SUCCEEDED;
368             BugleActionToasts.onSendMessageOrManualDownloadActionCompleted(
369                     // If download failed, use the wap push message's conversation instead
370                     success ? message.getConversationId()
371                             : actionParameters.getString(KEY_CONVERSATION_ID),
372                     success, status, false/*isSms*/, subId, false /*isSend*/);
373         }
374 
375         final boolean failed = (messageUri == null);
376         ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(failed, this);
377         if (failed) {
378             BugleNotifications.update(false, BugleNotifications.UPDATE_ERRORS);
379         }
380 
381         return message;
382     }
383 
384     @Override
processBackgroundFailure()385     protected Object processBackgroundFailure() {
386         if (actionParameters.getBoolean(KEY_SEND_DEFERRED_RESP_STATUS)) {
387             // We can early-out for these failures. processResult is only designed to handle
388             // post-processing of MMS downloads (whether successful or not).
389             LogUtil.w(TAG,
390                     "ProcessDownloadedMmsAction: Exception while sending deferred NotifyRespInd");
391             return null;
392         }
393 
394         // Background worker threw an exception; require manual retry
395         processResult(MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED,
396                 null /* mmsUri */);
397 
398         ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(true /* failed */,
399                 this);
400 
401         return null;
402     }
403 
processResult(final int status, final int rawStatus, final Uri mmsUri)404     private MessageData processResult(final int status, final int rawStatus, final Uri mmsUri) {
405         final Context context = Factory.get().getApplicationContext();
406         final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
407         final Uri mmsNotificationUri = actionParameters.getParcelable(KEY_NOTIFICATION_URI);
408         final String notificationConversationId = actionParameters.getString(KEY_CONVERSATION_ID);
409         final String notificationParticipantId = actionParameters.getString(KEY_PARTICIPANT_ID);
410         final int statusIfFailed = actionParameters.getInt(KEY_STATUS_IF_FAILED);
411         final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
412 
413         Assert.notNull(messageId);
414 
415         LogUtil.i(TAG, "ProcessDownloadedMmsAction: Processed MMS download of message " + messageId
416                 + "; status is " + MmsUtils.getRequestStatusDescription(status));
417 
418         DatabaseMessages.MmsMessage mms = null;
419         if (status == MmsUtils.MMS_REQUEST_SUCCEEDED && mmsUri != null) {
420             // Delete the initial M-Notification.ind from telephony
421             SqliteWrapper.delete(context, context.getContentResolver(),
422                     mmsNotificationUri, null, null);
423 
424             // Read the sent MMS from the telephony provider
425             mms = MmsUtils.loadMms(mmsUri);
426         }
427 
428         boolean messageInFocusedConversation = false;
429         boolean messageInObservableConversation = false;
430         String conversationId = null;
431         MessageData message = null;
432         final DatabaseWrapper db = DataModel.get().getDatabase();
433         db.beginTransaction();
434         try {
435             if (mms != null) {
436                 final ParticipantData self = ParticipantData.getSelfParticipant(mms.getSubId());
437                 final String selfId =
438                         BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
439 
440                 final List<String> recipients = MmsUtils.getRecipientsByThread(mms.mThreadId);
441                 String from = MmsUtils.getMmsSender(recipients, mms.getUri());
442                 if (from == null) {
443                     LogUtil.w(TAG,
444                             "Downloaded an MMS without sender address; using unknown sender.");
445                     from = ParticipantData.getUnknownSenderDestination();
446                 }
447                 final ParticipantData sender = ParticipantData.getFromRawPhoneBySimLocale(from,
448                         subId);
449                 final String senderParticipantId =
450                         BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, sender);
451                 if (!senderParticipantId.equals(notificationParticipantId)) {
452                     LogUtil.e(TAG, "ProcessDownloadedMmsAction: Downloaded MMS message "
453                             + messageId + " has different sender (participantId = "
454                             + senderParticipantId + ") than notification ("
455                             + notificationParticipantId + ")");
456                 }
457                 final boolean blockedSender = BugleDatabaseOperations.isBlockedDestination(
458                         db, sender.getNormalizedDestination());
459                 conversationId = BugleDatabaseOperations.getOrCreateConversationFromThreadId(db,
460                         mms.mThreadId, blockedSender, subId);
461 
462                 messageInFocusedConversation =
463                         DataModel.get().isFocusedConversation(conversationId);
464                 messageInObservableConversation =
465                         DataModel.get().isNewMessageObservable(conversationId);
466 
467                 // TODO: Also write these values to the telephony provider
468                 mms.mRead = messageInFocusedConversation;
469                 mms.mSeen = messageInObservableConversation;
470 
471                 // Translate to our format
472                 message = MmsUtils.createMmsMessage(mms, conversationId, senderParticipantId,
473                         selfId, MessageData.BUGLE_STATUS_INCOMING_COMPLETE);
474                 // Update image sizes.
475                 message.updateSizesForImageParts();
476                 // Inform sync that message has been added at local received timestamp
477                 final SyncManager syncManager = DataModel.get().getSyncManager();
478                 syncManager.onNewMessageInserted(message.getReceivedTimeStamp());
479                 final MessageData current = BugleDatabaseOperations.readMessageData(db, messageId);
480                 if (current == null) {
481                     LogUtil.w(TAG, "Message deleted prior to update");
482                     BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
483                 } else {
484                     // Overwrite existing notification message
485                     message.updateMessageId(messageId);
486                     // Write message
487                     BugleDatabaseOperations.updateMessageInTransaction(db, message);
488                 }
489 
490                 if (!TextUtils.equals(notificationConversationId, conversationId)) {
491                     // If this is a group conversation, the message is moved. So the original
492                     // 1v1 conversation (as referenced by notificationConversationId) could
493                     // be left with no non-draft message. Delete the conversation if that
494                     // happens. See the comment for the method below for why we need to do this.
495                     if (!BugleDatabaseOperations.deleteConversationIfEmptyInTransaction(
496                             db, notificationConversationId)) {
497                         BugleDatabaseOperations.maybeRefreshConversationMetadataInTransaction(
498                                 db, notificationConversationId, messageId,
499                                 true /*shouldAutoSwitchSelfId*/, blockedSender /*keepArchived*/);
500                     }
501                 }
502 
503                 BugleDatabaseOperations.refreshConversationMetadataInTransaction(db, conversationId,
504                         true /*shouldAutoSwitchSelfId*/, blockedSender /*keepArchived*/);
505             } else {
506                 messageInFocusedConversation =
507                         DataModel.get().isFocusedConversation(notificationConversationId);
508 
509                 // Default to retry status unless status indicates otherwise
510                 int bugleStatus = statusIfFailed;
511                 if (status == MmsUtils.MMS_REQUEST_MANUAL_RETRY) {
512                     bugleStatus = MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED;
513                 } else if (status == MmsUtils.MMS_REQUEST_NO_RETRY) {
514                     bugleStatus = MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE;
515                 }
516                 DownloadMmsAction.updateMessageStatus(mmsNotificationUri, messageId,
517                         notificationConversationId, bugleStatus, rawStatus);
518 
519                 // Log MMS download failed
520                 final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
521                 final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
522 
523                 // Just in case this was the latest message update the summary data
524                 BugleDatabaseOperations.refreshConversationMetadataInTransaction(db,
525                         notificationConversationId, true /*shouldAutoSwitchSelfId*/,
526                         false /*keepArchived*/);
527             }
528 
529             db.setTransactionSuccessful();
530         } finally {
531             db.endTransaction();
532         }
533 
534         if (mmsUri != null) {
535             // Update mms table with read status now we know the conversation id
536             final ContentValues values = new ContentValues(1);
537             values.put(Mms.READ, messageInFocusedConversation);
538             SqliteWrapper.update(context, context.getContentResolver(), mmsUri, values,
539                     null, null);
540         }
541 
542         // Show a notification to let the user know a new message has arrived
543         BugleNotifications.update(false /*silent*/, conversationId, BugleNotifications.UPDATE_ALL);
544 
545         // Messages may have changed in two conversations
546         if (conversationId != null) {
547             MessagingContentProvider.notifyMessagesChanged(conversationId);
548         }
549         MessagingContentProvider.notifyMessagesChanged(notificationConversationId);
550         MessagingContentProvider.notifyPartsChanged();
551 
552         return message;
553     }
554 
ProcessDownloadedMmsAction(final Parcel in)555     private ProcessDownloadedMmsAction(final Parcel in) {
556         super(in);
557     }
558 
559     public static final Parcelable.Creator<ProcessDownloadedMmsAction> CREATOR
560             = new Parcelable.Creator<ProcessDownloadedMmsAction>() {
561         @Override
562         public ProcessDownloadedMmsAction createFromParcel(final Parcel in) {
563             return new ProcessDownloadedMmsAction(in);
564         }
565 
566         @Override
567         public ProcessDownloadedMmsAction[] newArray(final int size) {
568             return new ProcessDownloadedMmsAction[size];
569         }
570     };
571 
572     @Override
writeToParcel(final Parcel parcel, final int flags)573     public void writeToParcel(final Parcel parcel, final int flags) {
574         writeActionToParcel(parcel, flags);
575     }
576 }
577