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