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