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