summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/datamodel/SyncManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/messaging/datamodel/SyncManager.java')
-rw-r--r--src/com/android/messaging/datamodel/SyncManager.java478
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;
+ }
+ }
+}