461 lines
18 KiB
Java
461 lines
18 KiB
Java
/*
|
|
* Copyright (C) 2015 The Android Open Source Project
|
|
* Copyright (C) 2024 The LineageOS 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.content.pm.PackageManager;
|
|
import android.database.ContentObserver;
|
|
import android.net.Uri;
|
|
import android.provider.Telephony;
|
|
import androidx.collection.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.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;
|
|
|
|
public ConversationCustomization(final boolean archived) {
|
|
mArchived = archived;
|
|
}
|
|
|
|
public boolean isArchived() {
|
|
return mArchived;
|
|
}
|
|
}
|
|
|
|
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 BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
|
|
|
|
final long lastFullSyncTime = prefs.getLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, -1L);
|
|
final long smsFullSyncBackoffTimeMillis =
|
|
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 (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
|
|
// This is default SMS app but on Auto platform, even default SMS app needs
|
|
// to be notified of changes because Bluetooth makes changes to Sms database
|
|
// (thru ContentProvider). Content Change notifications help the default SMS app
|
|
// refresh display with changes, whenever Bluetooth changes data in SMS db.
|
|
mNotifyOnChanges = true;
|
|
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<>();
|
|
|
|
// Cache of thread->recipients map
|
|
private final LongSparseArray<List<String>> mThreadToRecipients = new LongSparseArray<>();
|
|
|
|
// 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);
|
|
if (customization.isArchived()) {
|
|
mArchivedConversations.add(conversationId);
|
|
}
|
|
} else {
|
|
conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
|
|
false/*archived*/, participants);
|
|
}
|
|
|
|
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.
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
}
|