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 androidx.collection.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      */
fullSync()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      */
sync()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      */
immediateSync()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 
sync(final long startTimestamp)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 
SyncMessagesAction(final long lowerBound, final long upperBound, final int maxMessagesToUpdate, final long startTimestamp)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
executeAction()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
doBackgroundWork()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      */
syncCursorPair(final DatabaseWrapper db, final SyncCursorPair cursors, final ArrayList<SmsMessage> smsToAdd, final LongSparseArray<MmsMessage> mmsToAdd, final ArrayList<LocalDatabaseMessage> messagesToDelete, final int maxMessagesToScan, final int maxMessagesToUpdate, final ThreadInfoCache cache)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
processBackgroundResponse(final Bundle response)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      */
nextBatchSize(final int messagesUpdated, final long txnTimeMillis)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      */
loadMmsParts(final LongSparseArray<MmsMessage> mmses)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      */
setMmsSenders(final LongSparseArray<MmsMessage> mmses, final ThreadInfoCache cache)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      */
getMmsSender(final MmsMessage mms, final ThreadInfoCache cache)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 
SyncMessagesAction(final Parcel in)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
writeToParcel(final Parcel parcel, final int flags)634     public void writeToParcel(final Parcel parcel, final int flags) {
635         writeActionToParcel(parcel, flags);
636     }
637 }
638