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