| /* |
| * 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 android.support.v4.util.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); |
| } |
| } |