Home | History | Annotate | Download | only in action
      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 android.support.v4.util.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      */
     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