Home | History | Annotate | Download | only in action
      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.Context;
     20 import android.database.Cursor;
     21 import android.database.sqlite.SQLiteException;
     22 import android.os.Bundle;
     23 import android.os.Parcel;
     24 import android.os.Parcelable;
     25 import android.os.SystemClock;
     26 import android.provider.Telephony.Mms;
     27 import android.support.v4.util.LongSparseArray;
     28 
     29 import com.android.messaging.Factory;
     30 import com.android.messaging.datamodel.DataModel;
     31 import com.android.messaging.datamodel.DatabaseWrapper;
     32 import com.android.messaging.datamodel.MessagingContentProvider;
     33 import com.android.messaging.datamodel.SyncManager;
     34 import com.android.messaging.datamodel.SyncManager.ThreadInfoCache;
     35 import com.android.messaging.datamodel.data.ParticipantData;
     36 import com.android.messaging.mmslib.SqliteWrapper;
     37 import com.android.messaging.sms.DatabaseMessages;
     38 import com.android.messaging.sms.DatabaseMessages.LocalDatabaseMessage;
     39 import com.android.messaging.sms.DatabaseMessages.MmsMessage;
     40 import com.android.messaging.sms.DatabaseMessages.SmsMessage;
     41 import com.android.messaging.sms.MmsUtils;
     42 import com.android.messaging.util.Assert;
     43 import com.android.messaging.util.BugleGservices;
     44 import com.android.messaging.util.BugleGservicesKeys;
     45 import com.android.messaging.util.BuglePrefs;
     46 import com.android.messaging.util.BuglePrefsKeys;
     47 import com.android.messaging.util.ContentType;
     48 import com.android.messaging.util.LogUtil;
     49 import com.android.messaging.util.OsUtil;
     50 
     51 import java.util.ArrayList;
     52 import java.util.List;
     53 import java.util.Locale;
     54 
     55 /**
     56  * Action used to sync messages from smsmms db to local database
     57  */
     58 public class SyncMessagesAction extends Action implements Parcelable {
     59     static final long SYNC_FAILED = Long.MIN_VALUE;
     60 
     61     private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
     62 
     63     private static final String KEY_START_TIMESTAMP = "start_timestamp";
     64     private static final String KEY_MAX_UPDATE = "max_update";
     65     private static final String KEY_LOWER_BOUND = "lower_bound";
     66     private static final String KEY_UPPER_BOUND = "upper_bound";
     67     private static final String BUNDLE_KEY_LAST_TIMESTAMP = "last_timestamp";
     68     private static final String BUNDLE_KEY_SMS_MESSAGES = "sms_to_add";
     69     private static final String BUNDLE_KEY_MMS_MESSAGES = "mms_to_add";
     70     private static final String BUNDLE_KEY_MESSAGES_TO_DELETE = "messages_to_delete";
     71 
     72     /**
     73      * Start a full sync (backed off a few seconds to avoid pulling sending/receiving messages).
     74      */
     75     public static void fullSync() {
     76         final BugleGservices bugleGservices = BugleGservices.get();
     77         final long smsSyncBackoffTimeMillis = bugleGservices.getLong(
     78                 BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS,
     79                 BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS_DEFAULT);
     80 
     81         final long now = System.currentTimeMillis();
     82         // TODO: Could base this off most recent message in db but now should be okay...
     83         final long startTimestamp = now - smsSyncBackoffTimeMillis;
     84 
     85         final SyncMessagesAction action = new SyncMessagesAction(-1L, startTimestamp,
     86                 0, startTimestamp);
     87         action.start();
     88     }
     89 
     90     /**
     91      * Start an incremental sync to pull messages since last sync (backed off a few seconds)..
     92      */
     93     public static void sync() {
     94         final BugleGservices bugleGservices = BugleGservices.get();
     95         final long smsSyncBackoffTimeMillis = bugleGservices.getLong(
     96                 BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS,
     97                 BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS_DEFAULT);
     98 
     99         final long now = System.currentTimeMillis();
    100         // TODO: Could base this off most recent message in db but now should be okay...
    101         final long startTimestamp = now - smsSyncBackoffTimeMillis;
    102 
    103         sync(startTimestamp);
    104     }
    105 
    106     /**
    107      * Start an incremental sync when the application starts up (no back off as not yet
    108      *  sending/receiving).
    109      */
    110     public static void immediateSync() {
    111         final long now = System.currentTimeMillis();
    112         // TODO: Could base this off most recent message in db but now should be okay...
    113         final long startTimestamp = now;
    114 
    115         sync(startTimestamp);
    116     }
    117 
    118     private static void sync(final long startTimestamp) {
    119         if (!OsUtil.hasSmsPermission()) {
    120             // Sync requires READ_SMS permission
    121             return;
    122         }
    123 
    124         final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
    125         // Lower bound is end of previous sync
    126         final long syncLowerBoundTimeMillis = prefs.getLong(BuglePrefsKeys.LAST_SYNC_TIME,
    127                     BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT);
    128 
    129         final SyncMessagesAction action = new SyncMessagesAction(syncLowerBoundTimeMillis,
    130                 startTimestamp, 0, startTimestamp);
    131         action.start();
    132     }
    133 
    134     private SyncMessagesAction(final long lowerBound, final long upperBound,
    135             final int maxMessagesToUpdate, final long startTimestamp) {
    136         actionParameters.putLong(KEY_LOWER_BOUND, lowerBound);
    137         actionParameters.putLong(KEY_UPPER_BOUND, upperBound);
    138         actionParameters.putInt(KEY_MAX_UPDATE, maxMessagesToUpdate);
    139         actionParameters.putLong(KEY_START_TIMESTAMP, startTimestamp);
    140     }
    141 
    142     @Override
    143     protected Object executeAction() {
    144         final DatabaseWrapper db = DataModel.get().getDatabase();
    145 
    146         long lowerBoundTimeMillis = actionParameters.getLong(KEY_LOWER_BOUND);
    147         final long upperBoundTimeMillis = actionParameters.getLong(KEY_UPPER_BOUND);
    148         final int initialMaxMessagesToUpdate = actionParameters.getInt(KEY_MAX_UPDATE);
    149         final long startTimestamp = actionParameters.getLong(KEY_START_TIMESTAMP);
    150 
    151         if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    152             LogUtil.d(TAG, "SyncMessagesAction: Request to sync messages from "
    153                     + lowerBoundTimeMillis + " to " + upperBoundTimeMillis + " (start timestamp = "
    154                     + startTimestamp + ", message update limit = " + initialMaxMessagesToUpdate
    155                     + ")");
    156         }
    157 
    158         final SyncManager syncManager = DataModel.get().getSyncManager();
    159         if (lowerBoundTimeMillis >= 0) {
    160             // Cursors
    161             final SyncCursorPair cursors = new SyncCursorPair(-1L, lowerBoundTimeMillis);
    162             final boolean inSync = cursors.isSynchronized(db);
    163             if (!inSync) {
    164                 if (syncManager.delayUntilFullSync(startTimestamp) == 0) {
    165                     lowerBoundTimeMillis = -1;
    166                     actionParameters.putLong(KEY_LOWER_BOUND, lowerBoundTimeMillis);
    167 
    168                     if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    169                         LogUtil.d(TAG, "SyncMessagesAction: Messages before "
    170                                 + lowerBoundTimeMillis + " not in sync; promoting to full sync");
    171                     }
    172                 } else if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    173                     LogUtil.d(TAG, "SyncMessagesAction: Messages before "
    174                             + lowerBoundTimeMillis + " not in sync; will do incremental sync");
    175                 }
    176             } else {
    177                 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    178                     LogUtil.d(TAG, "SyncMessagesAction: Messages before " + lowerBoundTimeMillis
    179                             + " are in sync");
    180                 }
    181             }
    182         }
    183 
    184         // Check if sync allowed (can be too soon after last or one is already running)
    185         if (syncManager.shouldSync(lowerBoundTimeMillis < 0, startTimestamp)) {
    186             syncManager.startSyncBatch(upperBoundTimeMillis);
    187             requestBackgroundWork();
    188         }
    189 
    190         return null;
    191     }
    192 
    193     @Override
    194     protected Bundle doBackgroundWork() {
    195         final BugleGservices bugleGservices = BugleGservices.get();
    196         final DatabaseWrapper db = DataModel.get().getDatabase();
    197 
    198         final int maxMessagesToScan = bugleGservices.getInt(
    199                 BugleGservicesKeys.SMS_SYNC_BATCH_MAX_MESSAGES_TO_SCAN,
    200                 BugleGservicesKeys.SMS_SYNC_BATCH_MAX_MESSAGES_TO_SCAN_DEFAULT);
    201 
    202         final int initialMaxMessagesToUpdate = actionParameters.getInt(KEY_MAX_UPDATE);
    203         final int smsSyncSubsequentBatchSizeMin = bugleGservices.getInt(
    204                 BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MIN,
    205                 BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MIN_DEFAULT);
    206         final int smsSyncSubsequentBatchSizeMax = bugleGservices.getInt(
    207                 BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MAX,
    208                 BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MAX_DEFAULT);
    209 
    210         // Cap sync size to GServices limits
    211         final int maxMessagesToUpdate = Math.max(smsSyncSubsequentBatchSizeMin,
    212                 Math.min(initialMaxMessagesToUpdate, smsSyncSubsequentBatchSizeMax));
    213 
    214         final long lowerBoundTimeMillis = actionParameters.getLong(KEY_LOWER_BOUND);
    215         final long upperBoundTimeMillis = actionParameters.getLong(KEY_UPPER_BOUND);
    216 
    217         LogUtil.i(TAG, "SyncMessagesAction: Starting batch for messages from "
    218                 + lowerBoundTimeMillis + " to " + upperBoundTimeMillis
    219                 + " (message update limit = " + maxMessagesToUpdate + ", message scan limit = "
    220                 + maxMessagesToScan + ")");
    221 
    222         // Clear last change time so that we can work out if this batch is dirty when it completes
    223         final SyncManager syncManager = DataModel.get().getSyncManager();
    224 
    225         // Clear the singleton cache that maps threads to recipients and to conversations.
    226         final SyncManager.ThreadInfoCache cache = syncManager.getThreadInfoCache();
    227         cache.clear();
    228 
    229         // Sms messages to store
    230         final ArrayList<SmsMessage> smsToAdd = new ArrayList<SmsMessage>();
    231         // Mms messages to store
    232         final LongSparseArray<MmsMessage> mmsToAdd = new LongSparseArray<MmsMessage>();
    233         // List of local SMS/MMS to remove
    234         final ArrayList<LocalDatabaseMessage> messagesToDelete =
    235                 new ArrayList<LocalDatabaseMessage>();
    236 
    237         long lastTimestampMillis = SYNC_FAILED;
    238         if (syncManager.isSyncing(upperBoundTimeMillis)) {
    239             // Cursors
    240             final SyncCursorPair cursors = new SyncCursorPair(lowerBoundTimeMillis,
    241                     upperBoundTimeMillis);
    242 
    243             // Actually compare the messages using cursor pair
    244             lastTimestampMillis = syncCursorPair(db, cursors, smsToAdd, mmsToAdd,
    245                     messagesToDelete, maxMessagesToScan, maxMessagesToUpdate, cache);
    246         }
    247         final Bundle response = new Bundle();
    248 
    249         // If comparison succeeds bundle up the changes for processing in ActionService
    250         if (lastTimestampMillis > SYNC_FAILED) {
    251             final ArrayList<MmsMessage> mmsToAddList = new ArrayList<MmsMessage>();
    252             for (int i = 0; i < mmsToAdd.size(); i++) {
    253                 final MmsMessage mms = mmsToAdd.valueAt(i);
    254                 mmsToAddList.add(mms);
    255             }
    256 
    257             response.putParcelableArrayList(BUNDLE_KEY_SMS_MESSAGES, smsToAdd);
    258             response.putParcelableArrayList(BUNDLE_KEY_MMS_MESSAGES, mmsToAddList);
    259             response.putParcelableArrayList(BUNDLE_KEY_MESSAGES_TO_DELETE, messagesToDelete);
    260         }
    261         response.putLong(BUNDLE_KEY_LAST_TIMESTAMP, lastTimestampMillis);
    262 
    263         return response;
    264     }
    265 
    266     /**
    267      * Compare messages based on timestamp and uri
    268      * @param db local database wrapper
    269      * @param cursors cursor pair holding references to local and remote messages
    270      * @param smsToAdd newly found sms messages to add
    271      * @param mmsToAdd newly found mms messages to add
    272      * @param messagesToDelete messages not found needing deletion
    273      * @param maxMessagesToScan max messages to scan for changes
    274      * @param maxMessagesToUpdate max messages to return for updates
    275      * @param cache cache for conversation id / thread id / recipient set mapping
    276      * @return timestamp of the oldest message seen during the sync scan
    277      */
    278     private long syncCursorPair(final DatabaseWrapper db, final SyncCursorPair cursors,
    279             final ArrayList<SmsMessage> smsToAdd, final LongSparseArray<MmsMessage> mmsToAdd,
    280             final ArrayList<LocalDatabaseMessage> messagesToDelete, final int maxMessagesToScan,
    281             final int maxMessagesToUpdate, final ThreadInfoCache cache) {
    282         long lastTimestampMillis;
    283         final long startTimeMillis = SystemClock.elapsedRealtime();
    284 
    285         // Number of messages scanned local and remote
    286         int localPos = 0;
    287         int remotePos = 0;
    288         int localTotal = 0;
    289         int remoteTotal = 0;
    290         // Scan through the messages on both sides and prepare messages for local message table
    291         // changes (including adding and deleting)
    292         try {
    293             cursors.query(db);
    294 
    295             localTotal = cursors.getLocalCount();
    296             remoteTotal = cursors.getRemoteCount();
    297 
    298             if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    299                 LogUtil.d(TAG, "SyncMessagesAction: Scanning cursors (local count = " + localTotal
    300                         + ", remote count = " + remoteTotal + ", message update limit = "
    301                         + maxMessagesToUpdate + ", message scan limit = " + maxMessagesToScan
    302                         + ")");
    303             }
    304 
    305             lastTimestampMillis = cursors.scan(maxMessagesToScan, maxMessagesToUpdate,
    306                     smsToAdd, mmsToAdd, messagesToDelete, cache);
    307 
    308             localPos = cursors.getLocalPosition();
    309             remotePos = cursors.getRemotePosition();
    310 
    311             if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    312                 LogUtil.d(TAG, "SyncMessagesAction: Scanned cursors (local position = " + localPos
    313                         + " of " + localTotal + ", remote position = " + remotePos + " of "
    314                         + remoteTotal + ")");
    315             }
    316 
    317             // Batch loading the parts of the MMS messages in this batch
    318             loadMmsParts(mmsToAdd);
    319             // Lookup senders for incoming mms messages
    320             setMmsSenders(mmsToAdd, cache);
    321         } catch (final SQLiteException e) {
    322             LogUtil.e(TAG, "SyncMessagesAction: Database exception", e);
    323             // Let's abort
    324             lastTimestampMillis = SYNC_FAILED;
    325         } catch (final Exception e) {
    326             // We want to catch anything unexpected since this is running in a separate thread
    327             // and any unexpected exception will just fail this thread silently.
    328             // Let's crash for dogfooders!
    329             LogUtil.wtf(TAG, "SyncMessagesAction: unexpected failure in scan", e);
    330             lastTimestampMillis = SYNC_FAILED;
    331         } finally {
    332             if (cursors != null) {
    333                 cursors.close();
    334             }
    335         }
    336 
    337         final long endTimeMillis = SystemClock.elapsedRealtime();
    338 
    339         if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    340             LogUtil.d(TAG, "SyncMessagesAction: Scan complete (took "
    341                     + (endTimeMillis - startTimeMillis) + " ms). " + smsToAdd.size()
    342                     + " remote SMS to add, " + mmsToAdd.size() + " MMS to add, "
    343                     + messagesToDelete.size() + " local messages to delete. "
    344                     + "Oldest timestamp seen = " + lastTimestampMillis);
    345         }
    346 
    347         return lastTimestampMillis;
    348     }
    349 
    350     /**
    351      * Perform local database updates and schedule follow on sync actions
    352      */
    353     @Override
    354     protected Object processBackgroundResponse(final Bundle response) {
    355         final long lastTimestampMillis = response.getLong(BUNDLE_KEY_LAST_TIMESTAMP);
    356         final long lowerBoundTimeMillis = actionParameters.getLong(KEY_LOWER_BOUND);
    357         final long upperBoundTimeMillis = actionParameters.getLong(KEY_UPPER_BOUND);
    358         final int maxMessagesToUpdate = actionParameters.getInt(KEY_MAX_UPDATE);
    359         final long startTimestamp = actionParameters.getLong(KEY_START_TIMESTAMP);
    360 
    361         // Check with the sync manager if any conflicting updates have been made to databases
    362         final SyncManager syncManager = DataModel.get().getSyncManager();
    363         final boolean orphan = !syncManager.isSyncing(upperBoundTimeMillis);
    364 
    365         // lastTimestampMillis used to indicate failure
    366         if (orphan) {
    367             // This batch does not match current in progress timestamp.
    368             LogUtil.w(TAG, "SyncMessagesAction: Ignoring orphan sync batch for messages from "
    369                     + lowerBoundTimeMillis + " to " + upperBoundTimeMillis);
    370         } else {
    371             final boolean dirty = syncManager.isBatchDirty(lastTimestampMillis);
    372             if (lastTimestampMillis == SYNC_FAILED) {
    373                 LogUtil.e(TAG, "SyncMessagesAction: Sync failed - terminating");
    374 
    375                 // Failed - update last sync times to throttle our failure rate
    376                 final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
    377                 // Save sync completion time so next sync will start from here
    378                 prefs.putLong(BuglePrefsKeys.LAST_SYNC_TIME, startTimestamp);
    379                 // Remember last full sync so that don't start background full sync right away
    380                 prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, startTimestamp);
    381 
    382                 syncManager.complete();
    383             } else if (dirty) {
    384                 LogUtil.w(TAG, "SyncMessagesAction: Redoing dirty sync batch of messages from "
    385                         + lowerBoundTimeMillis + " to " + upperBoundTimeMillis);
    386 
    387                 // Redo this batch
    388                 final SyncMessagesAction nextBatch =
    389                         new SyncMessagesAction(lowerBoundTimeMillis, upperBoundTimeMillis,
    390                                 maxMessagesToUpdate, startTimestamp);
    391 
    392                 syncManager.startSyncBatch(upperBoundTimeMillis);
    393                 requestBackgroundWork(nextBatch);
    394             } else {
    395                 // Succeeded
    396                 final ArrayList<SmsMessage> smsToAdd =
    397                         response.getParcelableArrayList(BUNDLE_KEY_SMS_MESSAGES);
    398                 final ArrayList<MmsMessage> mmsToAdd =
    399                         response.getParcelableArrayList(BUNDLE_KEY_MMS_MESSAGES);
    400                 final ArrayList<LocalDatabaseMessage> messagesToDelete =
    401                         response.getParcelableArrayList(BUNDLE_KEY_MESSAGES_TO_DELETE);
    402 
    403                 final int messagesUpdated = smsToAdd.size() + mmsToAdd.size()
    404                         + messagesToDelete.size();
    405 
    406                 // Perform local database changes in one transaction
    407                 long txnTimeMillis = 0;
    408                 if (messagesUpdated > 0) {
    409                     final long startTimeMillis = SystemClock.elapsedRealtime();
    410                     final SyncMessageBatch batch = new SyncMessageBatch(smsToAdd, mmsToAdd,
    411                             messagesToDelete, syncManager.getThreadInfoCache());
    412                     batch.updateLocalDatabase();
    413                     final long endTimeMillis = SystemClock.elapsedRealtime();
    414                     txnTimeMillis = endTimeMillis - startTimeMillis;
    415 
    416                     LogUtil.i(TAG, "SyncMessagesAction: Updated local database "
    417                             + "(took " + txnTimeMillis + " ms). Added "
    418                             + smsToAdd.size() + " SMS, added " + mmsToAdd.size() + " MMS, deleted "
    419                             + messagesToDelete.size() + " messages.");
    420 
    421                     // TODO: Investigate whether we can make this more fine-grained.
    422                     MessagingContentProvider.notifyEverythingChanged();
    423                 } else {
    424                     if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    425                         LogUtil.d(TAG, "SyncMessagesAction: No local database updates to make");
    426                     }
    427 
    428                     if (!syncManager.getHasFirstSyncCompleted()) {
    429                         // If we have never completed a sync before (fresh install) and there are
    430                         // no messages, still inform the UI of a change so it can update syncing
    431                         // messages shown to the user
    432                         MessagingContentProvider.notifyConversationListChanged();
    433                         MessagingContentProvider.notifyPartsChanged();
    434                     }
    435                 }
    436                 // Determine if there are more messages that need to be scanned
    437                 if (lastTimestampMillis >= 0 && lastTimestampMillis >= lowerBoundTimeMillis) {
    438                     if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    439                         LogUtil.d(TAG, "SyncMessagesAction: More messages to sync; scheduling next "
    440                                 + "sync batch now.");
    441                     }
    442 
    443                     // Include final millisecond of last sync in next sync
    444                     final long newUpperBoundTimeMillis = lastTimestampMillis + 1;
    445                     final int newMaxMessagesToUpdate = nextBatchSize(messagesUpdated,
    446                             txnTimeMillis);
    447 
    448                     final SyncMessagesAction nextBatch =
    449                             new SyncMessagesAction(lowerBoundTimeMillis, newUpperBoundTimeMillis,
    450                                     newMaxMessagesToUpdate, startTimestamp);
    451 
    452                     // Proceed with next batch
    453                     syncManager.startSyncBatch(newUpperBoundTimeMillis);
    454                     requestBackgroundWork(nextBatch);
    455                 } else {
    456                     final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
    457                     // Save sync completion time so next sync will start from here
    458                     prefs.putLong(BuglePrefsKeys.LAST_SYNC_TIME, startTimestamp);
    459                     if (lowerBoundTimeMillis < 0) {
    460                         // Remember last full sync so that don't start another full sync right away
    461                         prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, startTimestamp);
    462                     }
    463 
    464                     final long now = System.currentTimeMillis();
    465 
    466                     // After any sync check if new messages have arrived
    467                     final SyncCursorPair recents = new SyncCursorPair(startTimestamp, now);
    468                     final SyncCursorPair olders = new SyncCursorPair(-1L, startTimestamp);
    469                     final DatabaseWrapper db = DataModel.get().getDatabase();
    470                     if (!recents.isSynchronized(db)) {
    471                         LogUtil.i(TAG, "SyncMessagesAction: Changed messages after sync; "
    472                                 + "scheduling an incremental sync now.");
    473 
    474                         // Just add a new batch for recent messages
    475                         final SyncMessagesAction nextBatch =
    476                                 new SyncMessagesAction(startTimestamp, now, 0, startTimestamp);
    477                         syncManager.startSyncBatch(now);
    478                         requestBackgroundWork(nextBatch);
    479                         // After partial sync verify sync state
    480                     } else if (lowerBoundTimeMillis >= 0 && !olders.isSynchronized(db)) {
    481                         // Add a batch going back to start of time
    482                         LogUtil.w(TAG, "SyncMessagesAction: Changed messages before sync batch; "
    483                                 + "scheduling a full sync now.");
    484 
    485                         final SyncMessagesAction nextBatch =
    486                                 new SyncMessagesAction(-1L, startTimestamp, 0, startTimestamp);
    487 
    488                         syncManager.startSyncBatch(startTimestamp);
    489                         requestBackgroundWork(nextBatch);
    490                     } else {
    491                         LogUtil.i(TAG, "SyncMessagesAction: All messages now in sync");
    492 
    493                         // All done, in sync
    494                         syncManager.complete();
    495                     }
    496                 }
    497                 // Either sync should be complete or we should have a follow up request
    498                 Assert.isTrue(hasBackgroundActions() || !syncManager.isSyncing());
    499             }
    500         }
    501 
    502         return null;
    503     }
    504 
    505     /**
    506      * Decide the next batch size based on the stats we collected with past batch
    507      * @param messagesUpdated number of messages updated in this batch
    508      * @param txnTimeMillis time the transaction took in ms
    509      * @return Target number of messages to sync for next batch
    510      */
    511     private static int nextBatchSize(final int messagesUpdated, final long txnTimeMillis) {
    512         final BugleGservices bugleGservices = BugleGservices.get();
    513         final long smsSyncSubsequentBatchTimeLimitMillis = bugleGservices.getLong(
    514                 BugleGservicesKeys.SMS_SYNC_BATCH_TIME_LIMIT_MILLIS,
    515                 BugleGservicesKeys.SMS_SYNC_BATCH_TIME_LIMIT_MILLIS_DEFAULT);
    516 
    517         if (txnTimeMillis <= 0) {
    518             return 0;
    519         }
    520         // Number of messages we can sync within the batch time limit using
    521         // the average sync time calculated based on the stats we collected
    522         // in previous batch
    523         return (int) ((double) (messagesUpdated) / (double) txnTimeMillis
    524                         * smsSyncSubsequentBatchTimeLimitMillis);
    525     }
    526 
    527     /**
    528      * Batch loading MMS parts for the messages in current batch
    529      */
    530     private void loadMmsParts(final LongSparseArray<MmsMessage> mmses) {
    531         final Context context = Factory.get().getApplicationContext();
    532         final int totalIds = mmses.size();
    533         for (int start = 0; start < totalIds; start += MmsUtils.MAX_IDS_PER_QUERY) {
    534             final int end = Math.min(start + MmsUtils.MAX_IDS_PER_QUERY, totalIds); //excluding
    535             final int count = end - start;
    536             final String batchSelection = String.format(
    537                     Locale.US,
    538                     "%s != '%s' AND %s IN %s",
    539                     Mms.Part.CONTENT_TYPE,
    540                     ContentType.APP_SMIL,
    541                     Mms.Part.MSG_ID,
    542                     MmsUtils.getSqlInOperand(count));
    543             final String[] batchSelectionArgs = new String[count];
    544             for (int i = 0; i < count; i++) {
    545                 batchSelectionArgs[i] = Long.toString(mmses.valueAt(start + i).getId());
    546             }
    547             final Cursor cursor = SqliteWrapper.query(
    548                     context,
    549                     context.getContentResolver(),
    550                     MmsUtils.MMS_PART_CONTENT_URI,
    551                     DatabaseMessages.MmsPart.PROJECTION,
    552                     batchSelection,
    553                     batchSelectionArgs,
    554                     null/*sortOrder*/);
    555             if (cursor != null) {
    556                 try {
    557                     while (cursor.moveToNext()) {
    558                         // Delay loading the media content for parsing for efficiency
    559                         // TODO: load the media and fill in the dimensions when
    560                         // we actually display it
    561                         final DatabaseMessages.MmsPart part =
    562                                 DatabaseMessages.MmsPart.get(cursor, false/*loadMedia*/);
    563                         final DatabaseMessages.MmsMessage mms = mmses.get(part.mMessageId);
    564                         if (mms != null) {
    565                             mms.addPart(part);
    566                         }
    567                     }
    568                 } finally {
    569                     cursor.close();
    570                 }
    571             }
    572         }
    573     }
    574 
    575     /**
    576      * Batch loading MMS sender for the messages in current batch
    577      */
    578     private void setMmsSenders(final LongSparseArray<MmsMessage> mmses,
    579             final ThreadInfoCache cache) {
    580         // Store all the MMS messages
    581         for (int i = 0; i < mmses.size(); i++) {
    582             final MmsMessage mms = mmses.valueAt(i);
    583 
    584             final boolean isOutgoing = mms.mType != Mms.MESSAGE_BOX_INBOX;
    585             String senderId = null;
    586             if (!isOutgoing) {
    587                 // We only need to find out sender phone number for received message
    588                 senderId = getMmsSender(mms, cache);
    589                 if (senderId == null) {
    590                     LogUtil.w(TAG, "SyncMessagesAction: Could not find sender of incoming MMS "
    591                             + "message " + mms.getUri() + "; using 'unknown sender' instead");
    592                     senderId = ParticipantData.getUnknownSenderDestination();
    593                 }
    594             }
    595             mms.setSender(senderId);
    596         }
    597     }
    598 
    599     /**
    600      * Find out the sender of an MMS message
    601      */
    602     private String getMmsSender(final MmsMessage mms, final ThreadInfoCache cache) {
    603         final List<String> recipients = cache.getThreadRecipients(mms.mThreadId);
    604         Assert.notNull(recipients);
    605         Assert.isTrue(recipients.size() > 0);
    606 
    607         if (recipients.size() == 1
    608                 && recipients.get(0).equals(ParticipantData.getUnknownSenderDestination())) {
    609             LogUtil.w(TAG, "SyncMessagesAction: MMS message " + mms.mUri + " has unknown sender "
    610                     + "(thread id = " + mms.mThreadId + ")");
    611         }
    612 
    613         return MmsUtils.getMmsSender(recipients, mms.mUri);
    614     }
    615 
    616     private SyncMessagesAction(final Parcel in) {
    617         super(in);
    618     }
    619 
    620     public static final Parcelable.Creator<SyncMessagesAction> CREATOR
    621             = new Parcelable.Creator<SyncMessagesAction>() {
    622         @Override
    623         public SyncMessagesAction createFromParcel(final Parcel in) {
    624             return new SyncMessagesAction(in);
    625         }
    626 
    627         @Override
    628         public SyncMessagesAction[] newArray(final int size) {
    629             return new SyncMessagesAction[size];
    630         }
    631     };
    632 
    633     @Override
    634     public void writeToParcel(final Parcel parcel, final int flags) {
    635         writeActionToParcel(parcel, flags);
    636     }
    637 }
    638