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