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 
ConversationCustomization(final boolean archived, final boolean muted, final boolean noVibrate, final String notificationSoundUri)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 
isArchived()65         public boolean isArchived() {
66             return mArchived;
67         }
68 
isMuted()69         public boolean isMuted() {
70             return mMuted;
71         }
72 
noVibrate()73         public boolean noVibrate() {
74             return mNoVibrate;
75         }
76 
getNotificationSoundUri()77         public String getNotificationSoundUri() {
78             return mNotificationSoundUri;
79         }
80     }
81 
SyncManager()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      */
sync()111     public static void sync() {
112         SyncMessagesAction.sync();
113     }
114 
115     /**
116      * Start an incremental sync (with no backoff)
117      */
immediateSync()118     public static void immediateSync() {
119         SyncMessagesAction.immediateSync();
120     }
121 
122     /**
123      * Start a full sync (for debugging)
124      */
forceSync()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      */
startSyncBatch(final long upperBoundTimestamp)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      */
onNewMessageInserted(final long timestamp)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      */
shouldSync(final boolean full, final long startTimestamp)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      */
delayUntilFullSync(final long startTimestamp)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      */
isSyncing()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      */
isSyncing(final long upperBoundTimestamp)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      */
getHasFirstSyncCompleted()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      */
complete()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      */
updateSyncObserver(final Context context)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 
registerObserver(final Context context)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 
setCustomization( final LongSparseArray<ConversationCustomization> customization)320     public synchronized void setCustomization(
321             final LongSparseArray<ConversationCustomization> customization) {
322         this.mCustomization = customization;
323     }
324 
getCustomizationForThread(final long threadId)325     public synchronized ConversationCustomization getCustomizationForThread(final long threadId) {
326         if (mCustomization != null) {
327             return mCustomization.get(threadId);
328         }
329         return null;
330     }
331 
resetLastSyncTimestamps()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 {
TelephonyMessagesObserver()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
onChange(final boolean selfChange)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
onChange(final boolean selfChange, final Uri uri)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 
getThreadInfoCache()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 
clear()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 
isArchived(final String conversationId)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          */
getOrCreateConversation(final DatabaseWrapper db, final long threadId, int refSubId, final ConversationCustomization customization)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          */
getThreadRecipients(final long threadId)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