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
17package com.android.messaging.datamodel.action;
18
19import android.content.Context;
20import android.database.Cursor;
21import android.database.sqlite.SQLiteException;
22import android.os.Bundle;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.os.SystemClock;
26import android.provider.Telephony.Mms;
27import android.support.v4.util.LongSparseArray;
28
29import com.android.messaging.Factory;
30import com.android.messaging.datamodel.DataModel;
31import com.android.messaging.datamodel.DatabaseWrapper;
32import com.android.messaging.datamodel.MessagingContentProvider;
33import com.android.messaging.datamodel.SyncManager;
34import com.android.messaging.datamodel.SyncManager.ThreadInfoCache;
35import com.android.messaging.datamodel.data.ParticipantData;
36import com.android.messaging.mmslib.SqliteWrapper;
37import com.android.messaging.sms.DatabaseMessages;
38import com.android.messaging.sms.DatabaseMessages.LocalDatabaseMessage;
39import com.android.messaging.sms.DatabaseMessages.MmsMessage;
40import com.android.messaging.sms.DatabaseMessages.SmsMessage;
41import com.android.messaging.sms.MmsUtils;
42import com.android.messaging.util.Assert;
43import com.android.messaging.util.BugleGservices;
44import com.android.messaging.util.BugleGservicesKeys;
45import com.android.messaging.util.BuglePrefs;
46import com.android.messaging.util.BuglePrefsKeys;
47import com.android.messaging.util.ContentType;
48import com.android.messaging.util.LogUtil;
49import com.android.messaging.util.OsUtil;
50
51import java.util.ArrayList;
52import java.util.List;
53import java.util.Locale;
54
55/**
56 * Action used to sync messages from smsmms db to local database
57 */
58public 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