diff options
author | Mike Dodd <mdodd@google.com> | 2015-08-11 11:16:59 -0700 |
---|---|---|
committer | Mike Dodd <mdodd@google.com> | 2015-08-12 08:58:28 -0700 |
commit | 461a34b466cb4b13dbbc2ec6330b31e217b2ac4e (patch) | |
tree | bc4b489af52d0e2521e21167d2ad76a47256f348 /src/com/android/messaging/datamodel/SyncManager.java | |
parent | 8b3e2b9c1b0a09423a7ba5d1091b9192106502f8 (diff) | |
download | android_packages_apps_Messaging-461a34b466cb4b13dbbc2ec6330b31e217b2ac4e.tar.gz android_packages_apps_Messaging-461a34b466cb4b13dbbc2ec6330b31e217b2ac4e.tar.bz2 android_packages_apps_Messaging-461a34b466cb4b13dbbc2ec6330b31e217b2ac4e.zip |
Initial checkin of AOSP Messaging app.
b/23110861
Change-Id: I9aa980d7569247d6b2ca78f5dcb4502e1eaadb8a
Diffstat (limited to 'src/com/android/messaging/datamodel/SyncManager.java')
-rw-r--r-- | src/com/android/messaging/datamodel/SyncManager.java | 478 |
1 files changed, 478 insertions, 0 deletions
diff --git a/src/com/android/messaging/datamodel/SyncManager.java b/src/com/android/messaging/datamodel/SyncManager.java new file mode 100644 index 0000000..b3571bf --- /dev/null +++ b/src/com/android/messaging/datamodel/SyncManager.java @@ -0,0 +1,478 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.messaging.datamodel; + +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.provider.Telephony; +import android.support.v4.util.LongSparseArray; + +import com.android.messaging.datamodel.action.SyncMessagesAction; +import com.android.messaging.datamodel.data.ParticipantData; +import com.android.messaging.sms.MmsUtils; +import com.android.messaging.util.Assert; +import com.android.messaging.util.BugleGservices; +import com.android.messaging.util.BugleGservicesKeys; +import com.android.messaging.util.BuglePrefs; +import com.android.messaging.util.BuglePrefsKeys; +import com.android.messaging.util.LogUtil; +import com.android.messaging.util.OsUtil; +import com.android.messaging.util.PhoneUtils; +import com.google.common.collect.Lists; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +/** + * This class manages message sync with the Telephony SmsProvider/MmsProvider. + */ +public class SyncManager { + private static final String TAG = LogUtil.BUGLE_TAG; + + /** + * Record of any user customization to conversation settings + */ + public static class ConversationCustomization { + private final boolean mArchived; + private final boolean mMuted; + private final boolean mNoVibrate; + private final String mNotificationSoundUri; + + public ConversationCustomization(final boolean archived, final boolean muted, + final boolean noVibrate, final String notificationSoundUri) { + mArchived = archived; + mMuted = muted; + mNoVibrate = noVibrate; + mNotificationSoundUri = notificationSoundUri; + } + + public boolean isArchived() { + return mArchived; + } + + public boolean isMuted() { + return mMuted; + } + + public boolean noVibrate() { + return mNoVibrate; + } + + public String getNotificationSoundUri() { + return mNotificationSoundUri; + } + } + + SyncManager() { + } + + /** + * Timestamp of in progress sync - used to keep track of whether sync is running + */ + private long mSyncInProgressTimestamp = -1; + + /** + * Timestamp of current sync batch upper bound - used to determine if message makes batch dirty + */ + private long mCurrentUpperBoundTimestamp = -1; + + /** + * Timestamp of messages inserted since sync batch started - used to determine if batch dirty + */ + private long mMaxRecentChangeTimestamp = -1L; + + private final ThreadInfoCache mThreadInfoCache = new ThreadInfoCache(); + + /** + * User customization to conversations. If this is set, we need to recover them after + * a full sync. + */ + private LongSparseArray<ConversationCustomization> mCustomization = null; + + /** + * Start an incremental sync (backed off a few seconds) + */ + public static void sync() { + SyncMessagesAction.sync(); + } + + /** + * Start an incremental sync (with no backoff) + */ + public static void immediateSync() { + SyncMessagesAction.immediateSync(); + } + + /** + * Start a full sync (for debugging) + */ + public static void forceSync() { + SyncMessagesAction.fullSync(); + } + + /** + * Called from data model thread when starting a sync batch + * @param upperBoundTimestamp upper bound timestamp for sync batch + */ + public synchronized void startSyncBatch(final long upperBoundTimestamp) { + Assert.isTrue(mCurrentUpperBoundTimestamp < 0); + mCurrentUpperBoundTimestamp = upperBoundTimestamp; + mMaxRecentChangeTimestamp = -1L; + } + + /** + * Called from data model thread at end of batch to determine if any messages added in window + * @param lowerBoundTimestamp lower bound timestamp for sync batch + * @return true if message added within window from lower to upper bound timestamp of batch + */ + public synchronized boolean isBatchDirty(final long lowerBoundTimestamp) { + Assert.isTrue(mCurrentUpperBoundTimestamp >= 0); + final long max = mMaxRecentChangeTimestamp; + + final boolean dirty = (max >= 0 && max >= lowerBoundTimestamp); + if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { + LogUtil.d(TAG, "SyncManager: Sync batch of messages from " + lowerBoundTimestamp + + " to " + mCurrentUpperBoundTimestamp + " is " + + (dirty ? "DIRTY" : "clean") + "; max change timestamp = " + + mMaxRecentChangeTimestamp); + } + + mCurrentUpperBoundTimestamp = -1L; + mMaxRecentChangeTimestamp = -1L; + + return dirty; + } + + /** + * Called from data model or background worker thread to indicate start of message add process + * (add must complete on that thread before action transitions to new thread/stage) + * @param timestamp timestamp of message being added + */ + public synchronized void onNewMessageInserted(final long timestamp) { + if (mCurrentUpperBoundTimestamp >= 0 && timestamp <= mCurrentUpperBoundTimestamp) { + // Message insert in current sync window + mMaxRecentChangeTimestamp = Math.max(mCurrentUpperBoundTimestamp, timestamp); + if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { + LogUtil.d(TAG, "SyncManager: New message @ " + timestamp + " before upper bound of " + + "current sync batch " + mCurrentUpperBoundTimestamp); + } + } else if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { + LogUtil.d(TAG, "SyncManager: New message @ " + timestamp + " after upper bound of " + + "current sync batch " + mCurrentUpperBoundTimestamp); + } + } + + /** + * Synchronously checks whether sync is allowed and starts sync if allowed + * @param full - true indicates a full (not incremental) sync operation + * @param startTimestamp - starttimestamp for this sync (if allowed) + * @return - true if sync should start + */ + public synchronized boolean shouldSync(final boolean full, final long startTimestamp) { + if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { + LogUtil.v(TAG, "SyncManager: Checking shouldSync " + (full ? "full " : "") + + "at " + startTimestamp); + } + + if (full) { + final long delayUntilFullSync = delayUntilFullSync(startTimestamp); + if (delayUntilFullSync > 0) { + if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { + LogUtil.d(TAG, "SyncManager: Full sync requested for " + startTimestamp + + " delayed for " + delayUntilFullSync + " ms"); + } + return false; + } + } + + if (isSyncing()) { + if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { + LogUtil.d(TAG, "SyncManager: Not allowed to " + (full ? "full " : "") + + "sync yet; still running sync started at " + mSyncInProgressTimestamp); + } + return false; + } + if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { + LogUtil.d(TAG, "SyncManager: Starting " + (full ? "full " : "") + "sync at " + + startTimestamp); + } + + mSyncInProgressTimestamp = startTimestamp; + + return true; + } + + /** + * Return delay (in ms) until allowed to run a full sync (0 meaning can run immediately) + * @param startTimestamp Timestamp used to start the sync + * @return 0 if allowed to run now, else delay in ms + */ + public long delayUntilFullSync(final long startTimestamp) { + final BugleGservices bugleGservices = BugleGservices.get(); + final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); + + final long lastFullSyncTime = prefs.getLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, -1L); + final long smsFullSyncBackoffTimeMillis = bugleGservices.getLong( + BugleGservicesKeys.SMS_FULL_SYNC_BACKOFF_TIME_MILLIS, + BugleGservicesKeys.SMS_FULL_SYNC_BACKOFF_TIME_MILLIS_DEFAULT); + final long noFullSyncBefore = (lastFullSyncTime < 0 ? startTimestamp : + lastFullSyncTime + smsFullSyncBackoffTimeMillis); + + final long delayUntilFullSync = noFullSyncBefore - startTimestamp; + if (delayUntilFullSync > 0) { + return delayUntilFullSync; + } + return 0; + } + + /** + * Check if sync currently in progress (public for asserts/logging). + */ + public synchronized boolean isSyncing() { + return (mSyncInProgressTimestamp >= 0); + } + + /** + * Check if sync batch should be in progress - compares upperBound with in memory value + * @param upperBoundTimestamp - upperbound timestamp for sync batch + * @return - true if timestamps match (otherwise batch is orphan from older process) + */ + public synchronized boolean isSyncing(final long upperBoundTimestamp) { + Assert.isTrue(upperBoundTimestamp >= 0); + return (upperBoundTimestamp == mCurrentUpperBoundTimestamp); + } + + /** + * Check if sync has completed for the first time. + */ + public boolean getHasFirstSyncCompleted() { + final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); + return prefs.getLong(BuglePrefsKeys.LAST_SYNC_TIME, + BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT) != + BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT; + } + + /** + * Called once sync is complete + */ + public synchronized void complete() { + if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { + LogUtil.d(TAG, "SyncManager: Sync started at " + mSyncInProgressTimestamp + + " marked as complete"); + } + mSyncInProgressTimestamp = -1L; + // Conversation customization only used once + mCustomization = null; + } + + private final ContentObserver mMmsSmsObserver = new TelephonyMessagesObserver(); + private boolean mSyncOnChanges = false; + private boolean mNotifyOnChanges = false; + + /** + * Register content observer when necessary and kick off a catch up sync + */ + public void updateSyncObserver(final Context context) { + registerObserver(context); + // Trigger an sms sync in case we missed and messages before registering this observer or + // becoming the SMS provider. + immediateSync(); + } + + private void registerObserver(final Context context) { + if (!PhoneUtils.getDefault().isDefaultSmsApp()) { + // Not default SMS app - need to actively monitor telephony but not notify + mNotifyOnChanges = false; + mSyncOnChanges = true; + } else if (OsUtil.isSecondaryUser()){ + // Secondary users default SMS app - need to actively monitor telephony and notify + mNotifyOnChanges = true; + mSyncOnChanges = true; + } else { + // Primary users default SMS app - don't monitor telephony (most changes from this app) + mNotifyOnChanges = false; + mSyncOnChanges = false; + } + if (mNotifyOnChanges || mSyncOnChanges) { + context.getContentResolver().registerContentObserver(Telephony.MmsSms.CONTENT_URI, + true, mMmsSmsObserver); + } else { + context.getContentResolver().unregisterContentObserver(mMmsSmsObserver); + } + } + + public synchronized void setCustomization( + final LongSparseArray<ConversationCustomization> customization) { + this.mCustomization = customization; + } + + public synchronized ConversationCustomization getCustomizationForThread(final long threadId) { + if (mCustomization != null) { + return mCustomization.get(threadId); + } + return null; + } + + public static void resetLastSyncTimestamps() { + final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); + prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, + BuglePrefsKeys.LAST_FULL_SYNC_TIME_DEFAULT); + prefs.putLong(BuglePrefsKeys.LAST_SYNC_TIME, BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT); + } + + private class TelephonyMessagesObserver extends ContentObserver { + public TelephonyMessagesObserver() { + // Just run on default thread + super(null); + } + + // Implement the onChange(boolean) method to delegate the change notification to + // the onChange(boolean, Uri) method to ensure correct operation on older versions + // of the framework that did not have the onChange(boolean, Uri) method. + @Override + public void onChange(final boolean selfChange) { + onChange(selfChange, null); + } + + // Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument. + @Override + public void onChange(final boolean selfChange, final Uri uri) { + // Handle change. + if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { + LogUtil.v(TAG, "SyncManager: Sms/Mms DB changed @" + System.currentTimeMillis() + + " for " + (uri == null ? "<unk>" : uri.toString()) + " " + + mSyncOnChanges + "/" + mNotifyOnChanges); + } + + if (mSyncOnChanges) { + // If sync is already running this will do nothing - but at end of each sync + // action there is a check for recent messages that should catch new changes. + SyncManager.immediateSync(); + } + if (mNotifyOnChanges) { + // TODO: Secondary users are not going to get notifications + } + } + } + + public ThreadInfoCache getThreadInfoCache() { + return mThreadInfoCache; + } + + public static class ThreadInfoCache { + // Cache of thread->conversationId map + private final LongSparseArray<String> mThreadToConversationId = + new LongSparseArray<String>(); + + // Cache of thread->recipients map + private final LongSparseArray<List<String>> mThreadToRecipients = + new LongSparseArray<List<String>>(); + + // Remember the conversation ids that need to be archived + private final HashSet<String> mArchivedConversations = new HashSet<>(); + + public synchronized void clear() { + if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { + LogUtil.d(TAG, "SyncManager: Cleared ThreadInfoCache"); + } + mThreadToConversationId.clear(); + mThreadToRecipients.clear(); + mArchivedConversations.clear(); + } + + public synchronized boolean isArchived(final String conversationId) { + return mArchivedConversations.contains(conversationId); + } + + /** + * Get or create a conversation based on the message's thread id + * + * @param threadId The message's thread + * @param refSubId The subId used for normalizing phone numbers in the thread + * @param customization The user setting customization to the conversation if any + * @return The existing conversation id or new conversation id + */ + public synchronized String getOrCreateConversation(final DatabaseWrapper db, + final long threadId, int refSubId, final ConversationCustomization customization) { + // This function has several components which need to be atomic. + Assert.isTrue(db.getDatabase().inTransaction()); + + // If we already have this conversation ID in our local map, just return it + String conversationId = mThreadToConversationId.get(threadId); + if (conversationId != null) { + return conversationId; + } + + final List<String> recipients = getThreadRecipients(threadId); + final ArrayList<ParticipantData> participants = + BugleDatabaseOperations.getConversationParticipantsFromRecipients(recipients, + refSubId); + + if (customization != null) { + // There is user customization we need to recover + conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId, + customization.isArchived(), participants, customization.isMuted(), + customization.noVibrate(), customization.getNotificationSoundUri()); + if (customization.isArchived()) { + mArchivedConversations.add(conversationId); + } + } else { + conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId, + false/*archived*/, participants, false/*noNotification*/, + false/*noVibrate*/, null/*soundUri*/); + } + + if (conversationId != null) { + mThreadToConversationId.put(threadId, conversationId); + return conversationId; + } + + return null; + } + + + /** + * Load the recipients of a thread from telephony provider. If we fail, use + * a predefined unknown recipient. This should not return null. + * + * @param threadId + */ + public synchronized List<String> getThreadRecipients(final long threadId) { + List<String> recipients = mThreadToRecipients.get(threadId); + if (recipients == null) { + recipients = MmsUtils.getRecipientsByThread(threadId); + if (recipients != null && recipients.size() > 0) { + mThreadToRecipients.put(threadId, recipients); + } + } + + if (recipients == null || recipients.isEmpty()) { + LogUtil.w(TAG, "SyncManager : using unknown sender since thread " + threadId + + " couldn't find any recipients."); + + // We want to try our best to load the messages, + // so if recipient info is broken, try to fix it with unknown recipient + recipients = Lists.newArrayList(); + recipients.add(ParticipantData.getUnknownSenderDestination()); + } + + return recipients; + } + } +} |