summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/datamodel/SyncManager.java
blob: b3571bf1fda93ede665eed33e934ed8ec261e877 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.messaging.datamodel;

import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
import android.provider.Telephony;
import android.support.v4.util.LongSparseArray;

import com.android.messaging.datamodel.action.SyncMessagesAction;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.Assert;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.BuglePrefs;
import com.android.messaging.util.BuglePrefsKeys;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils;
import com.google.common.collect.Lists;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

/**
 * This class manages message sync with the Telephony SmsProvider/MmsProvider.
 */
public class SyncManager {
    private static final String TAG = LogUtil.BUGLE_TAG;

    /**
     * Record of any user customization to conversation settings
     */
    public static class ConversationCustomization {
        private final boolean mArchived;
        private final boolean mMuted;
        private final boolean mNoVibrate;
        private final String mNotificationSoundUri;

        public ConversationCustomization(final boolean archived, final boolean muted,
                final boolean noVibrate, final String notificationSoundUri) {
            mArchived = archived;
            mMuted = muted;
            mNoVibrate = noVibrate;
            mNotificationSoundUri = notificationSoundUri;
        }

        public boolean isArchived() {
            return mArchived;
        }

        public boolean isMuted() {
            return mMuted;
        }

        public boolean noVibrate() {
            return mNoVibrate;
        }

        public String getNotificationSoundUri() {
            return mNotificationSoundUri;
        }
    }

    SyncManager() {
    }

    /**
     * Timestamp of in progress sync - used to keep track of whether sync is running
     */
    private long mSyncInProgressTimestamp = -1;

    /**
     * Timestamp of current sync batch upper bound - used to determine if message makes batch dirty
     */
    private long mCurrentUpperBoundTimestamp = -1;

    /**
     * Timestamp of messages inserted since sync batch started - used to determine if batch dirty
     */
    private long mMaxRecentChangeTimestamp = -1L;

    private final ThreadInfoCache mThreadInfoCache = new ThreadInfoCache();

    /**
     * User customization to conversations. If this is set, we need to recover them after
     * a full sync.
     */
    private LongSparseArray<ConversationCustomization> mCustomization = null;

    /**
     * Start an incremental sync (backed off a few seconds)
     */
    public static void sync() {
        SyncMessagesAction.sync();
    }

    /**
     * Start an incremental sync (with no backoff)
     */
    public static void immediateSync() {
        SyncMessagesAction.immediateSync();
    }

    /**
     * Start a full sync (for debugging)
     */
    public static void forceSync() {
        SyncMessagesAction.fullSync();
    }

    /**
     * Called from data model thread when starting a sync batch
     * @param upperBoundTimestamp upper bound timestamp for sync batch
     */
    public synchronized void startSyncBatch(final long upperBoundTimestamp) {
        Assert.isTrue(mCurrentUpperBoundTimestamp < 0);
        mCurrentUpperBoundTimestamp = upperBoundTimestamp;
        mMaxRecentChangeTimestamp = -1L;
    }

    /**
     * Called from data model thread at end of batch to determine if any messages added in window
     * @param lowerBoundTimestamp lower bound timestamp for sync batch
     * @return true if message added within window from lower to upper bound timestamp of batch
     */
    public synchronized boolean isBatchDirty(final long lowerBoundTimestamp) {
        Assert.isTrue(mCurrentUpperBoundTimestamp >= 0);
        final long max = mMaxRecentChangeTimestamp;

        final boolean dirty = (max >= 0 && max >= lowerBoundTimestamp);
        if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
            LogUtil.d(TAG, "SyncManager: Sync batch of messages from " + lowerBoundTimestamp
                    + " to " + mCurrentUpperBoundTimestamp + " is "
                    + (dirty ? "DIRTY" : "clean") + "; max change timestamp = "
                    + mMaxRecentChangeTimestamp);
        }

        mCurrentUpperBoundTimestamp = -1L;
        mMaxRecentChangeTimestamp = -1L;

        return dirty;
    }

    /**
     * Called from data model or background worker thread to indicate start of message add process
     * (add must complete on that thread before action transitions to new thread/stage)
     * @param timestamp timestamp of message being added
     */
    public synchronized void onNewMessageInserted(final long timestamp) {
        if (mCurrentUpperBoundTimestamp >= 0 && timestamp <= mCurrentUpperBoundTimestamp) {
            // Message insert in current sync window
            mMaxRecentChangeTimestamp = Math.max(mCurrentUpperBoundTimestamp, timestamp);
            if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
                LogUtil.d(TAG, "SyncManager: New message @ " + timestamp + " before upper bound of "
                        + "current sync batch " + mCurrentUpperBoundTimestamp);
            }
        } else if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
            LogUtil.d(TAG, "SyncManager: New message @ " + timestamp + " after upper bound of "
                    + "current sync batch " + mCurrentUpperBoundTimestamp);
        }
    }

    /**
     * Synchronously checks whether sync is allowed and starts sync if allowed
     * @param full - true indicates a full (not incremental) sync operation
     * @param startTimestamp - starttimestamp for this sync (if allowed)
     * @return - true if sync should start
     */
    public synchronized boolean shouldSync(final boolean full, final long startTimestamp) {
        if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
            LogUtil.v(TAG, "SyncManager: Checking shouldSync " + (full ? "full " : "")
                    + "at " + startTimestamp);
        }

        if (full) {
            final long delayUntilFullSync = delayUntilFullSync(startTimestamp);
            if (delayUntilFullSync > 0) {
                if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
                    LogUtil.d(TAG, "SyncManager: Full sync requested for " + startTimestamp
                            + " delayed for " + delayUntilFullSync + " ms");
                }
                return false;
            }
        }

        if (isSyncing()) {
            if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
                LogUtil.d(TAG, "SyncManager: Not allowed to " + (full ? "full " : "")
                        + "sync yet; still running sync started at " + mSyncInProgressTimestamp);
            }
            return false;
        }
        if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
            LogUtil.d(TAG, "SyncManager: Starting " + (full ? "full " : "") + "sync at "
                    + startTimestamp);
        }

        mSyncInProgressTimestamp = startTimestamp;

        return true;
    }

    /**
     * Return delay (in ms) until allowed to run a full sync (0 meaning can run immediately)
     * @param startTimestamp Timestamp used to start the sync
     * @return 0 if allowed to run now, else delay in ms
     */
    public long delayUntilFullSync(final long startTimestamp) {
        final BugleGservices bugleGservices = BugleGservices.get();
        final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();

        final long lastFullSyncTime = prefs.getLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, -1L);
        final long smsFullSyncBackoffTimeMillis = bugleGservices.getLong(
                BugleGservicesKeys.SMS_FULL_SYNC_BACKOFF_TIME_MILLIS,
                BugleGservicesKeys.SMS_FULL_SYNC_BACKOFF_TIME_MILLIS_DEFAULT);
        final long noFullSyncBefore = (lastFullSyncTime < 0 ? startTimestamp :
            lastFullSyncTime + smsFullSyncBackoffTimeMillis);

        final long delayUntilFullSync = noFullSyncBefore - startTimestamp;
        if (delayUntilFullSync > 0) {
            return delayUntilFullSync;
        }
        return 0;
    }

    /**
     * Check if sync currently in progress (public for asserts/logging).
     */
    public synchronized boolean isSyncing() {
        return (mSyncInProgressTimestamp >= 0);
    }

    /**
     * Check if sync batch should be in progress - compares upperBound with in memory value
     * @param upperBoundTimestamp - upperbound timestamp for sync batch
     * @return - true if timestamps match (otherwise batch is orphan from older process)
     */
    public synchronized boolean isSyncing(final long upperBoundTimestamp) {
        Assert.isTrue(upperBoundTimestamp >= 0);
        return (upperBoundTimestamp == mCurrentUpperBoundTimestamp);
    }

    /**
     * Check if sync has completed for the first time.
     */
    public boolean getHasFirstSyncCompleted() {
        final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
        return prefs.getLong(BuglePrefsKeys.LAST_SYNC_TIME,
                BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT) !=
                BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT;
    }

    /**
     * Called once sync is complete
     */
    public synchronized void complete() {
        if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
            LogUtil.d(TAG, "SyncManager: Sync started at " + mSyncInProgressTimestamp
                    + " marked as complete");
        }
        mSyncInProgressTimestamp = -1L;
        // Conversation customization only used once
        mCustomization = null;
    }

    private final ContentObserver mMmsSmsObserver = new TelephonyMessagesObserver();
    private boolean mSyncOnChanges = false;
    private boolean mNotifyOnChanges = false;

    /**
     * Register content observer when necessary and kick off a catch up sync
     */
    public void updateSyncObserver(final Context context) {
        registerObserver(context);
        // Trigger an sms sync in case we missed and messages before registering this observer or
        // becoming the SMS provider.
        immediateSync();
    }

    private void registerObserver(final Context context) {
        if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
            // Not default SMS app - need to actively monitor telephony but not notify
            mNotifyOnChanges = false;
            mSyncOnChanges = true;
        } else if (OsUtil.isSecondaryUser()){
            // Secondary users default SMS app - need to actively monitor telephony and notify
            mNotifyOnChanges = true;
            mSyncOnChanges = true;
        } else {
            // Primary users default SMS app - don't monitor telephony (most changes from this app)
            mNotifyOnChanges = false;
            mSyncOnChanges = false;
        }
        if (mNotifyOnChanges || mSyncOnChanges) {
            context.getContentResolver().registerContentObserver(Telephony.MmsSms.CONTENT_URI,
                    true, mMmsSmsObserver);
        } else {
            context.getContentResolver().unregisterContentObserver(mMmsSmsObserver);
        }
    }

    public synchronized void setCustomization(
            final LongSparseArray<ConversationCustomization> customization) {
        this.mCustomization = customization;
    }

    public synchronized ConversationCustomization getCustomizationForThread(final long threadId) {
        if (mCustomization != null) {
            return mCustomization.get(threadId);
        }
        return null;
    }

    public static void resetLastSyncTimestamps() {
        final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
        prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME,
                BuglePrefsKeys.LAST_FULL_SYNC_TIME_DEFAULT);
        prefs.putLong(BuglePrefsKeys.LAST_SYNC_TIME, BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT);
    }

    private class TelephonyMessagesObserver extends ContentObserver {
        public TelephonyMessagesObserver() {
            // Just run on default thread
            super(null);
        }

        // Implement the onChange(boolean) method to delegate the change notification to
        // the onChange(boolean, Uri) method to ensure correct operation on older versions
        // of the framework that did not have the onChange(boolean, Uri) method.
        @Override
        public void onChange(final boolean selfChange) {
            onChange(selfChange, null);
        }

        // Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument.
        @Override
        public void onChange(final boolean selfChange, final Uri uri) {
            // Handle change.
            if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
                LogUtil.v(TAG, "SyncManager: Sms/Mms DB changed @" + System.currentTimeMillis()
                        + " for " + (uri == null ? "<unk>" : uri.toString()) + " "
                        + mSyncOnChanges + "/" + mNotifyOnChanges);
            }

            if (mSyncOnChanges) {
                // If sync is already running this will do nothing - but at end of each sync
                // action there is a check for recent messages that should catch new changes.
                SyncManager.immediateSync();
            }
            if (mNotifyOnChanges) {
                // TODO: Secondary users are not going to get notifications
            }
        }
    }

    public ThreadInfoCache getThreadInfoCache() {
        return mThreadInfoCache;
    }

    public static class ThreadInfoCache {
        // Cache of thread->conversationId map
        private final LongSparseArray<String> mThreadToConversationId =
                new LongSparseArray<String>();

        // Cache of thread->recipients map
        private final LongSparseArray<List<String>> mThreadToRecipients =
                new LongSparseArray<List<String>>();

        // Remember the conversation ids that need to be archived
        private final HashSet<String> mArchivedConversations = new HashSet<>();

        public synchronized void clear() {
            if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
                LogUtil.d(TAG, "SyncManager: Cleared ThreadInfoCache");
            }
            mThreadToConversationId.clear();
            mThreadToRecipients.clear();
            mArchivedConversations.clear();
        }

        public synchronized boolean isArchived(final String conversationId) {
            return mArchivedConversations.contains(conversationId);
        }

        /**
         * Get or create a conversation based on the message's thread id
         *
         * @param threadId The message's thread
         * @param refSubId The subId used for normalizing phone numbers in the thread
         * @param customization The user setting customization to the conversation if any
         * @return The existing conversation id or new conversation id
         */
        public synchronized String getOrCreateConversation(final DatabaseWrapper db,
                final long threadId, int refSubId, final ConversationCustomization customization) {
            // This function has several components which need to be atomic.
            Assert.isTrue(db.getDatabase().inTransaction());

            // If we already have this conversation ID in our local map, just return it
            String conversationId = mThreadToConversationId.get(threadId);
            if (conversationId != null) {
                return conversationId;
            }

            final List<String> recipients = getThreadRecipients(threadId);
            final ArrayList<ParticipantData> participants =
                    BugleDatabaseOperations.getConversationParticipantsFromRecipients(recipients,
                            refSubId);

            if (customization != null) {
                // There is user customization we need to recover
                conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
                        customization.isArchived(), participants, customization.isMuted(),
                        customization.noVibrate(), customization.getNotificationSoundUri());
                if (customization.isArchived()) {
                    mArchivedConversations.add(conversationId);
                }
            } else {
                conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
                        false/*archived*/, participants, false/*noNotification*/,
                        false/*noVibrate*/, null/*soundUri*/);
            }

            if (conversationId != null) {
                mThreadToConversationId.put(threadId, conversationId);
                return conversationId;
            }

            return null;
        }


        /**
         * Load the recipients of a thread from telephony provider. If we fail, use
         * a predefined unknown recipient. This should not return null.
         *
         * @param threadId
         */
        public synchronized List<String> getThreadRecipients(final long threadId) {
            List<String> recipients = mThreadToRecipients.get(threadId);
            if (recipients == null) {
                recipients = MmsUtils.getRecipientsByThread(threadId);
                if (recipients != null && recipients.size() > 0) {
                    mThreadToRecipients.put(threadId, recipients);
                }
            }

            if (recipients == null || recipients.isEmpty()) {
                LogUtil.w(TAG, "SyncManager : using unknown sender since thread " + threadId +
                        " couldn't find any recipients.");

                // We want to try our best to load the messages,
                // so if recipient info is broken, try to fix it with unknown recipient
                recipients = Lists.newArrayList();
                recipients.add(ParticipantData.getUnknownSenderDestination());
            }

            return recipients;
        }
    }
}