/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.messaging.datamodel.action;

import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.provider.Telephony.Mms;
import androidx.collection.LongSparseArray;

import com.android.messaging.Factory;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.SyncManager;
import com.android.messaging.datamodel.SyncManager.ThreadInfoCache;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.mmslib.SqliteWrapper;
import com.android.messaging.sms.DatabaseMessages;
import com.android.messaging.sms.DatabaseMessages.LocalDatabaseMessage;
import com.android.messaging.sms.DatabaseMessages.MmsMessage;
import com.android.messaging.sms.DatabaseMessages.SmsMessage;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.Assert;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.BuglePrefs;
import com.android.messaging.util.BuglePrefsKeys;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**
 * Action used to sync messages from smsmms db to local database
 */
public class SyncMessagesAction extends Action implements Parcelable {
    static final long SYNC_FAILED = Long.MIN_VALUE;

    private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;

    private static final String KEY_START_TIMESTAMP = "start_timestamp";
    private static final String KEY_MAX_UPDATE = "max_update";
    private static final String KEY_LOWER_BOUND = "lower_bound";
    private static final String KEY_UPPER_BOUND = "upper_bound";
    private static final String BUNDLE_KEY_LAST_TIMESTAMP = "last_timestamp";
    private static final String BUNDLE_KEY_SMS_MESSAGES = "sms_to_add";
    private static final String BUNDLE_KEY_MMS_MESSAGES = "mms_to_add";
    private static final String BUNDLE_KEY_MESSAGES_TO_DELETE = "messages_to_delete";

    /**
     * Start a full sync (backed off a few seconds to avoid pulling sending/receiving messages).
     */
    public static void fullSync() {
        final BugleGservices bugleGservices = BugleGservices.get();
        final long smsSyncBackoffTimeMillis = bugleGservices.getLong(
                BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS,
                BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS_DEFAULT);

        final long now = System.currentTimeMillis();
        // TODO: Could base this off most recent message in db but now should be okay...
        final long startTimestamp = now - smsSyncBackoffTimeMillis;

        final SyncMessagesAction action = new SyncMessagesAction(-1L, startTimestamp,
                0, startTimestamp);
        action.start();
    }

    /**
     * Start an incremental sync to pull messages since last sync (backed off a few seconds)..
     */
    public static void sync() {
        final BugleGservices bugleGservices = BugleGservices.get();
        final long smsSyncBackoffTimeMillis = bugleGservices.getLong(
                BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS,
                BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS_DEFAULT);

        final long now = System.currentTimeMillis();
        // TODO: Could base this off most recent message in db but now should be okay...
        final long startTimestamp = now - smsSyncBackoffTimeMillis;

        sync(startTimestamp);
    }

    /**
     * Start an incremental sync when the application starts up (no back off as not yet
     *  sending/receiving).
     */
    public static void immediateSync() {
        final long now = System.currentTimeMillis();
        // TODO: Could base this off most recent message in db but now should be okay...
        final long startTimestamp = now;

        sync(startTimestamp);
    }

    private static void sync(final long startTimestamp) {
        if (!OsUtil.hasSmsPermission()) {
            // Sync requires READ_SMS permission
            return;
        }

        final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
        // Lower bound is end of previous sync
        final long syncLowerBoundTimeMillis = prefs.getLong(BuglePrefsKeys.LAST_SYNC_TIME,
                    BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT);

        final SyncMessagesAction action = new SyncMessagesAction(syncLowerBoundTimeMillis,
                startTimestamp, 0, startTimestamp);
        action.start();
    }

    private SyncMessagesAction(final long lowerBound, final long upperBound,
            final int maxMessagesToUpdate, final long startTimestamp) {
        actionParameters.putLong(KEY_LOWER_BOUND, lowerBound);
        actionParameters.putLong(KEY_UPPER_BOUND, upperBound);
        actionParameters.putInt(KEY_MAX_UPDATE, maxMessagesToUpdate);
        actionParameters.putLong(KEY_START_TIMESTAMP, startTimestamp);
    }

    @Override
    protected Object executeAction() {
        final DatabaseWrapper db = DataModel.get().getDatabase();

        long lowerBoundTimeMillis = actionParameters.getLong(KEY_LOWER_BOUND);
        final long upperBoundTimeMillis = actionParameters.getLong(KEY_UPPER_BOUND);
        final int initialMaxMessagesToUpdate = actionParameters.getInt(KEY_MAX_UPDATE);
        final long startTimestamp = actionParameters.getLong(KEY_START_TIMESTAMP);

        if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
            LogUtil.d(TAG, "SyncMessagesAction: Request to sync messages from "
                    + lowerBoundTimeMillis + " to " + upperBoundTimeMillis + " (start timestamp = "
                    + startTimestamp + ", message update limit = " + initialMaxMessagesToUpdate
                    + ")");
        }

        final SyncManager syncManager = DataModel.get().getSyncManager();
        if (lowerBoundTimeMillis >= 0) {
            // Cursors
            final SyncCursorPair cursors = new SyncCursorPair(-1L, lowerBoundTimeMillis);
            final boolean inSync = cursors.isSynchronized(db);
            if (!inSync) {
                if (syncManager.delayUntilFullSync(startTimestamp) == 0) {
                    lowerBoundTimeMillis = -1;
                    actionParameters.putLong(KEY_LOWER_BOUND, lowerBoundTimeMillis);

                    if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
                        LogUtil.d(TAG, "SyncMessagesAction: Messages before "
                                + lowerBoundTimeMillis + " not in sync; promoting to full sync");
                    }
                } else if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
                    LogUtil.d(TAG, "SyncMessagesAction: Messages before "
                            + lowerBoundTimeMillis + " not in sync; will do incremental sync");
                }
            } else {
                if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
                    LogUtil.d(TAG, "SyncMessagesAction: Messages before " + lowerBoundTimeMillis
                            + " are in sync");
                }
            }
        }

        // Check if sync allowed (can be too soon after last or one is already running)
        if (syncManager.shouldSync(lowerBoundTimeMillis < 0, startTimestamp)) {
            syncManager.startSyncBatch(upperBoundTimeMillis);
            requestBackgroundWork();
        }

        return null;
    }

    @Override
    protected Bundle doBackgroundWork() {
        final BugleGservices bugleGservices = BugleGservices.get();
        final DatabaseWrapper db = DataModel.get().getDatabase();

        final int maxMessagesToScan = bugleGservices.getInt(
                BugleGservicesKeys.SMS_SYNC_BATCH_MAX_MESSAGES_TO_SCAN,
                BugleGservicesKeys.SMS_SYNC_BATCH_MAX_MESSAGES_TO_SCAN_DEFAULT);

        final int initialMaxMessagesToUpdate = actionParameters.getInt(KEY_MAX_UPDATE);
        final int smsSyncSubsequentBatchSizeMin = bugleGservices.getInt(
                BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MIN,
                BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MIN_DEFAULT);
        final int smsSyncSubsequentBatchSizeMax = bugleGservices.getInt(
                BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MAX,
                BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MAX_DEFAULT);

        // Cap sync size to GServices limits
        final int maxMessagesToUpdate = Math.max(smsSyncSubsequentBatchSizeMin,
                Math.min(initialMaxMessagesToUpdate, smsSyncSubsequentBatchSizeMax));

        final long lowerBoundTimeMillis = actionParameters.getLong(KEY_LOWER_BOUND);
        final long upperBoundTimeMillis = actionParameters.getLong(KEY_UPPER_BOUND);

        LogUtil.i(TAG, "SyncMessagesAction: Starting batch for messages from "
                + lowerBoundTimeMillis + " to " + upperBoundTimeMillis
                + " (message update limit = " + maxMessagesToUpdate + ", message scan limit = "
                + maxMessagesToScan + ")");

        // Clear last change time so that we can work out if this batch is dirty when it completes
        final SyncManager syncManager = DataModel.get().getSyncManager();

        // Clear the singleton cache that maps threads to recipients and to conversations.
        final SyncManager.ThreadInfoCache cache = syncManager.getThreadInfoCache();
        cache.clear();

        // Sms messages to store
        final ArrayList<SmsMessage> smsToAdd = new ArrayList<SmsMessage>();
        // Mms messages to store
        final LongSparseArray<MmsMessage> mmsToAdd = new LongSparseArray<MmsMessage>();
        // List of local SMS/MMS to remove
        final ArrayList<LocalDatabaseMessage> messagesToDelete =
                new ArrayList<LocalDatabaseMessage>();

        long lastTimestampMillis = SYNC_FAILED;
        if (syncManager.isSyncing(upperBoundTimeMillis)) {
            // Cursors
            final SyncCursorPair cursors = new SyncCursorPair(lowerBoundTimeMillis,
                    upperBoundTimeMillis);

            // Actually compare the messages using cursor pair
            lastTimestampMillis = syncCursorPair(db, cursors, smsToAdd, mmsToAdd,
                    messagesToDelete, maxMessagesToScan, maxMessagesToUpdate, cache);
        }
        final Bundle response = new Bundle();

        // If comparison succeeds bundle up the changes for processing in ActionService
        if (lastTimestampMillis > SYNC_FAILED) {
            final ArrayList<MmsMessage> mmsToAddList = new ArrayList<MmsMessage>();
            for (int i = 0; i < mmsToAdd.size(); i++) {
                final MmsMessage mms = mmsToAdd.valueAt(i);
                mmsToAddList.add(mms);
            }

            response.putParcelableArrayList(BUNDLE_KEY_SMS_MESSAGES, smsToAdd);
            response.putParcelableArrayList(BUNDLE_KEY_MMS_MESSAGES, mmsToAddList);
            response.putParcelableArrayList(BUNDLE_KEY_MESSAGES_TO_DELETE, messagesToDelete);
        }
        response.putLong(BUNDLE_KEY_LAST_TIMESTAMP, lastTimestampMillis);

        return response;
    }

    /**
     * Compare messages based on timestamp and uri
     * @param db local database wrapper
     * @param cursors cursor pair holding references to local and remote messages
     * @param smsToAdd newly found sms messages to add
     * @param mmsToAdd newly found mms messages to add
     * @param messagesToDelete messages not found needing deletion
     * @param maxMessagesToScan max messages to scan for changes
     * @param maxMessagesToUpdate max messages to return for updates
     * @param cache cache for conversation id / thread id / recipient set mapping
     * @return timestamp of the oldest message seen during the sync scan
     */
    private long 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) {
        long lastTimestampMillis;
        final long startTimeMillis = SystemClock.elapsedRealtime();

        // Number of messages scanned local and remote
        int localPos = 0;
        int remotePos = 0;
        int localTotal = 0;
        int remoteTotal = 0;
        // Scan through the messages on both sides and prepare messages for local message table
        // changes (including adding and deleting)
        try {
            cursors.query(db);

            localTotal = cursors.getLocalCount();
            remoteTotal = cursors.getRemoteCount();

            if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
                LogUtil.d(TAG, "SyncMessagesAction: Scanning cursors (local count = " + localTotal
                        + ", remote count = " + remoteTotal + ", message update limit = "
                        + maxMessagesToUpdate + ", message scan limit = " + maxMessagesToScan
                        + ")");
            }

            lastTimestampMillis = cursors.scan(maxMessagesToScan, maxMessagesToUpdate,
                    smsToAdd, mmsToAdd, messagesToDelete, cache);

            localPos = cursors.getLocalPosition();
            remotePos = cursors.getRemotePosition();

            if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
                LogUtil.d(TAG, "SyncMessagesAction: Scanned cursors (local position = " + localPos
                        + " of " + localTotal + ", remote position = " + remotePos + " of "
                        + remoteTotal + ")");
            }

            // Batch loading the parts of the MMS messages in this batch
            loadMmsParts(mmsToAdd);
            // Lookup senders for incoming mms messages
            setMmsSenders(mmsToAdd, cache);
        } catch (final SQLiteException e) {
            LogUtil.e(TAG, "SyncMessagesAction: Database exception", e);
            // Let's abort
            lastTimestampMillis = SYNC_FAILED;
        } catch (final Exception e) {
            // We want to catch anything unexpected since this is running in a separate thread
            // and any unexpected exception will just fail this thread silently.
            // Let's crash for dogfooders!
            LogUtil.wtf(TAG, "SyncMessagesAction: unexpected failure in scan", e);
            lastTimestampMillis = SYNC_FAILED;
        } finally {
            if (cursors != null) {
                cursors.close();
            }
        }

        final long endTimeMillis = SystemClock.elapsedRealtime();

        if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
            LogUtil.d(TAG, "SyncMessagesAction: Scan complete (took "
                    + (endTimeMillis - startTimeMillis) + " ms). " + smsToAdd.size()
                    + " remote SMS to add, " + mmsToAdd.size() + " MMS to add, "
                    + messagesToDelete.size() + " local messages to delete. "
                    + "Oldest timestamp seen = " + lastTimestampMillis);
        }

        return lastTimestampMillis;
    }

    /**
     * Perform local database updates and schedule follow on sync actions
     */
    @Override
    protected Object processBackgroundResponse(final Bundle response) {
        final long lastTimestampMillis = response.getLong(BUNDLE_KEY_LAST_TIMESTAMP);
        final long lowerBoundTimeMillis = actionParameters.getLong(KEY_LOWER_BOUND);
        final long upperBoundTimeMillis = actionParameters.getLong(KEY_UPPER_BOUND);
        final int maxMessagesToUpdate = actionParameters.getInt(KEY_MAX_UPDATE);
        final long startTimestamp = actionParameters.getLong(KEY_START_TIMESTAMP);

        // Check with the sync manager if any conflicting updates have been made to databases
        final SyncManager syncManager = DataModel.get().getSyncManager();
        final boolean orphan = !syncManager.isSyncing(upperBoundTimeMillis);

        // lastTimestampMillis used to indicate failure
        if (orphan) {
            // This batch does not match current in progress timestamp.
            LogUtil.w(TAG, "SyncMessagesAction: Ignoring orphan sync batch for messages from "
                    + lowerBoundTimeMillis + " to " + upperBoundTimeMillis);
        } else {
            final boolean dirty = syncManager.isBatchDirty(lastTimestampMillis);
            if (lastTimestampMillis == SYNC_FAILED) {
                LogUtil.e(TAG, "SyncMessagesAction: Sync failed - terminating");

                // Failed - update last sync times to throttle our failure rate
                final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
                // Save sync completion time so next sync will start from here
                prefs.putLong(BuglePrefsKeys.LAST_SYNC_TIME, startTimestamp);
                // Remember last full sync so that don't start background full sync right away
                prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, startTimestamp);

                syncManager.complete();
            } else if (dirty) {
                LogUtil.w(TAG, "SyncMessagesAction: Redoing dirty sync batch of messages from "
                        + lowerBoundTimeMillis + " to " + upperBoundTimeMillis);

                // Redo this batch
                final SyncMessagesAction nextBatch =
                        new SyncMessagesAction(lowerBoundTimeMillis, upperBoundTimeMillis,
                                maxMessagesToUpdate, startTimestamp);

                syncManager.startSyncBatch(upperBoundTimeMillis);
                requestBackgroundWork(nextBatch);
            } else {
                // Succeeded
                final ArrayList<SmsMessage> smsToAdd =
                        response.getParcelableArrayList(BUNDLE_KEY_SMS_MESSAGES);
                final ArrayList<MmsMessage> mmsToAdd =
                        response.getParcelableArrayList(BUNDLE_KEY_MMS_MESSAGES);
                final ArrayList<LocalDatabaseMessage> messagesToDelete =
                        response.getParcelableArrayList(BUNDLE_KEY_MESSAGES_TO_DELETE);

                final int messagesUpdated = smsToAdd.size() + mmsToAdd.size()
                        + messagesToDelete.size();

                // Perform local database changes in one transaction
                long txnTimeMillis = 0;
                if (messagesUpdated > 0) {
                    final long startTimeMillis = SystemClock.elapsedRealtime();
                    final SyncMessageBatch batch = new SyncMessageBatch(smsToAdd, mmsToAdd,
                            messagesToDelete, syncManager.getThreadInfoCache());
                    batch.updateLocalDatabase();
                    final long endTimeMillis = SystemClock.elapsedRealtime();
                    txnTimeMillis = endTimeMillis - startTimeMillis;

                    LogUtil.i(TAG, "SyncMessagesAction: Updated local database "
                            + "(took " + txnTimeMillis + " ms). Added "
                            + smsToAdd.size() + " SMS, added " + mmsToAdd.size() + " MMS, deleted "
                            + messagesToDelete.size() + " messages.");

                    // TODO: Investigate whether we can make this more fine-grained.
                    MessagingContentProvider.notifyEverythingChanged();
                } else {
                    if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
                        LogUtil.d(TAG, "SyncMessagesAction: No local database updates to make");
                    }

                    if (!syncManager.getHasFirstSyncCompleted()) {
                        // If we have never completed a sync before (fresh install) and there are
                        // no messages, still inform the UI of a change so it can update syncing
                        // messages shown to the user
                        MessagingContentProvider.notifyConversationListChanged();
                        MessagingContentProvider.notifyPartsChanged();
                    }
                }
                // Determine if there are more messages that need to be scanned
                if (lastTimestampMillis >= 0 && lastTimestampMillis >= lowerBoundTimeMillis) {
                    if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
                        LogUtil.d(TAG, "SyncMessagesAction: More messages to sync; scheduling next "
                                + "sync batch now.");
                    }

                    // Include final millisecond of last sync in next sync
                    final long newUpperBoundTimeMillis = lastTimestampMillis + 1;
                    final int newMaxMessagesToUpdate = nextBatchSize(messagesUpdated,
                            txnTimeMillis);

                    final SyncMessagesAction nextBatch =
                            new SyncMessagesAction(lowerBoundTimeMillis, newUpperBoundTimeMillis,
                                    newMaxMessagesToUpdate, startTimestamp);

                    // Proceed with next batch
                    syncManager.startSyncBatch(newUpperBoundTimeMillis);
                    requestBackgroundWork(nextBatch);
                } else {
                    final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
                    // Save sync completion time so next sync will start from here
                    prefs.putLong(BuglePrefsKeys.LAST_SYNC_TIME, startTimestamp);
                    if (lowerBoundTimeMillis < 0) {
                        // Remember last full sync so that don't start another full sync right away
                        prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, startTimestamp);
                    }

                    final long now = System.currentTimeMillis();

                    // After any sync check if new messages have arrived
                    final SyncCursorPair recents = new SyncCursorPair(startTimestamp, now);
                    final SyncCursorPair olders = new SyncCursorPair(-1L, startTimestamp);
                    final DatabaseWrapper db = DataModel.get().getDatabase();
                    if (!recents.isSynchronized(db)) {
                        LogUtil.i(TAG, "SyncMessagesAction: Changed messages after sync; "
                                + "scheduling an incremental sync now.");

                        // Just add a new batch for recent messages
                        final SyncMessagesAction nextBatch =
                                new SyncMessagesAction(startTimestamp, now, 0, startTimestamp);
                        syncManager.startSyncBatch(now);
                        requestBackgroundWork(nextBatch);
                        // After partial sync verify sync state
                    } else if (lowerBoundTimeMillis >= 0 && !olders.isSynchronized(db)) {
                        // Add a batch going back to start of time
                        LogUtil.w(TAG, "SyncMessagesAction: Changed messages before sync batch; "
                                + "scheduling a full sync now.");

                        final SyncMessagesAction nextBatch =
                                new SyncMessagesAction(-1L, startTimestamp, 0, startTimestamp);

                        syncManager.startSyncBatch(startTimestamp);
                        requestBackgroundWork(nextBatch);
                    } else {
                        LogUtil.i(TAG, "SyncMessagesAction: All messages now in sync");

                        // All done, in sync
                        syncManager.complete();
                    }
                }
                // Either sync should be complete or we should have a follow up request
                Assert.isTrue(hasBackgroundActions() || !syncManager.isSyncing());
            }
        }

        return null;
    }

    /**
     * Decide the next batch size based on the stats we collected with past batch
     * @param messagesUpdated number of messages updated in this batch
     * @param txnTimeMillis time the transaction took in ms
     * @return Target number of messages to sync for next batch
     */
    private static int nextBatchSize(final int messagesUpdated, final long txnTimeMillis) {
        final BugleGservices bugleGservices = BugleGservices.get();
        final long smsSyncSubsequentBatchTimeLimitMillis = bugleGservices.getLong(
                BugleGservicesKeys.SMS_SYNC_BATCH_TIME_LIMIT_MILLIS,
                BugleGservicesKeys.SMS_SYNC_BATCH_TIME_LIMIT_MILLIS_DEFAULT);

        if (txnTimeMillis <= 0) {
            return 0;
        }
        // Number of messages we can sync within the batch time limit using
        // the average sync time calculated based on the stats we collected
        // in previous batch
        return (int) ((double) (messagesUpdated) / (double) txnTimeMillis
                        * smsSyncSubsequentBatchTimeLimitMillis);
    }

    /**
     * Batch loading MMS parts for the messages in current batch
     */
    private void loadMmsParts(final LongSparseArray<MmsMessage> mmses) {
        final Context context = Factory.get().getApplicationContext();
        final int totalIds = mmses.size();
        for (int start = 0; start < totalIds; start += MmsUtils.MAX_IDS_PER_QUERY) {
            final int end = Math.min(start + MmsUtils.MAX_IDS_PER_QUERY, totalIds); //excluding
            final int count = end - start;
            final String batchSelection = String.format(
                    Locale.US,
                    "%s != '%s' AND %s IN %s",
                    Mms.Part.CONTENT_TYPE,
                    ContentType.APP_SMIL,
                    Mms.Part.MSG_ID,
                    MmsUtils.getSqlInOperand(count));
            final String[] batchSelectionArgs = new String[count];
            for (int i = 0; i < count; i++) {
                batchSelectionArgs[i] = Long.toString(mmses.valueAt(start + i).getId());
            }
            final Cursor cursor = SqliteWrapper.query(
                    context,
                    context.getContentResolver(),
                    MmsUtils.MMS_PART_CONTENT_URI,
                    DatabaseMessages.MmsPart.PROJECTION,
                    batchSelection,
                    batchSelectionArgs,
                    null/*sortOrder*/);
            if (cursor != null) {
                try {
                    while (cursor.moveToNext()) {
                        // Delay loading the media content for parsing for efficiency
                        // TODO: load the media and fill in the dimensions when
                        // we actually display it
                        final DatabaseMessages.MmsPart part =
                                DatabaseMessages.MmsPart.get(cursor, false/*loadMedia*/);
                        final DatabaseMessages.MmsMessage mms = mmses.get(part.mMessageId);
                        if (mms != null) {
                            mms.addPart(part);
                        }
                    }
                } finally {
                    cursor.close();
                }
            }
        }
    }

    /**
     * Batch loading MMS sender for the messages in current batch
     */
    private void setMmsSenders(final LongSparseArray<MmsMessage> mmses,
            final ThreadInfoCache cache) {
        // Store all the MMS messages
        for (int i = 0; i < mmses.size(); i++) {
            final MmsMessage mms = mmses.valueAt(i);

            final boolean isOutgoing = mms.mType != Mms.MESSAGE_BOX_INBOX;
            String senderId = null;
            if (!isOutgoing) {
                // We only need to find out sender phone number for received message
                senderId = getMmsSender(mms, cache);
                if (senderId == null) {
                    LogUtil.w(TAG, "SyncMessagesAction: Could not find sender of incoming MMS "
                            + "message " + mms.getUri() + "; using 'unknown sender' instead");
                    senderId = ParticipantData.getUnknownSenderDestination();
                }
            }
            mms.setSender(senderId);
        }
    }

    /**
     * Find out the sender of an MMS message
     */
    private String getMmsSender(final MmsMessage mms, final ThreadInfoCache cache) {
        final List<String> recipients = cache.getThreadRecipients(mms.mThreadId);
        Assert.notNull(recipients);
        Assert.isTrue(recipients.size() > 0);

        if (recipients.size() == 1
                && recipients.get(0).equals(ParticipantData.getUnknownSenderDestination())) {
            LogUtil.w(TAG, "SyncMessagesAction: MMS message " + mms.mUri + " has unknown sender "
                    + "(thread id = " + mms.mThreadId + ")");
        }

        return MmsUtils.getMmsSender(recipients, mms.mUri);
    }

    private SyncMessagesAction(final Parcel in) {
        super(in);
    }

    public static final Parcelable.Creator<SyncMessagesAction> CREATOR
            = new Parcelable.Creator<SyncMessagesAction>() {
        @Override
        public SyncMessagesAction createFromParcel(final Parcel in) {
            return new SyncMessagesAction(in);
        }

        @Override
        public SyncMessagesAction[] newArray(final int size) {
            return new SyncMessagesAction[size];
        }
    };

    @Override
    public void writeToParcel(final Parcel parcel, final int flags) {
        writeActionToParcel(parcel, flags);
    }
}
