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