Home | History | Annotate | Download | only in datamodel
      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;
     18 
     19 import android.content.Context;
     20 import android.database.ContentObserver;
     21 import android.net.Uri;
     22 import android.provider.Telephony;
     23 import android.support.v4.util.LongSparseArray;
     24 
     25 import com.android.messaging.datamodel.action.SyncMessagesAction;
     26 import com.android.messaging.datamodel.data.ParticipantData;
     27 import com.android.messaging.sms.MmsUtils;
     28 import com.android.messaging.util.Assert;
     29 import com.android.messaging.util.BugleGservices;
     30 import com.android.messaging.util.BugleGservicesKeys;
     31 import com.android.messaging.util.BuglePrefs;
     32 import com.android.messaging.util.BuglePrefsKeys;
     33 import com.android.messaging.util.LogUtil;
     34 import com.android.messaging.util.OsUtil;
     35 import com.android.messaging.util.PhoneUtils;
     36 import com.google.common.collect.Lists;
     37 
     38 import java.util.ArrayList;
     39 import java.util.HashSet;
     40 import java.util.List;
     41 
     42 /**
     43  * This class manages message sync with the Telephony SmsProvider/MmsProvider.
     44  */
     45 public class SyncManager {
     46     private static final String TAG = LogUtil.BUGLE_TAG;
     47 
     48     /**
     49      * Record of any user customization to conversation settings
     50      */
     51     public static class ConversationCustomization {
     52         private final boolean mArchived;
     53         private final boolean mMuted;
     54         private final boolean mNoVibrate;
     55         private final String mNotificationSoundUri;
     56 
     57         public ConversationCustomization(final boolean archived, final boolean muted,
     58                 final boolean noVibrate, final String notificationSoundUri) {
     59             mArchived = archived;
     60             mMuted = muted;
     61             mNoVibrate = noVibrate;
     62             mNotificationSoundUri = notificationSoundUri;
     63         }
     64 
     65         public boolean isArchived() {
     66             return mArchived;
     67         }
     68 
     69         public boolean isMuted() {
     70             return mMuted;
     71         }
     72 
     73         public boolean noVibrate() {
     74             return mNoVibrate;
     75         }
     76 
     77         public String getNotificationSoundUri() {
     78             return mNotificationSoundUri;
     79         }
     80     }
     81 
     82     SyncManager() {
     83     }
     84 
     85     /**
     86      * Timestamp of in progress sync - used to keep track of whether sync is running
     87      */
     88     private long mSyncInProgressTimestamp = -1;
     89 
     90     /**
     91      * Timestamp of current sync batch upper bound - used to determine if message makes batch dirty
     92      */
     93     private long mCurrentUpperBoundTimestamp = -1;
     94 
     95     /**
     96      * Timestamp of messages inserted since sync batch started - used to determine if batch dirty
     97      */
     98     private long mMaxRecentChangeTimestamp = -1L;
     99 
    100     private final ThreadInfoCache mThreadInfoCache = new ThreadInfoCache();
    101 
    102     /**
    103      * User customization to conversations. If this is set, we need to recover them after
    104      * a full sync.
    105      */
    106     private LongSparseArray<ConversationCustomization> mCustomization = null;
    107 
    108     /**
    109      * Start an incremental sync (backed off a few seconds)
    110      */
    111     public static void sync() {
    112         SyncMessagesAction.sync();
    113     }
    114 
    115     /**
    116      * Start an incremental sync (with no backoff)
    117      */
    118     public static void immediateSync() {
    119         SyncMessagesAction.immediateSync();
    120     }
    121 
    122     /**
    123      * Start a full sync (for debugging)
    124      */
    125     public static void forceSync() {
    126         SyncMessagesAction.fullSync();
    127     }
    128 
    129     /**
    130      * Called from data model thread when starting a sync batch
    131      * @param upperBoundTimestamp upper bound timestamp for sync batch
    132      */
    133     public synchronized void startSyncBatch(final long upperBoundTimestamp) {
    134         Assert.isTrue(mCurrentUpperBoundTimestamp < 0);
    135         mCurrentUpperBoundTimestamp = upperBoundTimestamp;
    136         mMaxRecentChangeTimestamp = -1L;
    137     }
    138 
    139     /**
    140      * Called from data model thread at end of batch to determine if any messages added in window
    141      * @param lowerBoundTimestamp lower bound timestamp for sync batch
    142      * @return true if message added within window from lower to upper bound timestamp of batch
    143      */
    144     public synchronized boolean isBatchDirty(final long lowerBoundTimestamp) {
    145         Assert.isTrue(mCurrentUpperBoundTimestamp >= 0);
    146         final long max = mMaxRecentChangeTimestamp;
    147 
    148         final boolean dirty = (max >= 0 && max >= lowerBoundTimestamp);
    149         if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    150             LogUtil.d(TAG, "SyncManager: Sync batch of messages from " + lowerBoundTimestamp
    151                     + " to " + mCurrentUpperBoundTimestamp + " is "
    152                     + (dirty ? "DIRTY" : "clean") + "; max change timestamp = "
    153                     + mMaxRecentChangeTimestamp);
    154         }
    155 
    156         mCurrentUpperBoundTimestamp = -1L;
    157         mMaxRecentChangeTimestamp = -1L;
    158 
    159         return dirty;
    160     }
    161 
    162     /**
    163      * Called from data model or background worker thread to indicate start of message add process
    164      * (add must complete on that thread before action transitions to new thread/stage)
    165      * @param timestamp timestamp of message being added
    166      */
    167     public synchronized void onNewMessageInserted(final long timestamp) {
    168         if (mCurrentUpperBoundTimestamp >= 0 && timestamp <= mCurrentUpperBoundTimestamp) {
    169             // Message insert in current sync window
    170             mMaxRecentChangeTimestamp = Math.max(mCurrentUpperBoundTimestamp, timestamp);
    171             if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    172                 LogUtil.d(TAG, "SyncManager: New message @ " + timestamp + " before upper bound of "
    173                         + "current sync batch " + mCurrentUpperBoundTimestamp);
    174             }
    175         } else if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    176             LogUtil.d(TAG, "SyncManager: New message @ " + timestamp + " after upper bound of "
    177                     + "current sync batch " + mCurrentUpperBoundTimestamp);
    178         }
    179     }
    180 
    181     /**
    182      * Synchronously checks whether sync is allowed and starts sync if allowed
    183      * @param full - true indicates a full (not incremental) sync operation
    184      * @param startTimestamp - starttimestamp for this sync (if allowed)
    185      * @return - true if sync should start
    186      */
    187     public synchronized boolean shouldSync(final boolean full, final long startTimestamp) {
    188         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    189             LogUtil.v(TAG, "SyncManager: Checking shouldSync " + (full ? "full " : "")
    190                     + "at " + startTimestamp);
    191         }
    192 
    193         if (full) {
    194             final long delayUntilFullSync = delayUntilFullSync(startTimestamp);
    195             if (delayUntilFullSync > 0) {
    196                 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    197                     LogUtil.d(TAG, "SyncManager: Full sync requested for " + startTimestamp
    198                             + " delayed for " + delayUntilFullSync + " ms");
    199                 }
    200                 return false;
    201             }
    202         }
    203 
    204         if (isSyncing()) {
    205             if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    206                 LogUtil.d(TAG, "SyncManager: Not allowed to " + (full ? "full " : "")
    207                         + "sync yet; still running sync started at " + mSyncInProgressTimestamp);
    208             }
    209             return false;
    210         }
    211         if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    212             LogUtil.d(TAG, "SyncManager: Starting " + (full ? "full " : "") + "sync at "
    213                     + startTimestamp);
    214         }
    215 
    216         mSyncInProgressTimestamp = startTimestamp;
    217 
    218         return true;
    219     }
    220 
    221     /**
    222      * Return delay (in ms) until allowed to run a full sync (0 meaning can run immediately)
    223      * @param startTimestamp Timestamp used to start the sync
    224      * @return 0 if allowed to run now, else delay in ms
    225      */
    226     public long delayUntilFullSync(final long startTimestamp) {
    227         final BugleGservices bugleGservices = BugleGservices.get();
    228         final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
    229 
    230         final long lastFullSyncTime = prefs.getLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, -1L);
    231         final long smsFullSyncBackoffTimeMillis = bugleGservices.getLong(
    232                 BugleGservicesKeys.SMS_FULL_SYNC_BACKOFF_TIME_MILLIS,
    233                 BugleGservicesKeys.SMS_FULL_SYNC_BACKOFF_TIME_MILLIS_DEFAULT);
    234         final long noFullSyncBefore = (lastFullSyncTime < 0 ? startTimestamp :
    235             lastFullSyncTime + smsFullSyncBackoffTimeMillis);
    236 
    237         final long delayUntilFullSync = noFullSyncBefore - startTimestamp;
    238         if (delayUntilFullSync > 0) {
    239             return delayUntilFullSync;
    240         }
    241         return 0;
    242     }
    243 
    244     /**
    245      * Check if sync currently in progress (public for asserts/logging).
    246      */
    247     public synchronized boolean isSyncing() {
    248         return (mSyncInProgressTimestamp >= 0);
    249     }
    250 
    251     /**
    252      * Check if sync batch should be in progress - compares upperBound with in memory value
    253      * @param upperBoundTimestamp - upperbound timestamp for sync batch
    254      * @return - true if timestamps match (otherwise batch is orphan from older process)
    255      */
    256     public synchronized boolean isSyncing(final long upperBoundTimestamp) {
    257         Assert.isTrue(upperBoundTimestamp >= 0);
    258         return (upperBoundTimestamp == mCurrentUpperBoundTimestamp);
    259     }
    260 
    261     /**
    262      * Check if sync has completed for the first time.
    263      */
    264     public boolean getHasFirstSyncCompleted() {
    265         final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
    266         return prefs.getLong(BuglePrefsKeys.LAST_SYNC_TIME,
    267                 BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT) !=
    268                 BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT;
    269     }
    270 
    271     /**
    272      * Called once sync is complete
    273      */
    274     public synchronized void complete() {
    275         if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    276             LogUtil.d(TAG, "SyncManager: Sync started at " + mSyncInProgressTimestamp
    277                     + " marked as complete");
    278         }
    279         mSyncInProgressTimestamp = -1L;
    280         // Conversation customization only used once
    281         mCustomization = null;
    282     }
    283 
    284     private final ContentObserver mMmsSmsObserver = new TelephonyMessagesObserver();
    285     private boolean mSyncOnChanges = false;
    286     private boolean mNotifyOnChanges = false;
    287 
    288     /**
    289      * Register content observer when necessary and kick off a catch up sync
    290      */
    291     public void updateSyncObserver(final Context context) {
    292         registerObserver(context);
    293         // Trigger an sms sync in case we missed and messages before registering this observer or
    294         // becoming the SMS provider.
    295         immediateSync();
    296     }
    297 
    298     private void registerObserver(final Context context) {
    299         if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
    300             // Not default SMS app - need to actively monitor telephony but not notify
    301             mNotifyOnChanges = false;
    302             mSyncOnChanges = true;
    303         } else if (OsUtil.isSecondaryUser()){
    304             // Secondary users default SMS app - need to actively monitor telephony and notify
    305             mNotifyOnChanges = true;
    306             mSyncOnChanges = true;
    307         } else {
    308             // Primary users default SMS app - don't monitor telephony (most changes from this app)
    309             mNotifyOnChanges = false;
    310             mSyncOnChanges = false;
    311         }
    312         if (mNotifyOnChanges || mSyncOnChanges) {
    313             context.getContentResolver().registerContentObserver(Telephony.MmsSms.CONTENT_URI,
    314                     true, mMmsSmsObserver);
    315         } else {
    316             context.getContentResolver().unregisterContentObserver(mMmsSmsObserver);
    317         }
    318     }
    319 
    320     public synchronized void setCustomization(
    321             final LongSparseArray<ConversationCustomization> customization) {
    322         this.mCustomization = customization;
    323     }
    324 
    325     public synchronized ConversationCustomization getCustomizationForThread(final long threadId) {
    326         if (mCustomization != null) {
    327             return mCustomization.get(threadId);
    328         }
    329         return null;
    330     }
    331 
    332     public static void resetLastSyncTimestamps() {
    333         final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
    334         prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME,
    335                 BuglePrefsKeys.LAST_FULL_SYNC_TIME_DEFAULT);
    336         prefs.putLong(BuglePrefsKeys.LAST_SYNC_TIME, BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT);
    337     }
    338 
    339     private class TelephonyMessagesObserver extends ContentObserver {
    340         public TelephonyMessagesObserver() {
    341             // Just run on default thread
    342             super(null);
    343         }
    344 
    345         // Implement the onChange(boolean) method to delegate the change notification to
    346         // the onChange(boolean, Uri) method to ensure correct operation on older versions
    347         // of the framework that did not have the onChange(boolean, Uri) method.
    348         @Override
    349         public void onChange(final boolean selfChange) {
    350             onChange(selfChange, null);
    351         }
    352 
    353         // Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument.
    354         @Override
    355         public void onChange(final boolean selfChange, final Uri uri) {
    356             // Handle change.
    357             if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    358                 LogUtil.v(TAG, "SyncManager: Sms/Mms DB changed @" + System.currentTimeMillis()
    359                         + " for " + (uri == null ? "<unk>" : uri.toString()) + " "
    360                         + mSyncOnChanges + "/" + mNotifyOnChanges);
    361             }
    362 
    363             if (mSyncOnChanges) {
    364                 // If sync is already running this will do nothing - but at end of each sync
    365                 // action there is a check for recent messages that should catch new changes.
    366                 SyncManager.immediateSync();
    367             }
    368             if (mNotifyOnChanges) {
    369                 // TODO: Secondary users are not going to get notifications
    370             }
    371         }
    372     }
    373 
    374     public ThreadInfoCache getThreadInfoCache() {
    375         return mThreadInfoCache;
    376     }
    377 
    378     public static class ThreadInfoCache {
    379         // Cache of thread->conversationId map
    380         private final LongSparseArray<String> mThreadToConversationId =
    381                 new LongSparseArray<String>();
    382 
    383         // Cache of thread->recipients map
    384         private final LongSparseArray<List<String>> mThreadToRecipients =
    385                 new LongSparseArray<List<String>>();
    386 
    387         // Remember the conversation ids that need to be archived
    388         private final HashSet<String> mArchivedConversations = new HashSet<>();
    389 
    390         public synchronized void clear() {
    391             if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    392                 LogUtil.d(TAG, "SyncManager: Cleared ThreadInfoCache");
    393             }
    394             mThreadToConversationId.clear();
    395             mThreadToRecipients.clear();
    396             mArchivedConversations.clear();
    397         }
    398 
    399         public synchronized boolean isArchived(final String conversationId) {
    400             return mArchivedConversations.contains(conversationId);
    401         }
    402 
    403         /**
    404          * Get or create a conversation based on the message's thread id
    405          *
    406          * @param threadId The message's thread
    407          * @param refSubId The subId used for normalizing phone numbers in the thread
    408          * @param customization The user setting customization to the conversation if any
    409          * @return The existing conversation id or new conversation id
    410          */
    411         public synchronized String getOrCreateConversation(final DatabaseWrapper db,
    412                 final long threadId, int refSubId, final ConversationCustomization customization) {
    413             // This function has several components which need to be atomic.
    414             Assert.isTrue(db.getDatabase().inTransaction());
    415 
    416             // If we already have this conversation ID in our local map, just return it
    417             String conversationId = mThreadToConversationId.get(threadId);
    418             if (conversationId != null) {
    419                 return conversationId;
    420             }
    421 
    422             final List<String> recipients = getThreadRecipients(threadId);
    423             final ArrayList<ParticipantData> participants =
    424                     BugleDatabaseOperations.getConversationParticipantsFromRecipients(recipients,
    425                             refSubId);
    426 
    427             if (customization != null) {
    428                 // There is user customization we need to recover
    429                 conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
    430                         customization.isArchived(), participants, customization.isMuted(),
    431                         customization.noVibrate(), customization.getNotificationSoundUri());
    432                 if (customization.isArchived()) {
    433                     mArchivedConversations.add(conversationId);
    434                 }
    435             } else {
    436                 conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
    437                         false/*archived*/, participants, false/*noNotification*/,
    438                         false/*noVibrate*/, null/*soundUri*/);
    439             }
    440 
    441             if (conversationId != null) {
    442                 mThreadToConversationId.put(threadId, conversationId);
    443                 return conversationId;
    444             }
    445 
    446             return null;
    447         }
    448 
    449 
    450         /**
    451          * Load the recipients of a thread from telephony provider. If we fail, use
    452          * a predefined unknown recipient. This should not return null.
    453          *
    454          * @param threadId
    455          */
    456         public synchronized List<String> getThreadRecipients(final long threadId) {
    457             List<String> recipients = mThreadToRecipients.get(threadId);
    458             if (recipients == null) {
    459                 recipients = MmsUtils.getRecipientsByThread(threadId);
    460                 if (recipients != null && recipients.size() > 0) {
    461                     mThreadToRecipients.put(threadId, recipients);
    462                 }
    463             }
    464 
    465             if (recipients == null || recipients.isEmpty()) {
    466                 LogUtil.w(TAG, "SyncManager : using unknown sender since thread " + threadId +
    467                         " couldn't find any recipients.");
    468 
    469                 // We want to try our best to load the messages,
    470                 // so if recipient info is broken, try to fix it with unknown recipient
    471                 recipients = Lists.newArrayList();
    472                 recipients.add(ParticipantData.getUnknownSenderDestination());
    473             }
    474 
    475             return recipients;
    476         }
    477     }
    478 }
    479