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.provider.Telephony.Mms; 23import android.provider.Telephony.Sms; 24import android.support.v4.util.LongSparseArray; 25import android.text.TextUtils; 26 27import com.android.messaging.Factory; 28import com.android.messaging.datamodel.DatabaseHelper; 29import com.android.messaging.datamodel.DatabaseWrapper; 30import com.android.messaging.datamodel.SyncManager; 31import com.android.messaging.datamodel.DatabaseHelper.MessageColumns; 32import com.android.messaging.datamodel.SyncManager.ThreadInfoCache; 33import com.android.messaging.datamodel.data.MessageData; 34import com.android.messaging.mmslib.SqliteWrapper; 35import com.android.messaging.sms.DatabaseMessages; 36import com.android.messaging.sms.DatabaseMessages.DatabaseMessage; 37import com.android.messaging.sms.DatabaseMessages.LocalDatabaseMessage; 38import com.android.messaging.sms.DatabaseMessages.MmsMessage; 39import com.android.messaging.sms.DatabaseMessages.SmsMessage; 40import com.android.messaging.sms.MmsUtils; 41import com.android.messaging.util.Assert; 42import com.android.messaging.util.LogUtil; 43import com.google.common.collect.Sets; 44 45import java.util.ArrayList; 46import java.util.List; 47import java.util.Locale; 48import java.util.Set; 49 50/** 51 * Class holding a pair of cursors - one for local db and one for telephony provider - allowing 52 * synchronous stepping through messages as part of sync. 53 */ 54class SyncCursorPair { 55 private static final String TAG = LogUtil.BUGLE_TAG; 56 57 static final long SYNC_COMPLETE = -1L; 58 static final long SYNC_STARTING = Long.MAX_VALUE; 59 60 private CursorIterator mLocalCursorIterator; 61 private CursorIterator mRemoteCursorsIterator; 62 63 private final String mLocalSelection; 64 private final String mRemoteSmsSelection; 65 private final String mRemoteMmsSelection; 66 67 /** 68 * Check if SMS has been synchronized. We compare the counts of messages on both 69 * sides and return true if they are equal. 70 * 71 * Note that this may not be the most reliable way to tell if messages are in sync. 72 * For example, the local misses one message and has one obsolete message. 73 * However, we have background sms sync once a while, also some other events might 74 * trigger a full sync. So we will eventually catch up. And this should be rare to 75 * happen. 76 * 77 * @return If sms is in sync with telephony sms/mms providers 78 */ 79 static boolean allSynchronized(final DatabaseWrapper db) { 80 return isSynchronized(db, LOCAL_MESSAGES_SELECTION, null, 81 getSmsTypeSelectionSql(), null, getMmsTypeSelectionSql(), null); 82 } 83 84 SyncCursorPair(final long lowerBound, final long upperBound) { 85 mLocalSelection = getTimeConstrainedQuery( 86 LOCAL_MESSAGES_SELECTION, 87 MessageColumns.RECEIVED_TIMESTAMP, 88 lowerBound, 89 upperBound, 90 null /* threadColumn */, null /* threadId */); 91 mRemoteSmsSelection = getTimeConstrainedQuery( 92 getSmsTypeSelectionSql(), 93 "date", 94 lowerBound, 95 upperBound, 96 null /* threadColumn */, null /* threadId */); 97 mRemoteMmsSelection = getTimeConstrainedQuery( 98 getMmsTypeSelectionSql(), 99 "date", 100 ((lowerBound < 0) ? lowerBound : (lowerBound + 999) / 1000), /*seconds*/ 101 ((upperBound < 0) ? upperBound : (upperBound + 999) / 1000), /*seconds*/ 102 null /* threadColumn */, null /* threadId */); 103 } 104 105 SyncCursorPair(final long threadId, final String conversationId) { 106 mLocalSelection = getTimeConstrainedQuery( 107 LOCAL_MESSAGES_SELECTION, 108 MessageColumns.RECEIVED_TIMESTAMP, 109 -1L, 110 -1L, 111 MessageColumns.CONVERSATION_ID, conversationId); 112 // Find all SMS messages (excluding drafts) within the sync window 113 mRemoteSmsSelection = getTimeConstrainedQuery( 114 getSmsTypeSelectionSql(), 115 "date", 116 -1L, 117 -1L, 118 Sms.THREAD_ID, Long.toString(threadId)); 119 mRemoteMmsSelection = getTimeConstrainedQuery( 120 getMmsTypeSelectionSql(), 121 "date", 122 -1L, /*seconds*/ 123 -1L, /*seconds*/ 124 Mms.THREAD_ID, Long.toString(threadId)); 125 } 126 127 void query(final DatabaseWrapper db) { 128 // Load local messages in the sync window 129 mLocalCursorIterator = new LocalCursorIterator(db, mLocalSelection); 130 // Load remote messages in the sync window 131 mRemoteCursorsIterator = new RemoteCursorsIterator(mRemoteSmsSelection, 132 mRemoteMmsSelection); 133 } 134 135 boolean isSynchronized(final DatabaseWrapper db) { 136 return isSynchronized(db, mLocalSelection, null, mRemoteSmsSelection, 137 null, mRemoteMmsSelection, null); 138 } 139 140 void close() { 141 if (mLocalCursorIterator != null) { 142 mLocalCursorIterator.close(); 143 } 144 if (mRemoteCursorsIterator != null) { 145 mRemoteCursorsIterator.close(); 146 } 147 } 148 149 long scan(final int maxMessagesToScan, 150 final int maxMessagesToUpdate, final ArrayList<SmsMessage> smsToAdd, 151 final LongSparseArray<MmsMessage> mmsToAdd, 152 final ArrayList<LocalDatabaseMessage> messagesToDelete, 153 final SyncManager.ThreadInfoCache threadInfoCache) { 154 // Set of local messages matched with the timestamp of a remote message 155 final Set<DatabaseMessage> matchedLocalMessages = Sets.newHashSet(); 156 // Set of remote messages matched with the timestamp of a local message 157 final Set<DatabaseMessage> matchedRemoteMessages = Sets.newHashSet(); 158 long lastTimestampMillis = SYNC_STARTING; 159 // Number of messages scanned local and remote 160 int localCount = 0; 161 int remoteCount = 0; 162 // Seed the initial values of remote and local messages for comparison 163 DatabaseMessage remoteMessage = mRemoteCursorsIterator.next(); 164 DatabaseMessage localMessage = mLocalCursorIterator.next(); 165 // Iterate through messages on both sides in reverse time order 166 // Import messages in remote not in local, delete messages in local not in remote 167 while (localCount + remoteCount < maxMessagesToScan && smsToAdd.size() 168 + mmsToAdd.size() + messagesToDelete.size() < maxMessagesToUpdate) { 169 if (remoteMessage == null && localMessage == null) { 170 // No more message on both sides - scan complete 171 lastTimestampMillis = SYNC_COMPLETE; 172 break; 173 } else if ((remoteMessage == null && localMessage != null) || 174 (localMessage != null && remoteMessage != null && 175 localMessage.getTimestampInMillis() 176 > remoteMessage.getTimestampInMillis())) { 177 // Found a local message that is not in remote db 178 // Delete the local message 179 messagesToDelete.add((LocalDatabaseMessage) localMessage); 180 lastTimestampMillis = Math.min(lastTimestampMillis, 181 localMessage.getTimestampInMillis()); 182 // Advance to next local message 183 localMessage = mLocalCursorIterator.next(); 184 localCount += 1; 185 } else if ((localMessage == null && remoteMessage != null) || 186 (localMessage != null && remoteMessage != null && 187 localMessage.getTimestampInMillis() 188 < remoteMessage.getTimestampInMillis())) { 189 // Found a remote message that is not in local db 190 // Add the remote message 191 saveMessageToAdd(smsToAdd, mmsToAdd, remoteMessage, threadInfoCache); 192 lastTimestampMillis = Math.min(lastTimestampMillis, 193 remoteMessage.getTimestampInMillis()); 194 // Advance to next remote message 195 remoteMessage = mRemoteCursorsIterator.next(); 196 remoteCount += 1; 197 } else { 198 // Found remote and local messages at the same timestamp 199 final long matchedTimestamp = localMessage.getTimestampInMillis(); 200 lastTimestampMillis = Math.min(lastTimestampMillis, matchedTimestamp); 201 // Get the next local and remote messages 202 final DatabaseMessage remoteMessagePeek = mRemoteCursorsIterator.next(); 203 final DatabaseMessage localMessagePeek = mLocalCursorIterator.next(); 204 // Check if only one message on each side matches the current timestamp 205 // by looking at the next messages on both sides. If they are either null 206 // (meaning no more messages) or having a different timestamp. We want 207 // to optimize for this since this is the most common case when majority 208 // of the messages are in sync (so they one-to-one pair up at each timestamp), 209 // by not allocating the data structures required to compare a set of 210 // messages from both sides. 211 if ((remoteMessagePeek == null || 212 remoteMessagePeek.getTimestampInMillis() != matchedTimestamp) && 213 (localMessagePeek == null || 214 localMessagePeek.getTimestampInMillis() != matchedTimestamp)) { 215 // Optimize the common case where only one message on each side 216 // that matches the same timestamp 217 if (!remoteMessage.equals(localMessage)) { 218 // local != remote 219 // Delete local message 220 messagesToDelete.add((LocalDatabaseMessage) localMessage); 221 // Add remote message 222 saveMessageToAdd(smsToAdd, mmsToAdd, remoteMessage, threadInfoCache); 223 } 224 // Get next local and remote messages 225 localMessage = localMessagePeek; 226 remoteMessage = remoteMessagePeek; 227 localCount += 1; 228 remoteCount += 1; 229 } else { 230 // Rare case in which multiple messages are in the same timestamp 231 // on either or both sides 232 // Gather all the matched remote messages 233 matchedRemoteMessages.clear(); 234 matchedRemoteMessages.add(remoteMessage); 235 remoteCount += 1; 236 remoteMessage = remoteMessagePeek; 237 while (remoteMessage != null && 238 remoteMessage.getTimestampInMillis() == matchedTimestamp) { 239 Assert.isTrue(!matchedRemoteMessages.contains(remoteMessage)); 240 matchedRemoteMessages.add(remoteMessage); 241 remoteCount += 1; 242 remoteMessage = mRemoteCursorsIterator.next(); 243 } 244 // Gather all the matched local messages 245 matchedLocalMessages.clear(); 246 matchedLocalMessages.add(localMessage); 247 localCount += 1; 248 localMessage = localMessagePeek; 249 while (localMessage != null && 250 localMessage.getTimestampInMillis() == matchedTimestamp) { 251 if (matchedLocalMessages.contains(localMessage)) { 252 // Duplicate message is local database is deleted 253 messagesToDelete.add((LocalDatabaseMessage) localMessage); 254 } else { 255 matchedLocalMessages.add(localMessage); 256 } 257 localCount += 1; 258 localMessage = mLocalCursorIterator.next(); 259 } 260 // Delete messages local only 261 for (final DatabaseMessage msg : Sets.difference( 262 matchedLocalMessages, matchedRemoteMessages)) { 263 messagesToDelete.add((LocalDatabaseMessage) msg); 264 } 265 // Add messages remote only 266 for (final DatabaseMessage msg : Sets.difference( 267 matchedRemoteMessages, matchedLocalMessages)) { 268 saveMessageToAdd(smsToAdd, mmsToAdd, msg, threadInfoCache); 269 } 270 } 271 } 272 } 273 return lastTimestampMillis; 274 } 275 276 DatabaseMessage getLocalMessage() { 277 return mLocalCursorIterator.next(); 278 } 279 280 DatabaseMessage getRemoteMessage() { 281 return mRemoteCursorsIterator.next(); 282 } 283 284 int getLocalPosition() { 285 return mLocalCursorIterator.getPosition(); 286 } 287 288 int getRemotePosition() { 289 return mRemoteCursorsIterator.getPosition(); 290 } 291 292 int getLocalCount() { 293 return mLocalCursorIterator.getCount(); 294 } 295 296 int getRemoteCount() { 297 return mRemoteCursorsIterator.getCount(); 298 } 299 300 /** 301 * An iterator for a database cursor 302 */ 303 interface CursorIterator { 304 /** 305 * Move to next element in the cursor 306 * 307 * @return The next element (which becomes the current) 308 */ 309 public DatabaseMessage next(); 310 /** 311 * Close the cursor 312 */ 313 public void close(); 314 /** 315 * Get the position 316 */ 317 public int getPosition(); 318 /** 319 * Get the count 320 */ 321 public int getCount(); 322 } 323 324 private static final String ORDER_BY_DATE_DESC = "date DESC"; 325 326 // A subquery that selects SMS/MMS messages in Bugle which are also in telephony 327 private static final String LOCAL_MESSAGES_SELECTION = String.format( 328 Locale.US, 329 "(%s NOTNULL)", 330 MessageColumns.SMS_MESSAGE_URI); 331 332 private static final String ORDER_BY_TIMESTAMP_DESC = 333 MessageColumns.RECEIVED_TIMESTAMP + " DESC"; 334 335 // TODO : This should move into the provider 336 private static class LocalMessageQuery { 337 private static final String[] PROJECTION = new String[] { 338 MessageColumns._ID, 339 MessageColumns.RECEIVED_TIMESTAMP, 340 MessageColumns.SMS_MESSAGE_URI, 341 MessageColumns.PROTOCOL, 342 MessageColumns.CONVERSATION_ID, 343 }; 344 private static final int INDEX_MESSAGE_ID = 0; 345 private static final int INDEX_MESSAGE_TIMESTAMP = 1; 346 private static final int INDEX_SMS_MESSAGE_URI = 2; 347 private static final int INDEX_MESSAGE_SMS_TYPE = 3; 348 private static final int INDEX_CONVERSATION_ID = 4; 349 } 350 351 /** 352 * This class provides the same DatabaseMessage interface over a local SMS db message 353 */ 354 private static LocalDatabaseMessage getLocalDatabaseMessage(final Cursor cursor) { 355 if (cursor == null) { 356 return null; 357 } 358 return new LocalDatabaseMessage( 359 cursor.getLong(LocalMessageQuery.INDEX_MESSAGE_ID), 360 cursor.getInt(LocalMessageQuery.INDEX_MESSAGE_SMS_TYPE), 361 cursor.getString(LocalMessageQuery.INDEX_SMS_MESSAGE_URI), 362 cursor.getLong(LocalMessageQuery.INDEX_MESSAGE_TIMESTAMP), 363 cursor.getString(LocalMessageQuery.INDEX_CONVERSATION_ID)); 364 } 365 366 /** 367 * The buffered cursor iterator for local SMS 368 */ 369 private static class LocalCursorIterator implements CursorIterator { 370 private Cursor mCursor; 371 private final DatabaseWrapper mDatabase; 372 373 LocalCursorIterator(final DatabaseWrapper database, final String selection) 374 throws SQLiteException { 375 mDatabase = database; 376 try { 377 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 378 LogUtil.v(TAG, "SyncCursorPair: Querying for local messages; selection = " 379 + selection); 380 } 381 mCursor = mDatabase.query( 382 DatabaseHelper.MESSAGES_TABLE, 383 LocalMessageQuery.PROJECTION, 384 selection, 385 null /*selectionArgs*/, 386 null/*groupBy*/, 387 null/*having*/, 388 ORDER_BY_TIMESTAMP_DESC); 389 } catch (final SQLiteException e) { 390 LogUtil.e(TAG, "SyncCursorPair: failed to query local sms/mms", e); 391 // Can't query local database. So let's throw up the exception and abort sync 392 // because we may end up import duplicate messages. 393 throw e; 394 } 395 } 396 397 @Override 398 public DatabaseMessage next() { 399 if (mCursor != null && mCursor.moveToNext()) { 400 return getLocalDatabaseMessage(mCursor); 401 } 402 return null; 403 } 404 405 @Override 406 public int getCount() { 407 return (mCursor == null ? 0 : mCursor.getCount()); 408 } 409 410 @Override 411 public int getPosition() { 412 return (mCursor == null ? 0 : mCursor.getPosition()); 413 } 414 415 @Override 416 public void close() { 417 if (mCursor != null) { 418 mCursor.close(); 419 mCursor = null; 420 } 421 } 422 } 423 424 /** 425 * The cursor iterator for remote sms. 426 * Since SMS and MMS are stored in different tables in telephony provider, 427 * this class merges the two cursors and provides a unified view of messages 428 * from both cursors. Note that the order is DESC. 429 */ 430 private static class RemoteCursorsIterator implements CursorIterator { 431 private Cursor mSmsCursor; 432 private Cursor mMmsCursor; 433 private DatabaseMessage mNextSms; 434 private DatabaseMessage mNextMms; 435 436 RemoteCursorsIterator(final String smsSelection, final String mmsSelection) 437 throws SQLiteException { 438 mSmsCursor = null; 439 mMmsCursor = null; 440 try { 441 final Context context = Factory.get().getApplicationContext(); 442 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 443 LogUtil.v(TAG, "SyncCursorPair: Querying for remote SMS; selection = " 444 + smsSelection); 445 } 446 mSmsCursor = SqliteWrapper.query( 447 context, 448 context.getContentResolver(), 449 Sms.CONTENT_URI, 450 SmsMessage.getProjection(), 451 smsSelection, 452 null /* selectionArgs */, 453 ORDER_BY_DATE_DESC); 454 if (mSmsCursor == null) { 455 LogUtil.w(TAG, "SyncCursorPair: Remote SMS query returned null cursor; " 456 + "need to cancel sync"); 457 throw new RuntimeException("Null cursor from remote SMS query"); 458 } 459 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 460 LogUtil.v(TAG, "SyncCursorPair: Querying for remote MMS; selection = " 461 + mmsSelection); 462 } 463 mMmsCursor = SqliteWrapper.query( 464 context, 465 context.getContentResolver(), 466 Mms.CONTENT_URI, 467 DatabaseMessages.MmsMessage.getProjection(), 468 mmsSelection, 469 null /* selectionArgs */, 470 ORDER_BY_DATE_DESC); 471 if (mMmsCursor == null) { 472 LogUtil.w(TAG, "SyncCursorPair: Remote MMS query returned null cursor; " 473 + "need to cancel sync"); 474 throw new RuntimeException("Null cursor from remote MMS query"); 475 } 476 // Move to the first element in the combined stream from both cursors 477 mNextSms = getSmsCursorNext(); 478 mNextMms = getMmsCursorNext(); 479 } catch (final SQLiteException e) { 480 LogUtil.e(TAG, "SyncCursorPair: failed to query remote messages", e); 481 // If we ignore this, the following code would think there is no remote message 482 // and will delete all the local sms. We should be cautious here. So instead, 483 // let's throw the exception to the caller and abort sms sync. We do the same 484 // thing if either of the remote cursors is null. 485 throw e; 486 } 487 } 488 489 @Override 490 public DatabaseMessage next() { 491 DatabaseMessage result = null; 492 if (mNextSms != null && mNextMms != null) { 493 if (mNextSms.getTimestampInMillis() >= mNextMms.getTimestampInMillis()) { 494 result = mNextSms; 495 mNextSms = getSmsCursorNext(); 496 } else { 497 result = mNextMms; 498 mNextMms = getMmsCursorNext(); 499 } 500 } else { 501 if (mNextSms != null) { 502 result = mNextSms; 503 mNextSms = getSmsCursorNext(); 504 } else { 505 result = mNextMms; 506 mNextMms = getMmsCursorNext(); 507 } 508 } 509 return result; 510 } 511 512 private DatabaseMessage getSmsCursorNext() { 513 if (mSmsCursor != null && mSmsCursor.moveToNext()) { 514 return SmsMessage.get(mSmsCursor); 515 } 516 return null; 517 } 518 519 private DatabaseMessage getMmsCursorNext() { 520 if (mMmsCursor != null && mMmsCursor.moveToNext()) { 521 return MmsMessage.get(mMmsCursor); 522 } 523 return null; 524 } 525 526 @Override 527 // Return approximate cursor position allowing for read ahead on two cursors (hence -1) 528 public int getPosition() { 529 return (mSmsCursor == null ? 0 : mSmsCursor.getPosition()) + 530 (mMmsCursor == null ? 0 : mMmsCursor.getPosition()) - 1; 531 } 532 533 @Override 534 public int getCount() { 535 return (mSmsCursor == null ? 0 : mSmsCursor.getCount()) + 536 (mMmsCursor == null ? 0 : mMmsCursor.getCount()); 537 } 538 539 @Override 540 public void close() { 541 if (mSmsCursor != null) { 542 mSmsCursor.close(); 543 mSmsCursor = null; 544 } 545 if (mMmsCursor != null) { 546 mMmsCursor.close(); 547 mMmsCursor = null; 548 } 549 } 550 } 551 552 /** 553 * Type selection for importing sms messages. Only SENT and INBOX messages are imported. 554 * 555 * @return The SQL selection for importing sms messages 556 */ 557 public static String getSmsTypeSelectionSql() { 558 return MmsUtils.getSmsTypeSelectionSql(); 559 } 560 561 /** 562 * Type selection for importing mms messages. 563 * 564 * Criteria: 565 * MESSAGE_BOX is INBOX, SENT or OUTBOX 566 * MESSAGE_TYPE is SEND_REQ (sent), RETRIEVE_CONF (received) or NOTIFICATION_IND (download) 567 * 568 * @return The SQL selection for importing mms messages. This selects the message type, 569 * not including the selection on timestamp. 570 */ 571 public static String getMmsTypeSelectionSql() { 572 return MmsUtils.getMmsTypeSelectionSql(); 573 } 574 575 /** 576 * Get a SQL selection string using an existing selection and time window limits 577 * The limits are not applied if the value is < 0 578 * 579 * @param typeSelection The existing selection 580 * @param from The inclusive lower bound 581 * @param to The exclusive upper bound 582 * @return The created SQL selection 583 */ 584 private static String getTimeConstrainedQuery(final String typeSelection, 585 final String timeColumn, final long from, final long to, 586 final String threadColumn, final String threadId) { 587 final StringBuilder queryBuilder = new StringBuilder(); 588 queryBuilder.append(typeSelection); 589 if (from > 0) { 590 queryBuilder.append(" AND ").append(timeColumn).append(">=").append(from); 591 } 592 if (to > 0) { 593 queryBuilder.append(" AND ").append(timeColumn).append("<").append(to); 594 } 595 if (!TextUtils.isEmpty(threadColumn) && !TextUtils.isEmpty(threadId)) { 596 queryBuilder.append(" AND ").append(threadColumn).append("=").append(threadId); 597 } 598 return queryBuilder.toString(); 599 } 600 601 private static final String[] COUNT_PROJECTION = new String[] { "count()" }; 602 603 private static int getCountFromCursor(final Cursor cursor) { 604 if (cursor != null && cursor.moveToFirst()) { 605 return cursor.getInt(0); 606 } 607 // We should only return a number if we were able to read it from the cursor. 608 // Otherwise, we throw an exception to cancel the sync. 609 String cursorDesc = ""; 610 if (cursor == null) { 611 cursorDesc = "null"; 612 } else if (cursor.getCount() == 0) { 613 cursorDesc = "empty"; 614 } 615 throw new IllegalArgumentException("Cannot get count from " + cursorDesc + " cursor"); 616 } 617 618 private void saveMessageToAdd(final List<SmsMessage> smsToAdd, 619 final LongSparseArray<MmsMessage> mmsToAdd, final DatabaseMessage message, 620 final ThreadInfoCache threadInfoCache) { 621 long threadId; 622 if (message.getProtocol() == MessageData.PROTOCOL_MMS) { 623 final MmsMessage mms = (MmsMessage) message; 624 mmsToAdd.append(mms.getId(), mms); 625 threadId = mms.mThreadId; 626 } else { 627 final SmsMessage sms = (SmsMessage) message; 628 smsToAdd.add(sms); 629 threadId = sms.mThreadId; 630 } 631 // Cache the lookup and canonicalization of the phone number outside of the transaction... 632 threadInfoCache.getThreadRecipients(threadId); 633 } 634 635 /** 636 * Check if SMS has been synchronized. We compare the counts of messages on both 637 * sides and return true if they are equal. 638 * 639 * Note that this may not be the most reliable way to tell if messages are in sync. 640 * For example, the local misses one message and has one obsolete message. 641 * However, we have background sms sync once a while, also some other events might 642 * trigger a full sync. So we will eventually catch up. And this should be rare to 643 * happen. 644 * 645 * @return If sms is in sync with telephony sms/mms providers 646 */ 647 private static boolean isSynchronized(final DatabaseWrapper db, final String localSelection, 648 final String[] localSelectionArgs, final String smsSelection, 649 final String[] smsSelectionArgs, final String mmsSelection, 650 final String[] mmsSelectionArgs) { 651 final Context context = Factory.get().getApplicationContext(); 652 Cursor localCursor = null; 653 Cursor remoteSmsCursor = null; 654 Cursor remoteMmsCursor = null; 655 try { 656 localCursor = db.query( 657 DatabaseHelper.MESSAGES_TABLE, 658 COUNT_PROJECTION, 659 localSelection, 660 localSelectionArgs, 661 null/*groupBy*/, 662 null/*having*/, 663 null/*orderBy*/); 664 final int localCount = getCountFromCursor(localCursor); 665 remoteSmsCursor = SqliteWrapper.query( 666 context, 667 context.getContentResolver(), 668 Sms.CONTENT_URI, 669 COUNT_PROJECTION, 670 smsSelection, 671 smsSelectionArgs, 672 null/*orderBy*/); 673 final int smsCount = getCountFromCursor(remoteSmsCursor); 674 remoteMmsCursor = SqliteWrapper.query( 675 context, 676 context.getContentResolver(), 677 Mms.CONTENT_URI, 678 COUNT_PROJECTION, 679 mmsSelection, 680 mmsSelectionArgs, 681 null/*orderBy*/); 682 final int mmsCount = getCountFromCursor(remoteMmsCursor); 683 final int remoteCount = smsCount + mmsCount; 684 final boolean isInSync = (localCount == remoteCount); 685 if (isInSync) { 686 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { 687 LogUtil.d(TAG, "SyncCursorPair: Same # of local and remote messages = " 688 + localCount); 689 } 690 } else { 691 LogUtil.i(TAG, "SyncCursorPair: Not in sync; # local messages = " + localCount 692 + ", # remote message = " + remoteCount); 693 } 694 return isInSync; 695 } catch (final Exception e) { 696 LogUtil.e(TAG, "SyncCursorPair: failed to query local or remote message counts", e); 697 // If something is wrong in querying database, assume we are synced so 698 // we don't retry indefinitely 699 } finally { 700 if (localCursor != null) { 701 localCursor.close(); 702 } 703 if (remoteSmsCursor != null) { 704 remoteSmsCursor.close(); 705 } 706 if (remoteMmsCursor != null) { 707 remoteMmsCursor.close(); 708 } 709 } 710 return true; 711 } 712} 713