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.provider.Telephony.Mms; 23 import android.provider.Telephony.Sms; 24 import androidx.collection.LongSparseArray; 25 import android.text.TextUtils; 26 27 import com.android.messaging.Factory; 28 import com.android.messaging.datamodel.DatabaseHelper; 29 import com.android.messaging.datamodel.DatabaseWrapper; 30 import com.android.messaging.datamodel.SyncManager; 31 import com.android.messaging.datamodel.DatabaseHelper.MessageColumns; 32 import com.android.messaging.datamodel.SyncManager.ThreadInfoCache; 33 import com.android.messaging.datamodel.data.MessageData; 34 import com.android.messaging.mmslib.SqliteWrapper; 35 import com.android.messaging.sms.DatabaseMessages; 36 import com.android.messaging.sms.DatabaseMessages.DatabaseMessage; 37 import com.android.messaging.sms.DatabaseMessages.LocalDatabaseMessage; 38 import com.android.messaging.sms.DatabaseMessages.MmsMessage; 39 import com.android.messaging.sms.DatabaseMessages.SmsMessage; 40 import com.android.messaging.sms.MmsUtils; 41 import com.android.messaging.util.Assert; 42 import com.android.messaging.util.LogUtil; 43 import com.google.common.collect.Sets; 44 45 import java.util.ArrayList; 46 import java.util.List; 47 import java.util.Locale; 48 import 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 */ 54 class 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 */ allSynchronized(final DatabaseWrapper db)79 static boolean allSynchronized(final DatabaseWrapper db) { 80 return isSynchronized(db, LOCAL_MESSAGES_SELECTION, null, 81 getSmsTypeSelectionSql(), null, getMmsTypeSelectionSql(), null); 82 } 83 SyncCursorPair(final long lowerBound, final long upperBound)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 SyncCursorPair(final long threadId, final String conversationId)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 query(final DatabaseWrapper db)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 isSynchronized(final DatabaseWrapper db)135 boolean isSynchronized(final DatabaseWrapper db) { 136 return isSynchronized(db, mLocalSelection, null, mRemoteSmsSelection, 137 null, mRemoteMmsSelection, null); 138 } 139 close()140 void close() { 141 if (mLocalCursorIterator != null) { 142 mLocalCursorIterator.close(); 143 } 144 if (mRemoteCursorsIterator != null) { 145 mRemoteCursorsIterator.close(); 146 } 147 } 148 scan(final int maxMessagesToScan, final int maxMessagesToUpdate, final ArrayList<SmsMessage> smsToAdd, final LongSparseArray<MmsMessage> mmsToAdd, final ArrayList<LocalDatabaseMessage> messagesToDelete, final SyncManager.ThreadInfoCache threadInfoCache)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 getLocalMessage()276 DatabaseMessage getLocalMessage() { 277 return mLocalCursorIterator.next(); 278 } 279 getRemoteMessage()280 DatabaseMessage getRemoteMessage() { 281 return mRemoteCursorsIterator.next(); 282 } 283 getLocalPosition()284 int getLocalPosition() { 285 return mLocalCursorIterator.getPosition(); 286 } 287 getRemotePosition()288 int getRemotePosition() { 289 return mRemoteCursorsIterator.getPosition(); 290 } 291 getLocalCount()292 int getLocalCount() { 293 return mLocalCursorIterator.getCount(); 294 } 295 getRemoteCount()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 */ next()309 public DatabaseMessage next(); 310 /** 311 * Close the cursor 312 */ close()313 public void close(); 314 /** 315 * Get the position 316 */ getPosition()317 public int getPosition(); 318 /** 319 * Get the count 320 */ getCount()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 */ getLocalDatabaseMessage(final Cursor cursor)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 LocalCursorIterator(final DatabaseWrapper database, final String selection)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 next()398 public DatabaseMessage next() { 399 if (mCursor != null && mCursor.moveToNext()) { 400 return getLocalDatabaseMessage(mCursor); 401 } 402 return null; 403 } 404 405 @Override getCount()406 public int getCount() { 407 return (mCursor == null ? 0 : mCursor.getCount()); 408 } 409 410 @Override getPosition()411 public int getPosition() { 412 return (mCursor == null ? 0 : mCursor.getPosition()); 413 } 414 415 @Override close()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 RemoteCursorsIterator(final String smsSelection, final String mmsSelection)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 next()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 getSmsCursorNext()512 private DatabaseMessage getSmsCursorNext() { 513 if (mSmsCursor != null && mSmsCursor.moveToNext()) { 514 return SmsMessage.get(mSmsCursor); 515 } 516 return null; 517 } 518 getMmsCursorNext()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) getPosition()528 public int getPosition() { 529 return (mSmsCursor == null ? 0 : mSmsCursor.getPosition()) + 530 (mMmsCursor == null ? 0 : mMmsCursor.getPosition()) - 1; 531 } 532 533 @Override getCount()534 public int getCount() { 535 return (mSmsCursor == null ? 0 : mSmsCursor.getCount()) + 536 (mMmsCursor == null ? 0 : mMmsCursor.getCount()); 537 } 538 539 @Override close()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 */ getSmsTypeSelectionSql()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 */ getMmsTypeSelectionSql()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 */ getTimeConstrainedQuery(final String typeSelection, final String timeColumn, final long from, final long to, final String threadColumn, final String threadId)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 getCountFromCursor(final Cursor cursor)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 saveMessageToAdd(final List<SmsMessage> smsToAdd, final LongSparseArray<MmsMessage> mmsToAdd, final DatabaseMessage message, final ThreadInfoCache threadInfoCache)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 */ isSynchronized(final DatabaseWrapper db, final String localSelection, final String[] localSelectionArgs, final String smsSelection, final String[] smsSelectionArgs, final String mmsSelection, final String[] mmsSelectionArgs)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