Messaging: Looply home UI and Gradle app module layout
Restructure the tree for Android Studio/Gradle builds and add the iOS-style home experience with pinned conversations, selection chrome, and scroll-aware search padding. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 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;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Handler;
|
||||
import android.support.v7.mms.CarrierConfigValuesLoader;
|
||||
import android.support.v7.mms.MmsManager;
|
||||
import android.telephony.CarrierConfigManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.receiver.SmsReceiver;
|
||||
import com.android.messaging.sms.BugleApnSettingsLoader;
|
||||
import com.android.messaging.sms.BugleUserAgentInfoLoader;
|
||||
import com.android.messaging.sms.MmsConfig;
|
||||
import com.android.messaging.ui.ConversationDrawables;
|
||||
import com.android.messaging.util.BuglePrefsKeys;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
import com.android.messaging.util.Trace;
|
||||
|
||||
import java.lang.Thread.UncaughtExceptionHandler;
|
||||
|
||||
/**
|
||||
* The application object
|
||||
*/
|
||||
public class BugleApplication extends Application implements UncaughtExceptionHandler {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
private UncaughtExceptionHandler sSystemUncaughtExceptionHandler;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Trace.beginSection("app.onCreate");
|
||||
super.onCreate();
|
||||
|
||||
FactoryImpl.register(getApplicationContext(), this);
|
||||
|
||||
sSystemUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
Thread.setDefaultUncaughtExceptionHandler(this);
|
||||
Trace.endSection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull final Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
// Update conversation drawables when changing writing systems
|
||||
// (Right-To-Left / Left-To-Right)
|
||||
ConversationDrawables.get().updateDrawables();
|
||||
}
|
||||
|
||||
// Called by the "real" factory from FactoryImpl.register() (i.e. not run in tests)
|
||||
public void initializeSync(final Factory factory) {
|
||||
Trace.beginSection("app.initializeSync");
|
||||
final Context context = factory.getApplicationContext();
|
||||
final DataModel dataModel = factory.getDataModel();
|
||||
final CarrierConfigValuesLoader carrierConfigValuesLoader =
|
||||
factory.getCarrierConfigValuesLoader();
|
||||
|
||||
BugleApplication.updateAppConfig(context);
|
||||
|
||||
// Initialize MMS lib
|
||||
initMmsLib(context, carrierConfigValuesLoader);
|
||||
// Fixup messages in flight if we crashed and send any pending
|
||||
dataModel.onApplicationCreated();
|
||||
// Register carrier config change receiver
|
||||
registerCarrierConfigChangeReceiver(context);
|
||||
|
||||
Trace.endSection();
|
||||
}
|
||||
|
||||
private static void registerCarrierConfigChangeReceiver(final Context context) {
|
||||
context.registerReceiver(new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
LogUtil.i(TAG, "Carrier config changed. Reloading MMS config.");
|
||||
MmsConfig.loadAsync();
|
||||
}
|
||||
}, new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED),
|
||||
Context.RECEIVER_EXPORTED/*UNAUDITED*/);
|
||||
}
|
||||
|
||||
private static void initMmsLib(final Context context,
|
||||
final CarrierConfigValuesLoader carrierConfigValuesLoader) {
|
||||
MmsManager.setApnSettingsLoader(new BugleApnSettingsLoader(context));
|
||||
MmsManager.setCarrierConfigValuesLoader(carrierConfigValuesLoader);
|
||||
MmsManager.setUserAgentInfoLoader(new BugleUserAgentInfoLoader(context));
|
||||
}
|
||||
|
||||
public static void updateAppConfig(final Context context) {
|
||||
// Make sure we set the correct state for the SMS/MMS receivers
|
||||
SmsReceiver.updateSmsReceiveHandler(context);
|
||||
}
|
||||
|
||||
// Called from thread started in FactoryImpl.register() (i.e. not run in tests)
|
||||
public void initializeAsync(final Factory factory) {
|
||||
// Handle shared prefs upgrade & Load MMS Configuration
|
||||
Trace.beginSection("app.initializeAsync");
|
||||
maybeHandleSharedPrefsUpgrade(factory);
|
||||
MmsConfig.load();
|
||||
Trace.endSection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
super.onLowMemory();
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "BugleApplication.onLowMemory");
|
||||
}
|
||||
Factory.get().reclaimMemory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(@NonNull final Thread thread, @NonNull final Throwable ex) {
|
||||
final boolean background = getMainLooper().getThread() != thread;
|
||||
if (background) {
|
||||
LogUtil.e(TAG, "Uncaught exception in background thread " + thread, ex);
|
||||
|
||||
final Handler handler = new Handler(getMainLooper());
|
||||
handler.post(() -> sSystemUncaughtExceptionHandler.uncaughtException(thread, ex));
|
||||
} else {
|
||||
sSystemUncaughtExceptionHandler.uncaughtException(thread, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeHandleSharedPrefsUpgrade(final Factory factory) {
|
||||
final int existingVersion = factory.getApplicationPrefs().getInt(
|
||||
BuglePrefsKeys.SHARED_PREFERENCES_VERSION,
|
||||
BuglePrefsKeys.SHARED_PREFERENCES_VERSION_DEFAULT);
|
||||
final int targetVersion = Integer.parseInt(getString(R.string.pref_version));
|
||||
if (targetVersion > existingVersion) {
|
||||
LogUtil.i(LogUtil.BUGLE_TAG, "Upgrading shared prefs from " + existingVersion +
|
||||
" to " + targetVersion);
|
||||
try {
|
||||
// Perform upgrade on application-wide prefs.
|
||||
factory.getApplicationPrefs().onUpgrade(existingVersion, targetVersion);
|
||||
// Perform upgrade on each subscription's prefs.
|
||||
PhoneUtils.forEachActiveSubscription(subId -> factory.getSubscriptionPrefs(subId)
|
||||
.onUpgrade(existingVersion, targetVersion));
|
||||
factory.getApplicationPrefs().putInt(BuglePrefsKeys.SHARED_PREFERENCES_VERSION,
|
||||
targetVersion);
|
||||
} catch (final Exception ex) {
|
||||
// Upgrade failed. Don't crash the app because we can always fall back to the
|
||||
// default settings.
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Failed to upgrade shared prefs", ex);
|
||||
}
|
||||
} else if (targetVersion < existingVersion) {
|
||||
// We don't care about downgrade since real user shouldn't encounter this, so log it
|
||||
// and ignore any prefs migration.
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Shared prefs downgrade requested and ignored. " +
|
||||
"oldVersion = " + existingVersion + ", newVersion = " + targetVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.MemoryCacheManager;
|
||||
import com.android.messaging.datamodel.ParticipantRefresh.ContactContentObserver;
|
||||
import com.android.messaging.datamodel.media.MediaCacheManager;
|
||||
import com.android.messaging.datamodel.media.MediaResourceManager;
|
||||
import com.android.messaging.sms.BugleCarrierConfigValuesLoader;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.BuglePrefs;
|
||||
import com.android.messaging.util.MediaUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
|
||||
public abstract class Factory {
|
||||
|
||||
// Making this volatile because on the unit tests, setInstance is called from a unit test
|
||||
// thread, and then it's read on the UI thread.
|
||||
private static volatile Factory sInstance;
|
||||
protected static boolean sRegistered;
|
||||
protected static boolean sInitialized;
|
||||
|
||||
public static Factory get() {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
protected static void setInstance(final Factory factory) {
|
||||
// Not allowed to call this after real application initialization is complete
|
||||
Assert.isTrue(!sRegistered);
|
||||
Assert.isTrue(!sInitialized);
|
||||
sInstance = factory;
|
||||
}
|
||||
public abstract void onRequiredPermissionsAcquired();
|
||||
|
||||
public abstract Context getApplicationContext();
|
||||
public abstract DataModel getDataModel();
|
||||
public abstract BuglePrefs getApplicationPrefs();
|
||||
public abstract BuglePrefs getSubscriptionPrefs(int subId);
|
||||
public abstract BuglePrefs getWidgetPrefs();
|
||||
public abstract UIIntents getUIIntents();
|
||||
public abstract MemoryCacheManager getMemoryCacheManager();
|
||||
public abstract MediaResourceManager getMediaResourceManager();
|
||||
public abstract MediaCacheManager getMediaCacheManager();
|
||||
public abstract ContactContentObserver getContactContentObserver();
|
||||
public abstract PhoneUtils getPhoneUtils(int subId);
|
||||
public abstract MediaUtil getMediaUtil();
|
||||
public abstract BugleCarrierConfigValuesLoader getCarrierConfigValuesLoader();
|
||||
// Note this needs to run from any thread
|
||||
public abstract void reclaimMemory();
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Process;
|
||||
import android.telephony.SmsManager;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DataModelImpl;
|
||||
import com.android.messaging.datamodel.MemoryCacheManager;
|
||||
import com.android.messaging.datamodel.ParticipantRefresh.ContactContentObserver;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.datamodel.media.BugleMediaCacheManager;
|
||||
import com.android.messaging.datamodel.media.MediaCacheManager;
|
||||
import com.android.messaging.datamodel.media.MediaResourceManager;
|
||||
import com.android.messaging.sms.BugleCarrierConfigValuesLoader;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.ui.UIIntentsImpl;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.BugleApplicationPrefs;
|
||||
import com.android.messaging.util.BuglePrefs;
|
||||
import com.android.messaging.util.BugleSubscriptionPrefs;
|
||||
import com.android.messaging.util.BugleWidgetPrefs;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.MediaUtil;
|
||||
import com.android.messaging.util.MediaUtilImpl;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
class FactoryImpl extends Factory {
|
||||
private BugleApplication mApplication;
|
||||
private DataModel mDataModel;
|
||||
private BugleApplicationPrefs mBugleApplicationPrefs;
|
||||
private BugleWidgetPrefs mBugleWidgetPrefs;
|
||||
private Context mApplicationContext;
|
||||
private UIIntents mUIIntents;
|
||||
private MemoryCacheManager mMemoryCacheManager;
|
||||
private MediaResourceManager mMediaResourceManager;
|
||||
private MediaCacheManager mMediaCacheManager;
|
||||
private ContactContentObserver mContactContentObserver;
|
||||
private MediaUtil mMediaUtil;
|
||||
private SparseArray<BugleSubscriptionPrefs> mSubscriptionPrefs;
|
||||
private BugleCarrierConfigValuesLoader mCarrierConfigValuesLoader;
|
||||
|
||||
// Cached subId->instance for L_MR1 and beyond
|
||||
private static final ConcurrentHashMap<Integer, PhoneUtils> sPhoneUtilsInstanceCacheLMR1 =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
private FactoryImpl() {
|
||||
}
|
||||
|
||||
public static Factory register(final Context applicationContext,
|
||||
final BugleApplication application) {
|
||||
// This only gets called once (from BugleApplication.onCreate), but its not called in tests.
|
||||
Assert.isTrue(!sRegistered);
|
||||
Assert.isNull(Factory.get());
|
||||
|
||||
final FactoryImpl factory = new FactoryImpl();
|
||||
Factory.setInstance(factory);
|
||||
sRegistered = true;
|
||||
|
||||
// At this point Factory is published. Services can now get initialized and depend on
|
||||
// Factory.get().
|
||||
factory.mApplication = application;
|
||||
factory.mApplicationContext = applicationContext;
|
||||
factory.mMemoryCacheManager = new MemoryCacheManager();
|
||||
factory.mMediaCacheManager = new BugleMediaCacheManager();
|
||||
factory.mMediaResourceManager = new MediaResourceManager();
|
||||
factory.mBugleApplicationPrefs = new BugleApplicationPrefs(applicationContext);
|
||||
factory.mDataModel = new DataModelImpl(applicationContext);
|
||||
factory.mBugleWidgetPrefs = new BugleWidgetPrefs(applicationContext);
|
||||
factory.mUIIntents = new UIIntentsImpl();
|
||||
factory.mContactContentObserver = new ContactContentObserver();
|
||||
factory.mMediaUtil = new MediaUtilImpl();
|
||||
factory.mSubscriptionPrefs = new SparseArray<>();
|
||||
factory.mCarrierConfigValuesLoader = new BugleCarrierConfigValuesLoader(applicationContext);
|
||||
|
||||
if (OsUtil.hasRequiredPermissions()) {
|
||||
factory.onRequiredPermissionsAcquired();
|
||||
}
|
||||
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequiredPermissionsAcquired() {
|
||||
if (sInitialized) {
|
||||
return;
|
||||
}
|
||||
sInitialized = true;
|
||||
|
||||
mApplication.initializeSync(this);
|
||||
|
||||
final Thread asyncInitialization = new Thread(() -> {
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
||||
mApplication.initializeAsync(FactoryImpl.this);
|
||||
});
|
||||
asyncInitialization.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context getApplicationContext() {
|
||||
return mApplicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataModel getDataModel() {
|
||||
return mDataModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BuglePrefs getApplicationPrefs() {
|
||||
return mBugleApplicationPrefs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BuglePrefs getWidgetPrefs() {
|
||||
return mBugleWidgetPrefs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BuglePrefs getSubscriptionPrefs(int subId) {
|
||||
subId = PhoneUtils.getDefault().getEffectiveSubId(subId);
|
||||
BugleSubscriptionPrefs pref = mSubscriptionPrefs.get(subId);
|
||||
if (pref == null) {
|
||||
synchronized (this) {
|
||||
if ((pref = mSubscriptionPrefs.get(subId)) == null) {
|
||||
pref = new BugleSubscriptionPrefs(getApplicationContext(), subId);
|
||||
mSubscriptionPrefs.put(subId, pref);
|
||||
}
|
||||
}
|
||||
}
|
||||
return pref;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UIIntents getUIIntents() {
|
||||
return mUIIntents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MemoryCacheManager getMemoryCacheManager() {
|
||||
return mMemoryCacheManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaResourceManager getMediaResourceManager() {
|
||||
return mMediaResourceManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaCacheManager getMediaCacheManager() {
|
||||
return mMediaCacheManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContactContentObserver getContactContentObserver() {
|
||||
return mContactContentObserver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PhoneUtils getPhoneUtils(int subId) {
|
||||
if (subId == ParticipantData.DEFAULT_SELF_SUB_ID) {
|
||||
subId = SmsManager.getDefaultSmsSubscriptionId();
|
||||
}
|
||||
if (subId < 0) {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "PhoneUtils.getForLMR1(): invalid subId = " + subId);
|
||||
subId = ParticipantData.DEFAULT_SELF_SUB_ID;
|
||||
}
|
||||
PhoneUtils instance = sPhoneUtilsInstanceCacheLMR1.get(subId);
|
||||
if (instance == null) {
|
||||
instance = new PhoneUtils(subId);
|
||||
sPhoneUtilsInstanceCacheLMR1.putIfAbsent(subId, instance);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reclaimMemory() {
|
||||
mMemoryCacheManager.reclaimMemory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaUtil getMediaUtil() {
|
||||
return mMediaUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BugleCarrierConfigValuesLoader getCarrierConfigValuesLoader() {
|
||||
return mCarrierConfigValuesLoader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.net.Uri;
|
||||
|
||||
import androidx.loader.content.CursorLoader;
|
||||
|
||||
/**
|
||||
* Extension to basic cursor loader that has an attached binding id
|
||||
*/
|
||||
public class BoundCursorLoader extends CursorLoader {
|
||||
private final String mBindingId;
|
||||
|
||||
/**
|
||||
* Create cursor loader for associated binding id
|
||||
*/
|
||||
public BoundCursorLoader(final String bindingId, final Context context, final Uri uri,
|
||||
final String[] projection, final String selection, final String[] selectionArgs,
|
||||
final String sortOrder) {
|
||||
super(context, uri, projection, selection, selectionArgs, sortOrder);
|
||||
mBindingId = bindingId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binding id associated with this loader - consume can check to verify data still valid
|
||||
*/
|
||||
public String getBindingId() {
|
||||
return mBindingId;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.ex.chips.RecipientEntry;
|
||||
|
||||
/**
|
||||
* An extension of RecipientEntry for Bugle's use since Bugle uses phone numbers to identify
|
||||
* participants / recipients instead of contact ids. This allows the user to send to multiple
|
||||
* phone numbers of the same contact.
|
||||
*/
|
||||
public class BugleRecipientEntry extends RecipientEntry {
|
||||
|
||||
protected BugleRecipientEntry(final int entryType, final String displayName,
|
||||
final String destination, final int destinationType, final String destinationLabel,
|
||||
final long contactId, final Long directoryId, final long dataId,
|
||||
final Uri photoThumbnailUri, final boolean isFirstLevel, final boolean isValid,
|
||||
final String lookupKey) {
|
||||
super(entryType, displayName, destination, destinationType, destinationLabel, contactId,
|
||||
directoryId, dataId, photoThumbnailUri, isFirstLevel, isValid, lookupKey);
|
||||
}
|
||||
|
||||
public static BugleRecipientEntry constructTopLevelEntry(final String displayName,
|
||||
final int displayNameSource, final String destination, final int destinationType,
|
||||
final String destinationLabel, final long contactId, final Long directoryId,
|
||||
final long dataId, final String thumbnailUriAsString, final boolean isValid,
|
||||
final String lookupKey) {
|
||||
return new BugleRecipientEntry(ENTRY_TYPE_PERSON, displayName, destination, destinationType,
|
||||
destinationLabel, contactId, directoryId, dataId, (thumbnailUriAsString != null
|
||||
? Uri.parse(thumbnailUriAsString) : null), true, isValid, lookupKey);
|
||||
}
|
||||
|
||||
public static BugleRecipientEntry constructSecondLevelEntry(final String displayName,
|
||||
final int displayNameSource, final String destination, final int destinationType,
|
||||
final String destinationLabel, final long contactId, final Long directoryId,
|
||||
final long dataId, final String thumbnailUriAsString, final boolean isValid,
|
||||
final String lookupKey) {
|
||||
return new BugleRecipientEntry(ENTRY_TYPE_PERSON, displayName, destination, destinationType,
|
||||
destinationLabel, contactId, directoryId, dataId, (thumbnailUriAsString != null
|
||||
? Uri.parse(thumbnailUriAsString) : null), false, isValid, lookupKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSamePerson(final RecipientEntry entry) {
|
||||
return getDestination() != null && entry.getDestination() != null &&
|
||||
TextUtils.equals(getDestination(), entry.getDestination());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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.provider.BaseColumns;
|
||||
import android.support.v7.mms.pdu.ContentType;
|
||||
|
||||
import com.android.ex.photo.provider.PhotoContract.PhotoViewColumns;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
|
||||
|
||||
/**
|
||||
* View for the image parts for the conversation. It is used to provide the photoviewer with a
|
||||
* a data source for all the photos in a conversation, so that the photoviewer can support paging
|
||||
* through all the photos of the conversation. The columns of the view are a superset of
|
||||
* {@link com.android.ex.photo.provider.PhotoContract.PhotoViewColumns}.
|
||||
*/
|
||||
public class ConversationImagePartsView {
|
||||
private static final String VIEW_NAME = "conversation_image_parts_view";
|
||||
|
||||
private static final String CREATE_SQL = "CREATE VIEW " +
|
||||
VIEW_NAME + " AS SELECT "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.CONVERSATION_ID
|
||||
+ " as " + Columns.CONVERSATION_ID + ", "
|
||||
+ DatabaseHelper.PARTS_TABLE + '.' + PartColumns.CONTENT_URI
|
||||
+ " as " + Columns.URI + ", "
|
||||
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FULL_NAME
|
||||
+ " as " + Columns.SENDER_FULL_NAME + ", "
|
||||
+ DatabaseHelper.PARTS_TABLE + '.' + PartColumns.CONTENT_URI
|
||||
+ " as " + Columns.CONTENT_URI + ", "
|
||||
// Use NULL as the thumbnail uri
|
||||
+ " NULL as " + Columns.THUMBNAIL_URI + ", "
|
||||
+ DatabaseHelper.PARTS_TABLE + '.' + PartColumns.CONTENT_TYPE
|
||||
+ " as " + Columns.CONTENT_TYPE + ", "
|
||||
//
|
||||
// Columns in addition to those specified by PhotoContract
|
||||
//
|
||||
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.DISPLAY_DESTINATION
|
||||
+ " as " + Columns.DISPLAY_DESTINATION + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP
|
||||
+ " as " + Columns.RECEIVED_TIMESTAMP + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.STATUS
|
||||
+ " as " + Columns.STATUS + " "
|
||||
|
||||
+ " FROM " + DatabaseHelper.MESSAGES_TABLE + " LEFT JOIN " + DatabaseHelper.PARTS_TABLE
|
||||
+ " ON (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns._ID
|
||||
+ "=" + DatabaseHelper.PARTS_TABLE + "." + PartColumns.MESSAGE_ID + ") "
|
||||
+ " LEFT JOIN " + DatabaseHelper.PARTICIPANTS_TABLE + " ON ("
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID
|
||||
+ '=' + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns._ID + ")"
|
||||
|
||||
// "content_type like 'image/%'"
|
||||
+ " WHERE " + DatabaseHelper.PARTS_TABLE + "." + PartColumns.CONTENT_TYPE
|
||||
+ " like '" + ContentType.IMAGE_PREFIX + "%'"
|
||||
|
||||
+ " ORDER BY "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " ASC, "
|
||||
+ DatabaseHelper.PARTS_TABLE + '.' + PartColumns._ID + " ASC";
|
||||
|
||||
static class Columns implements BaseColumns {
|
||||
static final String CONVERSATION_ID = MessageColumns.CONVERSATION_ID;
|
||||
static final String URI = PhotoViewColumns.URI;
|
||||
static final String SENDER_FULL_NAME = PhotoViewColumns.NAME;
|
||||
static final String CONTENT_URI = PhotoViewColumns.CONTENT_URI;
|
||||
static final String THUMBNAIL_URI = PhotoViewColumns.THUMBNAIL_URI;
|
||||
static final String CONTENT_TYPE = PhotoViewColumns.CONTENT_TYPE;
|
||||
// Columns in addition to those specified by PhotoContract
|
||||
static final String DISPLAY_DESTINATION = ParticipantColumns.DISPLAY_DESTINATION;
|
||||
static final String RECEIVED_TIMESTAMP = MessageColumns.RECEIVED_TIMESTAMP;
|
||||
static final String STATUS = MessageColumns.STATUS;
|
||||
}
|
||||
|
||||
public interface PhotoViewQuery {
|
||||
String[] PROJECTION = {
|
||||
PhotoViewColumns.URI,
|
||||
PhotoViewColumns.NAME,
|
||||
PhotoViewColumns.CONTENT_URI,
|
||||
PhotoViewColumns.THUMBNAIL_URI,
|
||||
PhotoViewColumns.CONTENT_TYPE,
|
||||
// Columns in addition to those specified by PhotoContract
|
||||
Columns.DISPLAY_DESTINATION,
|
||||
Columns.RECEIVED_TIMESTAMP,
|
||||
Columns.STATUS,
|
||||
};
|
||||
|
||||
int INDEX_URI = 0;
|
||||
int INDEX_SENDER_FULL_NAME = 1;
|
||||
int INDEX_CONTENT_URI = 2;
|
||||
int INDEX_THUMBNAIL_URI = 3;
|
||||
int INDEX_CONTENT_TYPE = 4;
|
||||
// Columns in addition to those specified by PhotoContract
|
||||
int INDEX_DISPLAY_DESTINATION = 5;
|
||||
int INDEX_RECEIVED_TIMESTAMP = 6;
|
||||
int INDEX_STATUS = 7;
|
||||
}
|
||||
|
||||
static String getViewName() {
|
||||
return VIEW_NAME;
|
||||
}
|
||||
|
||||
static String getCreateSql() {
|
||||
return CREATE_SQL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
/**
|
||||
* Holds parameters and data (such as content URI) for performing queries on the content provider.
|
||||
* This class could then be used to perform a query using either a BoundCursorLoader or querying
|
||||
* on the content resolver directly.
|
||||
*
|
||||
* This class is used for cases where the way to load a cursor is not fixed. For example,
|
||||
* when using ContactUtil to query for phone numbers, the ContactPickerFragment wants to use
|
||||
* a CursorLoader to asynchronously load the data and tie in nicely with its data binding
|
||||
* paradigm, whereas ContactRecipientAdapter wants to synchronously perform the query on the
|
||||
* worker thread.
|
||||
*/
|
||||
public class CursorQueryData {
|
||||
protected final Uri mUri;
|
||||
protected final String[] mProjection;
|
||||
protected final String mSelection;
|
||||
protected final String[] mSelectionArgs;
|
||||
protected final String mSortOrder;
|
||||
protected final Context mContext;
|
||||
|
||||
public CursorQueryData(final Context context, final Uri uri, final String[] projection,
|
||||
final String selection, final String[] selectionArgs, final String sortOrder) {
|
||||
mContext = context;
|
||||
mUri = uri;
|
||||
mProjection = projection;
|
||||
mSelection = selection;
|
||||
mSelectionArgs = selectionArgs;
|
||||
mSortOrder = sortOrder;
|
||||
}
|
||||
|
||||
public BoundCursorLoader createBoundCursorLoader(final String bindingId) {
|
||||
return new BoundCursorLoader(bindingId, mContext, mUri, mProjection, mSelection,
|
||||
mSelectionArgs, mSortOrder);
|
||||
}
|
||||
|
||||
public Cursor performSynchronousQuery() {
|
||||
Assert.isNotMainThread();
|
||||
if (mUri == null) {
|
||||
// See {@link #getEmptyQueryData}
|
||||
return null;
|
||||
} else {
|
||||
return mContext.getContentResolver().query(mUri, mProjection, mSelection,
|
||||
mSelectionArgs, mSortOrder);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of an invalid query. {@link #performSynchronousQuery} will return
|
||||
* a null Cursor.
|
||||
*/
|
||||
public static CursorQueryData getEmptyQueryData() {
|
||||
return new CursorQueryData(null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.action.Action;
|
||||
import com.android.messaging.datamodel.action.ActionService;
|
||||
import com.android.messaging.datamodel.action.BackgroundWorker;
|
||||
import com.android.messaging.datamodel.data.BlockedParticipantsData;
|
||||
import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener;
|
||||
import com.android.messaging.datamodel.data.ContactListItemData;
|
||||
import com.android.messaging.datamodel.data.ContactPickerData;
|
||||
import com.android.messaging.datamodel.data.ContactPickerData.ContactPickerDataListener;
|
||||
import com.android.messaging.datamodel.data.ConversationData;
|
||||
import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener;
|
||||
import com.android.messaging.datamodel.data.ConversationListData;
|
||||
import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener;
|
||||
import com.android.messaging.datamodel.data.DraftMessageData;
|
||||
import com.android.messaging.datamodel.data.LaunchConversationData;
|
||||
import com.android.messaging.datamodel.data.LaunchConversationData.LaunchConversationDataListener;
|
||||
import com.android.messaging.datamodel.data.MediaPickerData;
|
||||
import com.android.messaging.datamodel.data.MessagePartData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.datamodel.data.ParticipantListItemData;
|
||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData;
|
||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData.PeopleAndOptionsDataListener;
|
||||
import com.android.messaging.datamodel.data.PeopleOptionsItemData;
|
||||
import com.android.messaging.datamodel.data.SettingsData;
|
||||
import com.android.messaging.datamodel.data.SettingsData.SettingsDataListener;
|
||||
import com.android.messaging.datamodel.data.SubscriptionListData;
|
||||
import com.android.messaging.datamodel.data.VCardContactItemData;
|
||||
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
|
||||
|
||||
public abstract class DataModel {
|
||||
private String mFocusedConversation;
|
||||
private boolean mConversationListScrolledToNewestConversation;
|
||||
|
||||
public static DataModel get() {
|
||||
return Factory.get().getDataModel();
|
||||
}
|
||||
|
||||
public static void startActionService(final Action action) {
|
||||
get().getActionService().startAction(action);
|
||||
}
|
||||
|
||||
public static void scheduleAction(final Action action, final int code, final long delayMs) {
|
||||
get().getActionService().scheduleAction(action, code, delayMs);
|
||||
}
|
||||
|
||||
public abstract ConversationListData createConversationListData(final Context context,
|
||||
final ConversationListDataListener listener, final boolean archivedMode);
|
||||
|
||||
public abstract ConversationData createConversationData(final Context context,
|
||||
final ConversationDataListener listener, final String conversationId);
|
||||
|
||||
public abstract ContactListItemData createContactListItemData();
|
||||
|
||||
public abstract ContactPickerData createContactPickerData(final Context context,
|
||||
final ContactPickerDataListener listener);
|
||||
|
||||
public abstract MediaPickerData createMediaPickerData(final Context context);
|
||||
|
||||
public abstract LaunchConversationData createLaunchConversationData(
|
||||
LaunchConversationDataListener listener);
|
||||
|
||||
public abstract PeopleOptionsItemData createPeopleOptionsItemData(final Context context);
|
||||
|
||||
public abstract PeopleAndOptionsData createPeopleAndOptionsData(final String conversationId,
|
||||
final Context context, final PeopleAndOptionsDataListener listener);
|
||||
|
||||
public abstract VCardContactItemData createVCardContactItemData(final Context context,
|
||||
final MessagePartData data);
|
||||
|
||||
public abstract VCardContactItemData createVCardContactItemData(final Context context,
|
||||
final Uri vCardUri);
|
||||
|
||||
public abstract ParticipantListItemData createParticipantListItemData(
|
||||
final ParticipantData participant);
|
||||
|
||||
public abstract BlockedParticipantsData createBlockedParticipantsData(Context context,
|
||||
BlockedParticipantsDataListener listener);
|
||||
|
||||
public abstract SubscriptionListData createSubscriptonListData(Context context);
|
||||
|
||||
public abstract SettingsData createSettingsData(Context context, SettingsDataListener listener);
|
||||
|
||||
public abstract DraftMessageData createDraftMessageData(String conversationId);
|
||||
|
||||
public abstract ActionService getActionService();
|
||||
|
||||
public abstract BackgroundWorker getBackgroundWorkerForActionService();
|
||||
|
||||
@DoesNotRunOnMainThread
|
||||
public abstract DatabaseWrapper getDatabase();
|
||||
|
||||
// Allow DataModel to coordinate with activity lifetime events.
|
||||
public abstract void onActivityResume();
|
||||
|
||||
abstract void onCreateTables(final SQLiteDatabase db);
|
||||
|
||||
public void setFocusedConversation(final String conversationId) {
|
||||
mFocusedConversation = conversationId;
|
||||
}
|
||||
|
||||
public boolean isFocusedConversation(final String conversationId) {
|
||||
return !TextUtils.isEmpty(mFocusedConversation)
|
||||
&& TextUtils.equals(mFocusedConversation, conversationId);
|
||||
}
|
||||
|
||||
public void setConversationListScrolledToNewestConversation(
|
||||
final boolean scrolledToNewestConversation) {
|
||||
mConversationListScrolledToNewestConversation = scrolledToNewestConversation;
|
||||
}
|
||||
|
||||
public boolean isConversationListScrolledToNewestConversation() {
|
||||
return mConversationListScrolledToNewestConversation;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a new message is received in the specified conversation, will the user be able to
|
||||
* observe it in some UI within the app?
|
||||
* @param conversationId conversation with the new incoming message
|
||||
*/
|
||||
public boolean isNewMessageObservable(final String conversationId) {
|
||||
return isConversationListScrolledToNewestConversation()
|
||||
|| isFocusedConversation(conversationId);
|
||||
}
|
||||
|
||||
public abstract void onApplicationCreated();
|
||||
|
||||
public abstract SyncManager getSyncManager();
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* 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.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.telephony.SubscriptionManager;
|
||||
|
||||
import com.android.messaging.datamodel.action.ActionService;
|
||||
import com.android.messaging.datamodel.action.BackgroundWorker;
|
||||
import com.android.messaging.datamodel.action.FixupMessageStatusOnStartupAction;
|
||||
import com.android.messaging.datamodel.action.ProcessPendingMessagesAction;
|
||||
import com.android.messaging.datamodel.data.BlockedParticipantsData;
|
||||
import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener;
|
||||
import com.android.messaging.datamodel.data.ContactListItemData;
|
||||
import com.android.messaging.datamodel.data.ContactPickerData;
|
||||
import com.android.messaging.datamodel.data.ContactPickerData.ContactPickerDataListener;
|
||||
import com.android.messaging.datamodel.data.ConversationData;
|
||||
import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener;
|
||||
import com.android.messaging.datamodel.data.ConversationListData;
|
||||
import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener;
|
||||
import com.android.messaging.datamodel.data.DraftMessageData;
|
||||
import com.android.messaging.datamodel.data.LaunchConversationData;
|
||||
import com.android.messaging.datamodel.data.LaunchConversationData.LaunchConversationDataListener;
|
||||
import com.android.messaging.datamodel.data.MediaPickerData;
|
||||
import com.android.messaging.datamodel.data.MessagePartData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.datamodel.data.ParticipantListItemData;
|
||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData;
|
||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData.PeopleAndOptionsDataListener;
|
||||
import com.android.messaging.datamodel.data.PeopleOptionsItemData;
|
||||
import com.android.messaging.datamodel.data.SettingsData;
|
||||
import com.android.messaging.datamodel.data.SettingsData.SettingsDataListener;
|
||||
import com.android.messaging.datamodel.data.SubscriptionListData;
|
||||
import com.android.messaging.datamodel.data.VCardContactItemData;
|
||||
import com.android.messaging.sms.MmsConfig;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
|
||||
import com.android.messaging.util.ConnectivityUtil;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class DataModelImpl extends DataModel {
|
||||
private final Context mContext;
|
||||
private final ActionService mActionService;
|
||||
private final BackgroundWorker mDataModelWorker;
|
||||
private final DatabaseHelper mDatabaseHelper;
|
||||
private final SyncManager mSyncManager;
|
||||
|
||||
// Cached ConnectivityUtil subId->instance for N and beyond
|
||||
private static final ConcurrentHashMap<Integer, ConnectivityUtil>
|
||||
sConnectivityUtilInstanceCacheN = new ConcurrentHashMap<>();
|
||||
|
||||
public DataModelImpl(final Context context) {
|
||||
super();
|
||||
mContext = context;
|
||||
mActionService = new ActionService();
|
||||
mDataModelWorker = new BackgroundWorker();
|
||||
mDatabaseHelper = DatabaseHelper.getInstance(context);
|
||||
mSyncManager = new SyncManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConversationListData createConversationListData(final Context context,
|
||||
final ConversationListDataListener listener, final boolean archivedMode) {
|
||||
return new ConversationListData(context, listener, archivedMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConversationData createConversationData(final Context context,
|
||||
final ConversationDataListener listener, final String conversationId) {
|
||||
return new ConversationData(context, listener, conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContactListItemData createContactListItemData() {
|
||||
return new ContactListItemData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContactPickerData createContactPickerData(final Context context,
|
||||
final ContactPickerDataListener listener) {
|
||||
return new ContactPickerData(context, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockedParticipantsData createBlockedParticipantsData(
|
||||
final Context context, final BlockedParticipantsDataListener listener) {
|
||||
return new BlockedParticipantsData(context, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaPickerData createMediaPickerData(final Context context) {
|
||||
return new MediaPickerData(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LaunchConversationData createLaunchConversationData(
|
||||
final LaunchConversationDataListener listener) {
|
||||
return new LaunchConversationData(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeopleOptionsItemData createPeopleOptionsItemData(final Context context) {
|
||||
return new PeopleOptionsItemData(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeopleAndOptionsData createPeopleAndOptionsData(final String conversationId,
|
||||
final Context context, final PeopleAndOptionsDataListener listener) {
|
||||
return new PeopleAndOptionsData(conversationId, context, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VCardContactItemData createVCardContactItemData(final Context context,
|
||||
final MessagePartData data) {
|
||||
return new VCardContactItemData(context, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VCardContactItemData createVCardContactItemData(final Context context,
|
||||
final Uri vCardUri) {
|
||||
return new VCardContactItemData(context, vCardUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParticipantListItemData createParticipantListItemData(
|
||||
final ParticipantData participant) {
|
||||
return new ParticipantListItemData(participant);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SubscriptionListData createSubscriptonListData(Context context) {
|
||||
return new SubscriptionListData(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SettingsData createSettingsData(Context context, SettingsDataListener listener) {
|
||||
return new SettingsData(context, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DraftMessageData createDraftMessageData(String conversationId) {
|
||||
return new DraftMessageData(conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionService getActionService() {
|
||||
// We need to allow access to this on the UI thread since it's used to start actions.
|
||||
return mActionService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BackgroundWorker getBackgroundWorkerForActionService() {
|
||||
return mDataModelWorker;
|
||||
}
|
||||
|
||||
@Override
|
||||
@DoesNotRunOnMainThread
|
||||
public DatabaseWrapper getDatabase() {
|
||||
// We prevent the main UI thread from accessing the database since we have to allow
|
||||
// public access to this class to enable sub-packages to access data.
|
||||
Assert.isNotMainThread();
|
||||
return mDatabaseHelper.getDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SyncManager getSyncManager() {
|
||||
return mSyncManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
void onCreateTables(final SQLiteDatabase db) {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Rebuilt databases: reseting related state");
|
||||
// Clear other things that implicitly reference the DB
|
||||
SyncManager.resetLastSyncTimestamps();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResume() {
|
||||
// Perform an incremental sync and register for changes if necessary
|
||||
mSyncManager.updateSyncObserver(mContext);
|
||||
|
||||
// Trigger a participant refresh if needed, we should only need to refresh if there is
|
||||
// contact change while the activity was paused.
|
||||
ParticipantRefresh.refreshParticipantsIfNeeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationCreated() {
|
||||
createConnectivityUtilForEachActiveSubscription();
|
||||
|
||||
FixupMessageStatusOnStartupAction.fixupMessageStatus();
|
||||
ProcessPendingMessagesAction.processFirstPendingMessage();
|
||||
SyncManager.immediateSync();
|
||||
|
||||
// Start listening for subscription change events for refreshing any data associated
|
||||
// with subscriptions.
|
||||
PhoneUtils.getDefault().registerOnSubscriptionsChangedListener(
|
||||
new SubscriptionManager.OnSubscriptionsChangedListener() {
|
||||
@Override
|
||||
public void onSubscriptionsChanged() {
|
||||
// TODO: This dynamically changes the mms config that app is
|
||||
// currently using. It may cause inconsistency in some cases. We need
|
||||
// to check the usage of mms config and handle the dynamic change
|
||||
// gracefully
|
||||
MmsConfig.loadAsync();
|
||||
ParticipantRefresh.refreshSelfParticipants();
|
||||
createConnectivityUtilForEachActiveSubscription();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createConnectivityUtilForEachActiveSubscription() {
|
||||
PhoneUtils.forEachActiveSubscription(subId -> {
|
||||
// Create the ConnectivityUtil instance for given subId if absent.
|
||||
if (subId <= ParticipantData.DEFAULT_SELF_SUB_ID) {
|
||||
subId = PhoneUtils.getDefault().getDefaultSmsSubscriptionId();
|
||||
}
|
||||
if (!sConnectivityUtilInstanceCacheN.containsKey(subId)) {
|
||||
sConnectivityUtilInstanceCacheN.put(
|
||||
subId, new ConnectivityUtil(mContext, subId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static ConnectivityUtil getConnectivityUtil(final int subId) {
|
||||
return sConnectivityUtilInstanceCacheN.get(subId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,794 @@
|
||||
/*
|
||||
* 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.database.Cursor;
|
||||
import android.database.SQLException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Allows access to the SQL database. This is package private.
|
||||
*/
|
||||
public class DatabaseHelper extends SQLiteOpenHelper {
|
||||
public static final String DATABASE_NAME = "bugle_db";
|
||||
|
||||
private static int getDatabaseVersion(final Context context) {
|
||||
return Integer.parseInt(context.getResources().getString(R.string.database_version));
|
||||
}
|
||||
|
||||
/**
|
||||
* Table containing names of all other tables and views.
|
||||
* TODO(rtenneti): Fix the following special SQLLite table name when SQLLite changes.
|
||||
*/
|
||||
private static final String PRIMARY_TABLE = "sqlite_master";
|
||||
/** Column containing the name of the tables and views */
|
||||
private static final String[] PRIMARY_COLUMNS = new String[] { "name", };
|
||||
|
||||
// Table names
|
||||
public static final String CONVERSATIONS_TABLE = "conversations";
|
||||
public static final String MESSAGES_TABLE = "messages";
|
||||
public static final String PARTS_TABLE = "parts";
|
||||
public static final String PARTICIPANTS_TABLE = "participants";
|
||||
public static final String CONVERSATION_PARTICIPANTS_TABLE = "conversation_participants";
|
||||
|
||||
// Views
|
||||
static final String DRAFT_PARTS_VIEW = "draft_parts_view";
|
||||
|
||||
// Conversations table schema
|
||||
public static class ConversationColumns implements BaseColumns {
|
||||
/* SMS/MMS Thread ID from the system provider */
|
||||
public static final String SMS_THREAD_ID = "sms_thread_id";
|
||||
|
||||
/* Display name for the conversation */
|
||||
public static final String NAME = "name";
|
||||
|
||||
/* Latest Message ID for the read status to display in conversation list */
|
||||
public static final String LATEST_MESSAGE_ID = "latest_message_id";
|
||||
|
||||
/* Latest text snippet for display in conversation list */
|
||||
public static final String SNIPPET_TEXT = "snippet_text";
|
||||
|
||||
/* Latest text subject for display in conversation list, empty string if none exists */
|
||||
public static final String SUBJECT_TEXT = "subject_text";
|
||||
|
||||
/* Preview Uri */
|
||||
public static final String PREVIEW_URI = "preview_uri";
|
||||
|
||||
/* The preview uri's content type */
|
||||
public static final String PREVIEW_CONTENT_TYPE = "preview_content_type";
|
||||
|
||||
/* If we should display the current draft snippet/preview pair or snippet/preview pair */
|
||||
public static final String SHOW_DRAFT = "show_draft";
|
||||
|
||||
/* Latest draft text subject for display in conversation list, empty string if none exists*/
|
||||
public static final String DRAFT_SUBJECT_TEXT = "draft_subject_text";
|
||||
|
||||
/* Latest draft text snippet for display, empty string if none exists */
|
||||
public static final String DRAFT_SNIPPET_TEXT = "draft_snippet_text";
|
||||
|
||||
/* Draft Preview Uri, empty string if none exists */
|
||||
public static final String DRAFT_PREVIEW_URI = "draft_preview_uri";
|
||||
|
||||
/* The preview uri's content type */
|
||||
public static final String DRAFT_PREVIEW_CONTENT_TYPE = "draft_preview_content_type";
|
||||
|
||||
/* If this conversation is archived */
|
||||
public static final String ARCHIVE_STATUS = "archive_status";
|
||||
|
||||
/* Timestamp for sorting purposes */
|
||||
public static final String SORT_TIMESTAMP = "sort_timestamp";
|
||||
|
||||
/* Last read message timestamp */
|
||||
public static final String LAST_READ_TIMESTAMP = "last_read_timestamp";
|
||||
|
||||
/* Avatar for the conversation. Could be for group of individual */
|
||||
public static final String ICON = "icon";
|
||||
|
||||
/* Participant contact ID if this conversation has a single participant. -1 otherwise */
|
||||
public static final String PARTICIPANT_CONTACT_ID = "participant_contact_id";
|
||||
|
||||
/* Participant lookup key if this conversation has a single participant. null otherwise */
|
||||
public static final String PARTICIPANT_LOOKUP_KEY = "participant_lookup_key";
|
||||
|
||||
/*
|
||||
* Participant's normalized destination if this conversation has a single participant.
|
||||
* null otherwise.
|
||||
*/
|
||||
public static final String OTHER_PARTICIPANT_NORMALIZED_DESTINATION =
|
||||
"participant_normalized_destination";
|
||||
|
||||
/* Default self participant for the conversation */
|
||||
public static final String CURRENT_SELF_ID = "current_self_id";
|
||||
|
||||
/* Participant count not including self (so will be 1 for 1:1 or bigger for group) */
|
||||
public static final String PARTICIPANT_COUNT = "participant_count";
|
||||
|
||||
/* Conversation recipients include email address */
|
||||
public static final String INCLUDE_EMAIL_ADDRESS = "include_email_addr";
|
||||
|
||||
// Record the last received sms's service center info if it indicates that the reply path
|
||||
// is present (TP-Reply-Path), so that we could use it for the subsequent message to send.
|
||||
// Refer to TS 23.040 D.6 and SmsMessageSender.java in Android Messaging app.
|
||||
public static final String SMS_SERVICE_CENTER = "sms_service_center";
|
||||
|
||||
// A conversation is enterprise if one of the participant is a enterprise contact.
|
||||
public static final String IS_ENTERPRISE = "IS_ENTERPRISE";
|
||||
}
|
||||
|
||||
// Conversation table SQL
|
||||
private static final String CREATE_CONVERSATIONS_TABLE_SQL =
|
||||
"CREATE TABLE " + CONVERSATIONS_TABLE + "("
|
||||
+ ConversationColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
// TODO : Int? Required not default?
|
||||
+ ConversationColumns.SMS_THREAD_ID + " INT DEFAULT(0), "
|
||||
+ ConversationColumns.NAME + " TEXT, "
|
||||
+ ConversationColumns.LATEST_MESSAGE_ID + " INT, "
|
||||
+ ConversationColumns.SNIPPET_TEXT + " TEXT, "
|
||||
+ ConversationColumns.SUBJECT_TEXT + " TEXT, "
|
||||
+ ConversationColumns.PREVIEW_URI + " TEXT, "
|
||||
+ ConversationColumns.PREVIEW_CONTENT_TYPE + " TEXT, "
|
||||
+ ConversationColumns.SHOW_DRAFT + " INT DEFAULT(0), "
|
||||
+ ConversationColumns.DRAFT_SNIPPET_TEXT + " TEXT, "
|
||||
+ ConversationColumns.DRAFT_SUBJECT_TEXT + " TEXT, "
|
||||
+ ConversationColumns.DRAFT_PREVIEW_URI + " TEXT, "
|
||||
+ ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE + " TEXT, "
|
||||
+ ConversationColumns.ARCHIVE_STATUS + " INT DEFAULT(0), "
|
||||
+ ConversationColumns.SORT_TIMESTAMP + " INT DEFAULT(0), "
|
||||
+ ConversationColumns.LAST_READ_TIMESTAMP + " INT DEFAULT(0), "
|
||||
+ ConversationColumns.ICON + " TEXT, "
|
||||
+ ConversationColumns.PARTICIPANT_CONTACT_ID + " INT DEFAULT ( "
|
||||
+ ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED + "), "
|
||||
+ ConversationColumns.PARTICIPANT_LOOKUP_KEY + " TEXT, "
|
||||
+ ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION + " TEXT, "
|
||||
+ ConversationColumns.CURRENT_SELF_ID + " TEXT, "
|
||||
+ ConversationColumns.PARTICIPANT_COUNT + " INT DEFAULT(0), "
|
||||
+ ConversationColumns.INCLUDE_EMAIL_ADDRESS + " INT DEFAULT(0), "
|
||||
+ ConversationColumns.SMS_SERVICE_CENTER + " TEXT ,"
|
||||
+ ConversationColumns.IS_ENTERPRISE + " INT DEFAULT(0)"
|
||||
+ ");";
|
||||
|
||||
private static final String CONVERSATIONS_TABLE_SMS_THREAD_ID_INDEX_SQL =
|
||||
"CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.SMS_THREAD_ID
|
||||
+ " ON " + CONVERSATIONS_TABLE
|
||||
+ "(" + ConversationColumns.SMS_THREAD_ID + ")";
|
||||
|
||||
private static final String CONVERSATIONS_TABLE_ARCHIVE_STATUS_INDEX_SQL =
|
||||
"CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.ARCHIVE_STATUS
|
||||
+ " ON " + CONVERSATIONS_TABLE
|
||||
+ "(" + ConversationColumns.ARCHIVE_STATUS + ")";
|
||||
|
||||
private static final String CONVERSATIONS_TABLE_SORT_TIMESTAMP_INDEX_SQL =
|
||||
"CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.SORT_TIMESTAMP
|
||||
+ " ON " + CONVERSATIONS_TABLE
|
||||
+ "(" + ConversationColumns.SORT_TIMESTAMP + ")";
|
||||
|
||||
// Messages table schema
|
||||
public static class MessageColumns implements BaseColumns {
|
||||
/* conversation id that this message belongs to */
|
||||
public static final String CONVERSATION_ID = "conversation_id";
|
||||
|
||||
/* participant which send this message */
|
||||
public static final String SENDER_PARTICIPANT_ID = "sender_id";
|
||||
|
||||
/* This is bugle's internal status for the message */
|
||||
public static final String STATUS = "message_status";
|
||||
|
||||
/* Type of message: SMS, MMS or MMS notification */
|
||||
public static final String PROTOCOL = "message_protocol";
|
||||
|
||||
/* This is the time that the sender sent the message */
|
||||
public static final String SENT_TIMESTAMP = "sent_timestamp";
|
||||
|
||||
/* Time that we received the message on this device */
|
||||
public static final String RECEIVED_TIMESTAMP = "received_timestamp";
|
||||
|
||||
/* When the message has been seen by a user in a notification */
|
||||
public static final String SEEN = "seen";
|
||||
|
||||
/* When the message has been read by a user */
|
||||
public static final String READ = "read";
|
||||
|
||||
/* participant representing the sim which processed this message */
|
||||
public static final String SELF_PARTICIPANT_ID = "self_id";
|
||||
|
||||
/*
|
||||
* Time when a retry is initiated. This is used to compute the retry window
|
||||
* when we retry sending/downloading a message.
|
||||
*/
|
||||
public static final String RETRY_START_TIMESTAMP = "retry_start_timestamp";
|
||||
|
||||
// Columns which map to the SMS provider
|
||||
|
||||
/* Message ID from the platform provider */
|
||||
public static final String SMS_MESSAGE_URI = "sms_message_uri";
|
||||
|
||||
/* The message priority for MMS message */
|
||||
public static final String SMS_PRIORITY = "sms_priority";
|
||||
|
||||
/* The message size for MMS message */
|
||||
public static final String SMS_MESSAGE_SIZE = "sms_message_size";
|
||||
|
||||
/* The subject for MMS message */
|
||||
public static final String MMS_SUBJECT = "mms_subject";
|
||||
|
||||
/* Transaction id for MMS notificaiton */
|
||||
public static final String MMS_TRANSACTION_ID = "mms_transaction_id";
|
||||
|
||||
/* Content location for MMS notificaiton */
|
||||
public static final String MMS_CONTENT_LOCATION = "mms_content_location";
|
||||
|
||||
/* The expiry time (ms) for MMS message */
|
||||
public static final String MMS_EXPIRY = "mms_expiry";
|
||||
|
||||
/* The detailed status (RESPONSE_STATUS or RETRIEVE_STATUS) for MMS message */
|
||||
public static final String RAW_TELEPHONY_STATUS = "raw_status";
|
||||
}
|
||||
|
||||
// Messages table SQL
|
||||
private static final String CREATE_MESSAGES_TABLE_SQL =
|
||||
"CREATE TABLE " + MESSAGES_TABLE + " ("
|
||||
+ MessageColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ MessageColumns.CONVERSATION_ID + " INT, "
|
||||
+ MessageColumns.SENDER_PARTICIPANT_ID + " INT, "
|
||||
+ MessageColumns.SENT_TIMESTAMP + " INT DEFAULT(0), "
|
||||
+ MessageColumns.RECEIVED_TIMESTAMP + " INT DEFAULT(0), "
|
||||
+ MessageColumns.PROTOCOL + " INT DEFAULT(0), "
|
||||
+ MessageColumns.STATUS + " INT DEFAULT(0), "
|
||||
+ MessageColumns.SEEN + " INT DEFAULT(0), "
|
||||
+ MessageColumns.READ + " INT DEFAULT(0), "
|
||||
+ MessageColumns.SMS_MESSAGE_URI + " TEXT, "
|
||||
+ MessageColumns.SMS_PRIORITY + " INT DEFAULT(0), "
|
||||
+ MessageColumns.SMS_MESSAGE_SIZE + " INT DEFAULT(0), "
|
||||
+ MessageColumns.MMS_SUBJECT + " TEXT, "
|
||||
+ MessageColumns.MMS_TRANSACTION_ID + " TEXT, "
|
||||
+ MessageColumns.MMS_CONTENT_LOCATION + " TEXT, "
|
||||
+ MessageColumns.MMS_EXPIRY + " INT DEFAULT(0), "
|
||||
+ MessageColumns.RAW_TELEPHONY_STATUS + " INT DEFAULT(0), "
|
||||
+ MessageColumns.SELF_PARTICIPANT_ID + " INT, "
|
||||
+ MessageColumns.RETRY_START_TIMESTAMP + " INT DEFAULT(0), "
|
||||
+ "FOREIGN KEY (" + MessageColumns.CONVERSATION_ID + ") REFERENCES "
|
||||
+ CONVERSATIONS_TABLE + "(" + ConversationColumns._ID + ") ON DELETE CASCADE "
|
||||
+ "FOREIGN KEY (" + MessageColumns.SENDER_PARTICIPANT_ID + ") REFERENCES "
|
||||
+ PARTICIPANTS_TABLE + "(" + ParticipantColumns._ID + ") ON DELETE SET NULL "
|
||||
+ "FOREIGN KEY (" + MessageColumns.SELF_PARTICIPANT_ID + ") REFERENCES "
|
||||
+ PARTICIPANTS_TABLE + "(" + ParticipantColumns._ID + ") ON DELETE SET NULL "
|
||||
+ ");";
|
||||
|
||||
// Primary sort index for messages table : by conversation id, status, received timestamp.
|
||||
private static final String MESSAGES_TABLE_SORT_INDEX_SQL =
|
||||
"CREATE INDEX index_" + MESSAGES_TABLE + "_sort ON " + MESSAGES_TABLE + "("
|
||||
+ MessageColumns.CONVERSATION_ID + ", "
|
||||
+ MessageColumns.STATUS + ", "
|
||||
+ MessageColumns.RECEIVED_TIMESTAMP + ")";
|
||||
|
||||
private static final String MESSAGES_TABLE_STATUS_SEEN_INDEX_SQL =
|
||||
"CREATE INDEX index_" + MESSAGES_TABLE + "_status_seen ON " + MESSAGES_TABLE + "("
|
||||
+ MessageColumns.STATUS + ", "
|
||||
+ MessageColumns.SEEN + ")";
|
||||
|
||||
// Parts table schema
|
||||
// A part may contain text or a media url, but not both.
|
||||
public static class PartColumns implements BaseColumns {
|
||||
/* message id that this part belongs to */
|
||||
public static final String MESSAGE_ID = "message_id";
|
||||
|
||||
/* conversation id that this part belongs to */
|
||||
public static final String CONVERSATION_ID = "conversation_id";
|
||||
|
||||
/* text for this part */
|
||||
public static final String TEXT = "text";
|
||||
|
||||
/* content uri for this part */
|
||||
public static final String CONTENT_URI = "uri";
|
||||
|
||||
/* content type for this part */
|
||||
public static final String CONTENT_TYPE = "content_type";
|
||||
|
||||
/* cached width for this part (for layout while loading) */
|
||||
public static final String WIDTH = "width";
|
||||
|
||||
/* cached height for this part (for layout while loading) */
|
||||
public static final String HEIGHT = "height";
|
||||
|
||||
/* de-normalized copy of timestamp from the messages table. This is populated
|
||||
* via an insert trigger on the parts table.
|
||||
*/
|
||||
public static final String TIMESTAMP = "timestamp";
|
||||
}
|
||||
|
||||
// Message part table SQL
|
||||
private static final String CREATE_PARTS_TABLE_SQL =
|
||||
"CREATE TABLE " + PARTS_TABLE + "("
|
||||
+ PartColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
+ PartColumns.MESSAGE_ID + " INT,"
|
||||
+ PartColumns.TEXT + " TEXT,"
|
||||
+ PartColumns.CONTENT_URI + " TEXT,"
|
||||
+ PartColumns.CONTENT_TYPE + " TEXT,"
|
||||
+ PartColumns.WIDTH + " INT DEFAULT("
|
||||
+ MessagingContentProvider.UNSPECIFIED_SIZE + "),"
|
||||
+ PartColumns.HEIGHT + " INT DEFAULT("
|
||||
+ MessagingContentProvider.UNSPECIFIED_SIZE + "),"
|
||||
+ PartColumns.TIMESTAMP + " INT, "
|
||||
+ PartColumns.CONVERSATION_ID + " INT NOT NULL,"
|
||||
+ "FOREIGN KEY (" + PartColumns.MESSAGE_ID + ") REFERENCES "
|
||||
+ MESSAGES_TABLE + "(" + MessageColumns._ID + ") ON DELETE CASCADE "
|
||||
+ "FOREIGN KEY (" + PartColumns.CONVERSATION_ID + ") REFERENCES "
|
||||
+ CONVERSATIONS_TABLE + "(" + ConversationColumns._ID + ") ON DELETE CASCADE "
|
||||
+ ");";
|
||||
|
||||
public static final String CREATE_PARTS_TRIGGER_SQL =
|
||||
"CREATE TRIGGER " + PARTS_TABLE + "_TRIGGER" + " AFTER INSERT ON " + PARTS_TABLE
|
||||
+ " FOR EACH ROW "
|
||||
+ " BEGIN UPDATE " + PARTS_TABLE
|
||||
+ " SET " + PartColumns.TIMESTAMP + "="
|
||||
+ " (SELECT received_timestamp FROM " + MESSAGES_TABLE + " WHERE " + MESSAGES_TABLE
|
||||
+ "." + MessageColumns._ID + "=" + "NEW." + PartColumns.MESSAGE_ID + ")"
|
||||
+ " WHERE " + PARTS_TABLE + "." + PartColumns._ID + "=" + "NEW." + PartColumns._ID
|
||||
+ "; END";
|
||||
|
||||
public static final String CREATE_MESSAGES_TRIGGER_SQL =
|
||||
"CREATE TRIGGER " + MESSAGES_TABLE + "_TRIGGER" + " AFTER UPDATE OF "
|
||||
+ MessageColumns.RECEIVED_TIMESTAMP + " ON " + MESSAGES_TABLE
|
||||
+ " FOR EACH ROW BEGIN UPDATE " + PARTS_TABLE + " SET " + PartColumns.TIMESTAMP
|
||||
+ " = NEW." + MessageColumns.RECEIVED_TIMESTAMP + " WHERE " + PARTS_TABLE + "."
|
||||
+ PartColumns.MESSAGE_ID + " = NEW." + MessageColumns._ID
|
||||
+ "; END;";
|
||||
|
||||
// Primary sort index for parts table : by message_id
|
||||
private static final String PARTS_TABLE_MESSAGE_INDEX_SQL =
|
||||
"CREATE INDEX index_" + PARTS_TABLE + "_message_id ON " + PARTS_TABLE + "("
|
||||
+ PartColumns.MESSAGE_ID + ")";
|
||||
|
||||
// Participants table schema
|
||||
public static class ParticipantColumns implements BaseColumns {
|
||||
/* The subscription id for the sim associated with this self participant.
|
||||
* Introduced in L. For earlier versions will always be default_sub_id (-1).
|
||||
* For multi sim devices (or cases where the sim was changed) single device
|
||||
* may have several different sub_id values */
|
||||
public static final String SUB_ID = "sub_id";
|
||||
|
||||
/* The slot of the active SIM (inserted in the device) for this self-participant. If the
|
||||
* self-participant doesn't correspond to any active SIM, this will be
|
||||
* {@link android.telephony.SubscriptionManager#INVALID_SLOT_ID}.
|
||||
* The column is ignored for all non-self participants.
|
||||
*/
|
||||
public static final String SIM_SLOT_ID = "sim_slot_id";
|
||||
|
||||
/* The phone number stored in a standard E164 format if possible. This is unique for a
|
||||
* given participant. We can't handle multiple participants with the same phone number
|
||||
* since we don't know which of them a message comes from. This can also be an email
|
||||
* address, in which case this is the same as the displayed address */
|
||||
public static final String NORMALIZED_DESTINATION = "normalized_destination";
|
||||
|
||||
/* The phone number as originally supplied and used for dialing. Not necessarily in E164
|
||||
* format or unique */
|
||||
public static final String SEND_DESTINATION = "send_destination";
|
||||
|
||||
/* The user-friendly formatting of the phone number according to the region setting of
|
||||
* the device when the row was added. */
|
||||
public static final String DISPLAY_DESTINATION = "display_destination";
|
||||
|
||||
/* A string with this participant's full name or a pretty printed phone number */
|
||||
public static final String FULL_NAME = "full_name";
|
||||
|
||||
/* A string with just this participant's first name */
|
||||
public static final String FIRST_NAME = "first_name";
|
||||
|
||||
/* A local URI to an asset for the icon for this participant */
|
||||
public static final String PROFILE_PHOTO_URI = "profile_photo_uri";
|
||||
|
||||
/* Contact id for matching local contact for this participant */
|
||||
public static final String CONTACT_ID = "contact_id";
|
||||
|
||||
/* String that contains hints on how to find contact information in a contact lookup */
|
||||
public static final String LOOKUP_KEY = "lookup_key";
|
||||
|
||||
/* If this participant is blocked */
|
||||
public static final String BLOCKED = "blocked";
|
||||
|
||||
/* The color of the subscription (FOR SELF PARTICIPANTS ONLY) */
|
||||
public static final String SUBSCRIPTION_COLOR = "subscription_color";
|
||||
|
||||
/* The name of the subscription (FOR SELF PARTICIPANTS ONLY) */
|
||||
public static final String SUBSCRIPTION_NAME = "subscription_name";
|
||||
|
||||
/* The exact destination stored in Contacts for this participant */
|
||||
public static final String CONTACT_DESTINATION = "contact_destination";
|
||||
}
|
||||
|
||||
// Participants table SQL
|
||||
private static final String CREATE_PARTICIPANTS_TABLE_SQL =
|
||||
"CREATE TABLE " + PARTICIPANTS_TABLE + "("
|
||||
+ ParticipantColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
+ ParticipantColumns.SUB_ID + " INT DEFAULT("
|
||||
+ ParticipantData.OTHER_THAN_SELF_SUB_ID + "),"
|
||||
+ ParticipantColumns.SIM_SLOT_ID + " INT DEFAULT("
|
||||
+ ParticipantData.INVALID_SLOT_ID + "),"
|
||||
+ ParticipantColumns.NORMALIZED_DESTINATION + " TEXT,"
|
||||
+ ParticipantColumns.SEND_DESTINATION + " TEXT,"
|
||||
+ ParticipantColumns.DISPLAY_DESTINATION + " TEXT,"
|
||||
+ ParticipantColumns.FULL_NAME + " TEXT,"
|
||||
+ ParticipantColumns.FIRST_NAME + " TEXT,"
|
||||
+ ParticipantColumns.PROFILE_PHOTO_URI + " TEXT, "
|
||||
+ ParticipantColumns.CONTACT_ID + " INT DEFAULT( "
|
||||
+ ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED + "), "
|
||||
+ ParticipantColumns.LOOKUP_KEY + " STRING, "
|
||||
+ ParticipantColumns.BLOCKED + " INT DEFAULT(0), "
|
||||
+ ParticipantColumns.SUBSCRIPTION_NAME + " TEXT, "
|
||||
+ ParticipantColumns.SUBSCRIPTION_COLOR + " INT DEFAULT(0), "
|
||||
+ ParticipantColumns.CONTACT_DESTINATION + " TEXT, "
|
||||
+ "UNIQUE (" + ParticipantColumns.NORMALIZED_DESTINATION + ", "
|
||||
+ ParticipantColumns.SUB_ID + ") ON CONFLICT FAIL" + ");";
|
||||
|
||||
private static final String CREATE_SELF_PARTICIPANT_SQL =
|
||||
"INSERT INTO " + PARTICIPANTS_TABLE
|
||||
+ " ( " + ParticipantColumns.SUB_ID + " ) VALUES ( %s )";
|
||||
|
||||
static String getCreateSelfParticipantSql(int subId) {
|
||||
return String.format(CREATE_SELF_PARTICIPANT_SQL, subId);
|
||||
}
|
||||
|
||||
// Conversation Participants table schema - contains a list of participants excluding the user
|
||||
// in a given conversation.
|
||||
public static class ConversationParticipantsColumns implements BaseColumns {
|
||||
/* participant id of someone in this conversation */
|
||||
public static final String PARTICIPANT_ID = "participant_id";
|
||||
|
||||
/* conversation id that this participant belongs to */
|
||||
public static final String CONVERSATION_ID = "conversation_id";
|
||||
}
|
||||
|
||||
// Conversation Participants table SQL
|
||||
private static final String CREATE_CONVERSATION_PARTICIPANTS_TABLE_SQL =
|
||||
"CREATE TABLE " + CONVERSATION_PARTICIPANTS_TABLE + "("
|
||||
+ ConversationParticipantsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
+ ConversationParticipantsColumns.CONVERSATION_ID + " INT,"
|
||||
+ ConversationParticipantsColumns.PARTICIPANT_ID + " INT,"
|
||||
+ "UNIQUE (" + ConversationParticipantsColumns.CONVERSATION_ID + ","
|
||||
+ ConversationParticipantsColumns.PARTICIPANT_ID + ") ON CONFLICT FAIL, "
|
||||
+ "FOREIGN KEY (" + ConversationParticipantsColumns.CONVERSATION_ID + ") "
|
||||
+ "REFERENCES " + CONVERSATIONS_TABLE + "(" + ConversationColumns._ID + ")"
|
||||
+ " ON DELETE CASCADE "
|
||||
+ "FOREIGN KEY (" + ConversationParticipantsColumns.PARTICIPANT_ID + ")"
|
||||
+ " REFERENCES " + PARTICIPANTS_TABLE + "(" + ParticipantColumns._ID + "));";
|
||||
|
||||
// Primary access pattern for conversation participants is to look them up for a specific
|
||||
// conversation.
|
||||
private static final String CONVERSATION_PARTICIPANTS_TABLE_CONVERSATION_ID_INDEX_SQL =
|
||||
"CREATE INDEX index_" + CONVERSATION_PARTICIPANTS_TABLE + "_"
|
||||
+ ConversationParticipantsColumns.CONVERSATION_ID
|
||||
+ " ON " + CONVERSATION_PARTICIPANTS_TABLE
|
||||
+ "(" + ConversationParticipantsColumns.CONVERSATION_ID + ")";
|
||||
|
||||
// View for getting parts which are for draft messages.
|
||||
static final String DRAFT_PARTS_VIEW_SQL = "CREATE VIEW " +
|
||||
DRAFT_PARTS_VIEW + " AS SELECT "
|
||||
+ PARTS_TABLE + '.' + PartColumns._ID
|
||||
+ " as " + PartColumns._ID + ", "
|
||||
+ PARTS_TABLE + '.' + PartColumns.MESSAGE_ID
|
||||
+ " as " + PartColumns.MESSAGE_ID + ", "
|
||||
+ PARTS_TABLE + '.' + PartColumns.TEXT
|
||||
+ " as " + PartColumns.TEXT + ", "
|
||||
+ PARTS_TABLE + '.' + PartColumns.CONTENT_URI
|
||||
+ " as " + PartColumns.CONTENT_URI + ", "
|
||||
+ PARTS_TABLE + '.' + PartColumns.CONTENT_TYPE
|
||||
+ " as " + PartColumns.CONTENT_TYPE + ", "
|
||||
+ PARTS_TABLE + '.' + PartColumns.WIDTH
|
||||
+ " as " + PartColumns.WIDTH + ", "
|
||||
+ PARTS_TABLE + '.' + PartColumns.HEIGHT
|
||||
+ " as " + PartColumns.HEIGHT + ", "
|
||||
+ MESSAGES_TABLE + '.' + MessageColumns.CONVERSATION_ID
|
||||
+ " as " + MessageColumns.CONVERSATION_ID + " "
|
||||
+ " FROM " + MESSAGES_TABLE + " LEFT JOIN " + PARTS_TABLE + " ON ("
|
||||
+ MESSAGES_TABLE + "." + MessageColumns._ID
|
||||
+ "=" + PARTS_TABLE + "." + PartColumns.MESSAGE_ID + ")"
|
||||
// Exclude draft messages from main view
|
||||
+ " WHERE " + MESSAGES_TABLE + "." + MessageColumns.STATUS
|
||||
+ " = " + MessageData.BUGLE_STATUS_OUTGOING_DRAFT;
|
||||
|
||||
// List of all our SQL tables
|
||||
private static final String[] CREATE_TABLE_SQLS = new String[] {
|
||||
CREATE_CONVERSATIONS_TABLE_SQL,
|
||||
CREATE_MESSAGES_TABLE_SQL,
|
||||
CREATE_PARTS_TABLE_SQL,
|
||||
CREATE_PARTICIPANTS_TABLE_SQL,
|
||||
CREATE_CONVERSATION_PARTICIPANTS_TABLE_SQL,
|
||||
};
|
||||
|
||||
// List of all our indices
|
||||
private static final String[] CREATE_INDEX_SQLS = new String[] {
|
||||
CONVERSATIONS_TABLE_SMS_THREAD_ID_INDEX_SQL,
|
||||
CONVERSATIONS_TABLE_ARCHIVE_STATUS_INDEX_SQL,
|
||||
CONVERSATIONS_TABLE_SORT_TIMESTAMP_INDEX_SQL,
|
||||
MESSAGES_TABLE_SORT_INDEX_SQL,
|
||||
MESSAGES_TABLE_STATUS_SEEN_INDEX_SQL,
|
||||
PARTS_TABLE_MESSAGE_INDEX_SQL,
|
||||
CONVERSATION_PARTICIPANTS_TABLE_CONVERSATION_ID_INDEX_SQL,
|
||||
};
|
||||
|
||||
// List of all our SQL triggers
|
||||
private static final String[] CREATE_TRIGGER_SQLS = new String[] {
|
||||
CREATE_PARTS_TRIGGER_SQL,
|
||||
CREATE_MESSAGES_TRIGGER_SQL,
|
||||
};
|
||||
|
||||
// List of all our views
|
||||
private static final String[] CREATE_VIEW_SQLS = new String[] {
|
||||
ConversationListItemData.getConversationListViewSql(),
|
||||
ConversationImagePartsView.getCreateSql(),
|
||||
DRAFT_PARTS_VIEW_SQL,
|
||||
};
|
||||
|
||||
private static final Object sLock = new Object();
|
||||
private final Context mApplicationContext;
|
||||
private static DatabaseHelper sHelperInstance; // Protected by sLock.
|
||||
|
||||
private final Object mDatabaseWrapperLock = new Object();
|
||||
private DatabaseWrapper mDatabaseWrapper; // Protected by mDatabaseWrapperLock.
|
||||
private final DatabaseUpgradeHelper mUpgradeHelper = new DatabaseUpgradeHelper();
|
||||
|
||||
/**
|
||||
* Get a (singleton) instance of {@link DatabaseHelper}, creating one if there isn't one yet.
|
||||
* This is the only public method for getting a new instance of the class.
|
||||
* @param context Should be the application context (or something that will live for the
|
||||
* lifetime of the application).
|
||||
* @return The current (or a new) DatabaseHelper instance.
|
||||
*/
|
||||
public static DatabaseHelper getInstance(final Context context) {
|
||||
synchronized (sLock) {
|
||||
if (sHelperInstance == null) {
|
||||
sHelperInstance = new DatabaseHelper(context);
|
||||
}
|
||||
return sHelperInstance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor, used from {@link #getInstance()}.
|
||||
* @param context Should be the application context (or something that will live for the
|
||||
* lifetime of the application).
|
||||
*/
|
||||
private DatabaseHelper(final Context context) {
|
||||
super(context, DATABASE_NAME, null, getDatabaseVersion(context), null);
|
||||
mApplicationContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the (singleton) instance of @{link DatabaseWrapper}.
|
||||
* <p>The database is always opened as a writeable database.
|
||||
* @return The current (or a new) DatabaseWrapper instance.
|
||||
*/
|
||||
@DoesNotRunOnMainThread
|
||||
DatabaseWrapper getDatabase() {
|
||||
// We prevent the main UI thread from accessing the database here since we have to allow
|
||||
// public access to this class to enable sub-packages to access data.
|
||||
Assert.isNotMainThread();
|
||||
|
||||
synchronized (mDatabaseWrapperLock) {
|
||||
if (mDatabaseWrapper == null) {
|
||||
mDatabaseWrapper = new DatabaseWrapper(mApplicationContext, getWritableDatabase());
|
||||
}
|
||||
return mDatabaseWrapper;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
|
||||
mUpgradeHelper.onDowngrade(db, oldVersion, newVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops and recreates all tables.
|
||||
*/
|
||||
public static void rebuildTables(final SQLiteDatabase db) {
|
||||
// Drop tables first, then views, and indices.
|
||||
dropAllTables(db);
|
||||
dropAllViews(db);
|
||||
dropAllIndexes(db);
|
||||
dropAllTriggers(db);
|
||||
|
||||
// Recreate the whole database.
|
||||
createDatabase(db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop and rebuild a given view.
|
||||
*/
|
||||
static void rebuildView(final SQLiteDatabase db, final String viewName,
|
||||
final String createViewSql) {
|
||||
dropView(db, viewName, true /* throwOnFailure */);
|
||||
db.execSQL(createViewSql);
|
||||
}
|
||||
|
||||
private static void dropView(final SQLiteDatabase db, final String viewName,
|
||||
final boolean throwOnFailure) {
|
||||
final String dropPrefix = "DROP VIEW IF EXISTS ";
|
||||
try {
|
||||
db.execSQL(dropPrefix + viewName);
|
||||
} catch (final SQLException ex) {
|
||||
if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop view " + viewName + " "
|
||||
+ ex);
|
||||
}
|
||||
|
||||
if (throwOnFailure) {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void rebuildAllViews(final DatabaseWrapper db) {
|
||||
for (final String sql : DatabaseHelper.CREATE_VIEW_SQLS) {
|
||||
db.execSQL(sql);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops all user-defined tables from the given database.
|
||||
*/
|
||||
private static void dropAllTables(final SQLiteDatabase db) {
|
||||
final Cursor tableCursor =
|
||||
db.query(PRIMARY_TABLE, PRIMARY_COLUMNS, "type='table'", null, null, null, null);
|
||||
if (tableCursor != null) {
|
||||
try {
|
||||
final String dropPrefix = "DROP TABLE IF EXISTS ";
|
||||
while (tableCursor.moveToNext()) {
|
||||
final String tableName = tableCursor.getString(0);
|
||||
|
||||
// Skip special tables
|
||||
if (tableName.startsWith("android_") || tableName.startsWith("sqlite_")) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
db.execSQL(dropPrefix + tableName);
|
||||
} catch (final SQLException ex) {
|
||||
if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop table " + tableName + " "
|
||||
+ ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
tableCursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops all user-defined triggers from the given database.
|
||||
*/
|
||||
private static void dropAllTriggers(final SQLiteDatabase db) {
|
||||
final Cursor triggerCursor =
|
||||
db.query(PRIMARY_TABLE, PRIMARY_COLUMNS, "type='trigger'", null, null, null, null);
|
||||
if (triggerCursor != null) {
|
||||
try {
|
||||
final String dropPrefix = "DROP TRIGGER IF EXISTS ";
|
||||
while (triggerCursor.moveToNext()) {
|
||||
final String triggerName = triggerCursor.getString(0);
|
||||
|
||||
// Skip special tables
|
||||
if (triggerName.startsWith("android_") || triggerName.startsWith("sqlite_")) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
db.execSQL(dropPrefix + triggerName);
|
||||
} catch (final SQLException ex) {
|
||||
if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop trigger " + triggerName +
|
||||
" " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
triggerCursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops all user-defined views from the given database.
|
||||
*/
|
||||
public static void dropAllViews(final SQLiteDatabase db) {
|
||||
final Cursor viewCursor =
|
||||
db.query(PRIMARY_TABLE, PRIMARY_COLUMNS, "type='view'", null, null, null, null);
|
||||
if (viewCursor != null) {
|
||||
try {
|
||||
while (viewCursor.moveToNext()) {
|
||||
final String viewName = viewCursor.getString(0);
|
||||
dropView(db, viewName, false /* throwOnFailure */);
|
||||
}
|
||||
} finally {
|
||||
viewCursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops all user-defined views from the given database.
|
||||
*/
|
||||
private static void dropAllIndexes(final SQLiteDatabase db) {
|
||||
final Cursor indexCursor =
|
||||
db.query(PRIMARY_TABLE, PRIMARY_COLUMNS, "type='index'", null, null, null, null);
|
||||
if (indexCursor != null) {
|
||||
try {
|
||||
final String dropPrefix = "DROP INDEX IF EXISTS ";
|
||||
while (indexCursor.moveToNext()) {
|
||||
final String indexName = indexCursor.getString(0);
|
||||
try {
|
||||
db.execSQL(dropPrefix + indexName);
|
||||
} catch (final SQLException ex) {
|
||||
if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop index " + indexName + " "
|
||||
+ ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
indexCursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void createDatabase(final SQLiteDatabase db) {
|
||||
for (final String sql : CREATE_TABLE_SQLS) {
|
||||
db.execSQL(sql);
|
||||
}
|
||||
|
||||
for (final String sql : CREATE_INDEX_SQLS) {
|
||||
db.execSQL(sql);
|
||||
}
|
||||
|
||||
for (final String sql : CREATE_VIEW_SQLS) {
|
||||
db.execSQL(sql);
|
||||
}
|
||||
|
||||
for (final String sql : CREATE_TRIGGER_SQLS) {
|
||||
db.execSQL(sql);
|
||||
}
|
||||
|
||||
// Enable foreign key constraints
|
||||
db.execSQL("PRAGMA foreign_keys=ON;");
|
||||
|
||||
// Add the default self participant. The default self will be assigned a proper slot id
|
||||
// during participant refresh.
|
||||
db.execSQL(getCreateSelfParticipantSql(ParticipantData.DEFAULT_SELF_SUB_ID));
|
||||
|
||||
DataModel.get().onCreateTables(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
createDatabase(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
mUpgradeHelper.doOnUpgrade(db, oldVersion, newVersion);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.sqlite.SQLiteDatabase;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
public class DatabaseUpgradeHelper {
|
||||
private static final String TAG = LogUtil.BUGLE_DATABASE_TAG;
|
||||
|
||||
public void doOnUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
|
||||
Assert.isTrue(newVersion >= oldVersion);
|
||||
if (oldVersion == newVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtil.i(TAG, "Database upgrade started from version " + oldVersion + " to " + newVersion);
|
||||
try {
|
||||
doUpgradeWithExceptions(db, oldVersion, newVersion);
|
||||
LogUtil.i(TAG, "Finished database upgrade");
|
||||
} catch (final Exception ex) {
|
||||
LogUtil.e(TAG, "Failed to perform db upgrade from version " +
|
||||
oldVersion + " to version " + newVersion, ex);
|
||||
DatabaseHelper.rebuildTables(db);
|
||||
}
|
||||
}
|
||||
|
||||
public void doUpgradeWithExceptions(final SQLiteDatabase db, final int oldVersion,
|
||||
final int newVersion) throws Exception {
|
||||
int currentVersion = oldVersion;
|
||||
if (currentVersion < 2) {
|
||||
currentVersion = upgradeToVersion2(db);
|
||||
}
|
||||
// Rebuild all the views
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
DatabaseHelper.dropAllViews(db);
|
||||
DatabaseHelper.rebuildAllViews(new DatabaseWrapper(context, db));
|
||||
// Finally, check if we have arrived at the final version.
|
||||
checkAndUpdateVersionAtReleaseEnd(currentVersion, Integer.MAX_VALUE, newVersion);
|
||||
}
|
||||
|
||||
private int upgradeToVersion2(final SQLiteDatabase db) {
|
||||
db.execSQL("ALTER TABLE " + DatabaseHelper.CONVERSATIONS_TABLE + " ADD COLUMN " +
|
||||
DatabaseHelper.ConversationColumns.IS_ENTERPRISE + " INT DEFAULT(0)");
|
||||
LogUtil.i(TAG, "Ugraded database to version 2");
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks db version correctness at the end of each milestone release. If target database
|
||||
* version lies beyond the version range that the current release may handle, we snap the
|
||||
* current version to the end of the release, so that we may go on to the next release' upgrade
|
||||
* path. Otherwise, if target version is within reach of the current release, but we are not
|
||||
* at the target version, then throw an exception to force a table rebuild.
|
||||
*/
|
||||
private int checkAndUpdateVersionAtReleaseEnd(final int currentVersion,
|
||||
final int maxVersionForRelease, final int targetVersion) throws Exception {
|
||||
if (maxVersionForRelease < targetVersion) {
|
||||
// Target version is beyond the current release. Snap to max version for the
|
||||
// current release so we can go on to the upgrade path for the next release.
|
||||
return maxVersionForRelease;
|
||||
}
|
||||
|
||||
// If we are here, this means the current release' upgrade handler should upgrade to
|
||||
// target version...
|
||||
if (currentVersion != targetVersion) {
|
||||
// No more upgrade handlers. So we can't possibly upgrade to the final version.
|
||||
throw new Exception("Missing upgrade handler from version " +
|
||||
currentVersion + " to version " + targetVersion);
|
||||
}
|
||||
// Upgrade succeeded.
|
||||
return targetVersion;
|
||||
}
|
||||
|
||||
public void onDowngrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
|
||||
DatabaseHelper.rebuildTables(db);
|
||||
LogUtil.e(TAG, "Database downgrade requested for version " +
|
||||
oldVersion + " version " + newVersion + ", forcing db rebuild!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
* 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.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteFullException;
|
||||
import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.UiUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Stack;
|
||||
|
||||
public class DatabaseWrapper {
|
||||
private static final String TAG = LogUtil.BUGLE_DATABASE_TAG;
|
||||
|
||||
private final SQLiteDatabase mDatabase;
|
||||
private final Context mContext;
|
||||
private final boolean mLog;
|
||||
private static final int sTimingThreshold = 50; // in milliseconds
|
||||
|
||||
public static final int INDEX_INSERT_MESSAGE_PART = 0;
|
||||
public static final int INDEX_INSERT_MESSAGE = 1;
|
||||
public static final int INDEX_QUERY_CONVERSATIONS_LATEST_MESSAGE = 2;
|
||||
public static final int INDEX_QUERY_MESSAGES_LATEST_MESSAGE = 3;
|
||||
|
||||
private final SparseArray<SQLiteStatement> mCompiledStatements;
|
||||
|
||||
static class TransactionData {
|
||||
long time;
|
||||
boolean transactionSuccessful;
|
||||
}
|
||||
|
||||
// track transaction on a per thread basis
|
||||
private static final ThreadLocal<Stack<TransactionData>> sTransactionDepth =
|
||||
ThreadLocal.withInitial(Stack::new);
|
||||
|
||||
private static final String[] sFormatStrings = new String[] {
|
||||
"took %d ms to %s",
|
||||
" took %d ms to %s",
|
||||
" took %d ms to %s",
|
||||
};
|
||||
|
||||
DatabaseWrapper(final Context context, final SQLiteDatabase db) {
|
||||
mLog = LogUtil.isLoggable(LogUtil.BUGLE_DATABASE_PERF_TAG, LogUtil.VERBOSE);
|
||||
mDatabase = db;
|
||||
mContext = context;
|
||||
mCompiledStatements = new SparseArray<>();
|
||||
}
|
||||
|
||||
public SQLiteStatement getStatementInTransaction(final int index, final String statement) {
|
||||
// Use transaction to serialize access to statements
|
||||
Assert.isTrue(mDatabase.inTransaction());
|
||||
SQLiteStatement compiled = mCompiledStatements.get(index);
|
||||
if (compiled == null) {
|
||||
compiled = mDatabase.compileStatement(statement);
|
||||
Assert.isTrue(compiled.toString().contains(statement.trim()));
|
||||
mCompiledStatements.put(index, compiled);
|
||||
}
|
||||
return compiled;
|
||||
}
|
||||
|
||||
private static void printTiming(final long t1, final String msg) {
|
||||
final int transactionDepth = sTransactionDepth.get().size();
|
||||
final long t2 = System.currentTimeMillis();
|
||||
final long delta = t2 - t1;
|
||||
if (delta > sTimingThreshold) {
|
||||
LogUtil.v(LogUtil.BUGLE_DATABASE_PERF_TAG, String.format(Locale.US,
|
||||
sFormatStrings[Math.min(sFormatStrings.length - 1, transactionDepth)],
|
||||
delta,
|
||||
msg));
|
||||
}
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
public void beginTransaction() {
|
||||
final long t1 = System.currentTimeMillis();
|
||||
|
||||
// push the current time onto the transaction stack
|
||||
final TransactionData f = new TransactionData();
|
||||
f.time = t1;
|
||||
sTransactionDepth.get().push(f);
|
||||
|
||||
mDatabase.beginTransaction();
|
||||
}
|
||||
|
||||
public void setTransactionSuccessful() {
|
||||
final TransactionData f = sTransactionDepth.get().peek();
|
||||
f.transactionSuccessful = true;
|
||||
mDatabase.setTransactionSuccessful();
|
||||
}
|
||||
|
||||
public void endTransaction() {
|
||||
long t1 = 0;
|
||||
long transactionStartTime = 0;
|
||||
final TransactionData f = sTransactionDepth.get().pop();
|
||||
if (f.transactionSuccessful == false) {
|
||||
LogUtil.w(TAG, "endTransaction without setting successful");
|
||||
for (final StackTraceElement st : (new Exception()).getStackTrace()) {
|
||||
LogUtil.w(TAG, " " + st.toString());
|
||||
}
|
||||
}
|
||||
if (mLog) {
|
||||
transactionStartTime = f.time;
|
||||
t1 = System.currentTimeMillis();
|
||||
}
|
||||
try {
|
||||
mDatabase.endTransaction();
|
||||
} catch (SQLiteFullException ex) {
|
||||
LogUtil.e(TAG, "Database full, unable to endTransaction", ex);
|
||||
UiUtils.showToastAtBottom(R.string.db_full);
|
||||
}
|
||||
if (mLog) {
|
||||
printTiming(t1, String.format(Locale.US,
|
||||
">>> endTransaction (total for this transaction: %d)",
|
||||
(System.currentTimeMillis() - transactionStartTime)));
|
||||
}
|
||||
}
|
||||
|
||||
public void yieldTransaction() {
|
||||
long yieldStartTime = 0;
|
||||
if (mLog) {
|
||||
yieldStartTime = System.currentTimeMillis();
|
||||
}
|
||||
final boolean wasYielded = mDatabase.yieldIfContendedSafely();
|
||||
if (wasYielded && mLog) {
|
||||
printTiming(yieldStartTime, "yieldTransaction");
|
||||
}
|
||||
}
|
||||
|
||||
public void insertWithOnConflict(final String searchTable, final String nullColumnHack,
|
||||
final ContentValues initialValues, final int conflictAlgorithm) {
|
||||
long t1 = 0;
|
||||
if (mLog) {
|
||||
t1 = System.currentTimeMillis();
|
||||
}
|
||||
try {
|
||||
mDatabase.insertWithOnConflict(searchTable, nullColumnHack, initialValues,
|
||||
conflictAlgorithm);
|
||||
} catch (SQLiteFullException ex) {
|
||||
LogUtil.e(TAG, "Database full, unable to insertWithOnConflict", ex);
|
||||
UiUtils.showToastAtBottom(R.string.db_full);
|
||||
}
|
||||
if (mLog) {
|
||||
printTiming(t1, String.format(Locale.US,
|
||||
"insertWithOnConflict with %s", searchTable));
|
||||
}
|
||||
}
|
||||
|
||||
public Cursor query(final String searchTable, final String[] projection,
|
||||
final String selection, final String[] selectionArgs, final String groupBy,
|
||||
final String having, final String orderBy, final String limit) {
|
||||
|
||||
long t1 = 0;
|
||||
if (mLog) {
|
||||
t1 = System.currentTimeMillis();
|
||||
}
|
||||
final Cursor cursor = mDatabase.query(searchTable, projection, selection, selectionArgs,
|
||||
groupBy, having, orderBy, limit);
|
||||
if (mLog) {
|
||||
printTiming(
|
||||
t1,
|
||||
String.format(Locale.US, "query %s with %s ==> %d",
|
||||
searchTable, selection, cursor.getCount()));
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public Cursor query(final String searchTable, final String[] columns,
|
||||
final String selection, final String[] selectionArgs, final String groupBy,
|
||||
final String having, final String orderBy) {
|
||||
return query(
|
||||
searchTable, columns, selection, selectionArgs,
|
||||
groupBy, having, orderBy, null);
|
||||
}
|
||||
|
||||
public Cursor query(final SQLiteQueryBuilder qb,
|
||||
final String[] projection, final String selection, final String[] queryArgs,
|
||||
final String groupBy, final String having, final String sortOrder, final String limit) {
|
||||
long t1 = 0;
|
||||
if (mLog) {
|
||||
t1 = System.currentTimeMillis();
|
||||
}
|
||||
final Cursor cursor = qb.query(mDatabase, projection, selection, queryArgs, groupBy,
|
||||
having, sortOrder, limit);
|
||||
if (mLog) {
|
||||
printTiming(
|
||||
t1,
|
||||
String.format(Locale.US, "query %s with %s ==> %d",
|
||||
qb.getTables(), selection, cursor.getCount()));
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public long queryNumEntries(final String table, final String selection,
|
||||
final String[] selectionArgs) {
|
||||
long t1 = 0;
|
||||
if (mLog) {
|
||||
t1 = System.currentTimeMillis();
|
||||
}
|
||||
final long retval =
|
||||
DatabaseUtils.queryNumEntries(mDatabase, table, selection, selectionArgs);
|
||||
if (mLog){
|
||||
printTiming(
|
||||
t1,
|
||||
String.format(Locale.US, "queryNumEntries %s with %s ==> %d", table,
|
||||
selection, retval));
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
public Cursor rawQuery(final String sql, final String[] args) {
|
||||
long t1 = 0;
|
||||
if (mLog) {
|
||||
t1 = System.currentTimeMillis();
|
||||
}
|
||||
final Cursor cursor = mDatabase.rawQuery(sql, args);
|
||||
if (mLog) {
|
||||
printTiming(
|
||||
t1,
|
||||
String.format(Locale.US, "rawQuery %s ==> %d", sql, cursor.getCount()));
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public int update(final String table, final ContentValues values,
|
||||
final String selection, final String[] selectionArgs) {
|
||||
long t1 = 0;
|
||||
if (mLog) {
|
||||
t1 = System.currentTimeMillis();
|
||||
}
|
||||
int count = 0;
|
||||
try {
|
||||
count = mDatabase.update(table, values, selection, selectionArgs);
|
||||
} catch (SQLiteFullException ex) {
|
||||
LogUtil.e(TAG, "Database full, unable to update", ex);
|
||||
UiUtils.showToastAtBottom(R.string.db_full);
|
||||
}
|
||||
if (mLog) {
|
||||
printTiming(t1, String.format(Locale.US, "update %s with %s ==> %d",
|
||||
table, selection, count));
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public int delete(final String table, final String whereClause, final String[] whereArgs) {
|
||||
long t1 = 0;
|
||||
if (mLog) {
|
||||
t1 = System.currentTimeMillis();
|
||||
}
|
||||
int count = 0;
|
||||
try {
|
||||
count = mDatabase.delete(table, whereClause, whereArgs);
|
||||
} catch (SQLiteFullException ex) {
|
||||
LogUtil.e(TAG, "Database full, unable to delete", ex);
|
||||
UiUtils.showToastAtBottom(R.string.db_full);
|
||||
}
|
||||
if (mLog) {
|
||||
printTiming(t1,
|
||||
String.format(Locale.US, "delete from %s with %s ==> %d", table,
|
||||
whereClause, count));
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public long insert(final String table, final String nullColumnHack,
|
||||
final ContentValues values) {
|
||||
long t1 = 0;
|
||||
if (mLog) {
|
||||
t1 = System.currentTimeMillis();
|
||||
}
|
||||
long rowId = -1;
|
||||
try {
|
||||
rowId = mDatabase.insert(table, nullColumnHack, values);
|
||||
} catch (SQLiteFullException ex) {
|
||||
LogUtil.e(TAG, "Database full, unable to insert", ex);
|
||||
UiUtils.showToastAtBottom(R.string.db_full);
|
||||
}
|
||||
if (mLog) {
|
||||
printTiming(t1, String.format(Locale.US, "insert to %s", table));
|
||||
}
|
||||
return rowId;
|
||||
}
|
||||
|
||||
public long replace(final String table, final String nullColumnHack,
|
||||
final ContentValues values) {
|
||||
long t1 = 0;
|
||||
if (mLog) {
|
||||
t1 = System.currentTimeMillis();
|
||||
}
|
||||
long rowId = -1;
|
||||
try {
|
||||
rowId = mDatabase.replace(table, nullColumnHack, values);
|
||||
} catch (SQLiteFullException ex) {
|
||||
LogUtil.e(TAG, "Database full, unable to replace", ex);
|
||||
UiUtils.showToastAtBottom(R.string.db_full);
|
||||
}
|
||||
if (mLog) {
|
||||
printTiming(t1, String.format(Locale.US, "replace to %s", table));
|
||||
}
|
||||
return rowId;
|
||||
}
|
||||
|
||||
public void setLocale(final Locale locale) {
|
||||
mDatabase.setLocale(locale);
|
||||
}
|
||||
|
||||
public void execSQL(final String sql, final String[] bindArgs) {
|
||||
long t1 = 0;
|
||||
if (mLog) {
|
||||
t1 = System.currentTimeMillis();
|
||||
}
|
||||
try {
|
||||
mDatabase.execSQL(sql, bindArgs);
|
||||
} catch (SQLiteFullException ex) {
|
||||
LogUtil.e(TAG, "Database full, unable to execSQL", ex);
|
||||
UiUtils.showToastAtBottom(R.string.db_full);
|
||||
}
|
||||
|
||||
if (mLog) {
|
||||
printTiming(t1, String.format(Locale.US, "execSQL %s", sql));
|
||||
}
|
||||
}
|
||||
|
||||
public void execSQL(final String sql) {
|
||||
long t1 = 0;
|
||||
if (mLog) {
|
||||
t1 = System.currentTimeMillis();
|
||||
}
|
||||
try {
|
||||
mDatabase.execSQL(sql);
|
||||
} catch (SQLiteFullException ex) {
|
||||
LogUtil.e(TAG, "Database full, unable to execSQL", ex);
|
||||
UiUtils.showToastAtBottom(R.string.db_full);
|
||||
}
|
||||
|
||||
if (mLog) {
|
||||
printTiming(t1, String.format(Locale.US, "execSQL %s", sql));
|
||||
}
|
||||
}
|
||||
|
||||
public int execSQLUpdateDelete(final String sql) {
|
||||
long t1 = 0;
|
||||
if (mLog) {
|
||||
t1 = System.currentTimeMillis();
|
||||
}
|
||||
final SQLiteStatement statement = mDatabase.compileStatement(sql);
|
||||
int rowsUpdated = 0;
|
||||
try {
|
||||
rowsUpdated = statement.executeUpdateDelete();
|
||||
} catch (SQLiteFullException ex) {
|
||||
LogUtil.e(TAG, "Database full, unable to execSQLUpdateDelete", ex);
|
||||
UiUtils.showToastAtBottom(R.string.db_full);
|
||||
}
|
||||
if (mLog) {
|
||||
printTiming(t1, String.format(Locale.US, "execSQLUpdateDelete %s", sql));
|
||||
}
|
||||
return rowsUpdated;
|
||||
}
|
||||
|
||||
public SQLiteDatabase getDatabase() {
|
||||
return mDatabase;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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.ContentProvider;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* A very simple content provider that can serve files.
|
||||
*/
|
||||
public abstract class FileProvider extends ContentProvider {
|
||||
// Object to generate random id for temp images.
|
||||
private static final Random RANDOM_ID = new Random();
|
||||
|
||||
abstract File getFile(final String path, final String extension);
|
||||
|
||||
private static final String FILE_EXTENSION_PARAM_KEY = "ext";
|
||||
|
||||
/**
|
||||
* Check if filename conforms to requirement for our provider
|
||||
* @param fileId filename (optionally starting with path character
|
||||
* @return true if filename consists only of digits
|
||||
*/
|
||||
protected static boolean isValidFileId(final String fileId) {
|
||||
// Ignore initial "/"
|
||||
for (int index = (fileId.startsWith("/") ? 1 : 0); index < fileId.length(); index++) {
|
||||
final Character c = fileId.charAt(index);
|
||||
if (!Character.isDigit(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a temp file (to allow writing to that one particular file)
|
||||
* @param file the file to create
|
||||
* @return true if file successfully created
|
||||
*/
|
||||
protected static boolean ensureFileExists(final File file) {
|
||||
try {
|
||||
final File parentDir = file.getParentFile();
|
||||
if (parentDir.exists() || parentDir.mkdirs()) {
|
||||
return file.createNewFile();
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
// fail on exceptions creating the file
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build uri for a new temporary file (creating file)
|
||||
* @param authority authority with which to populate uri
|
||||
* @param extension optional file extension
|
||||
* @return unique uri that can be used to write temporary files
|
||||
*/
|
||||
protected static Uri buildFileUri(final String authority, final String extension) {
|
||||
final long fileId = Math.abs(RANDOM_ID.nextLong());
|
||||
final Uri.Builder builder = (new Uri.Builder()).authority(authority).scheme(
|
||||
ContentResolver.SCHEME_CONTENT);
|
||||
builder.appendPath(String.valueOf(fileId));
|
||||
if (!TextUtils.isEmpty(extension)) {
|
||||
builder.appendQueryParameter(FILE_EXTENSION_PARAM_KEY, extension);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(final Uri uri, final String selection, final String[] selectionArgs) {
|
||||
final String fileId = uri.getPath();
|
||||
if (isValidFileId(fileId)) {
|
||||
final File file = getFile(fileId, getExtensionFromUri(uri));
|
||||
return file.delete() ? 1 : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(final Uri uri, @NonNull final String fileMode)
|
||||
throws FileNotFoundException {
|
||||
final String fileId = uri.getPath();
|
||||
if (isValidFileId(fileId)) {
|
||||
final File file = getFile(fileId, getExtensionFromUri(uri));
|
||||
final int mode =
|
||||
(TextUtils.equals(fileMode, "r") ? ParcelFileDescriptor.MODE_READ_ONLY :
|
||||
ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_TRUNCATE);
|
||||
return ParcelFileDescriptor.open(file, mode);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static String getExtensionFromUri(final Uri uri) {
|
||||
return uri.getQueryParameter(FILE_EXTENSION_PARAM_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull final Uri uri, final String[] projection, final String selection,
|
||||
final String[] selectionArgs, final String sortOrder) {
|
||||
// Don't support queries.
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(@NonNull final Uri uri, final ContentValues values) {
|
||||
// Don't support inserts.
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull final Uri uri, final ContentValues values, final String selection,
|
||||
final String[] selectionArgs) {
|
||||
// Don't support updates.
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull final Uri uri) {
|
||||
// No need for mime types.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* 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.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Phone;
|
||||
import androidx.collection.SimpleArrayMap;
|
||||
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.ContactUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* A cursor builder that takes the frequent contacts cursor and aggregate it with the all contacts
|
||||
* cursor to fill in contact details such as phone numbers and strip away invalid contacts.
|
||||
*
|
||||
* Because the frequent contact list depends on the loading of two cursors, it needs to temporarily
|
||||
* store the cursor that it receives with setFrequents() and setAllContacts() calls. Because it
|
||||
* doesn't know which one will be finished first, it always checks whether both cursors are ready
|
||||
* to pull data from and construct the aggregate cursor when it's ready to do so. Note that
|
||||
* this cursor builder doesn't assume ownership of the cursors passed in - it merely references
|
||||
* them and always does a isClosed() check before consuming them. The ownership still belongs to
|
||||
* the loader framework and the cursor may be closed when the UI is torn down.
|
||||
*/
|
||||
public class FrequentContactsCursorBuilder {
|
||||
private Cursor mAllContactsCursor;
|
||||
private Cursor mFrequentContactsCursor;
|
||||
|
||||
/**
|
||||
* Sets the frequent contacts cursor as soon as it is loaded, or null if it's reset.
|
||||
*/
|
||||
public void setFrequents(final Cursor frequentContactsCursor) {
|
||||
mFrequentContactsCursor = frequentContactsCursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the all contacts cursor as soon as it is loaded, or null if it's reset.
|
||||
*/
|
||||
public void setAllContacts(final Cursor allContactsCursor) {
|
||||
mAllContactsCursor = allContactsCursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this builder. Must be called when the consumer resets its data.
|
||||
*/
|
||||
public void resetBuilder() {
|
||||
mAllContactsCursor = null;
|
||||
mFrequentContactsCursor = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to build the cursor records from the frequent and all contacts cursor if they
|
||||
* are both ready to be consumed.
|
||||
* @return the frequent contact cursor if built successfully, or null if it can't be built yet.
|
||||
*/
|
||||
public Cursor build(boolean getAllContacts) {
|
||||
if (mFrequentContactsCursor != null && mAllContactsCursor != null) {
|
||||
Assert.isTrue(!mFrequentContactsCursor.isClosed());
|
||||
Assert.isTrue(!mAllContactsCursor.isClosed());
|
||||
|
||||
// Frequent contacts cursor has one record per contact, plus it doesn't contain info
|
||||
// such as phone number and type. In order for the records to be usable by Bugle, we
|
||||
// would like to populate it with information from the all contacts cursor.
|
||||
final MatrixCursor retCursor = new MatrixCursor(ContactUtil.PhoneQuery.PROJECTION);
|
||||
|
||||
// First, go through the frequents cursor and take note of all lookup keys and their
|
||||
// corresponding rank in the frequents list.
|
||||
final SimpleArrayMap<String, Integer> lookupKeyToRankMap = new SimpleArrayMap<>();
|
||||
int oldPosition = mFrequentContactsCursor.getPosition();
|
||||
int rank = 0;
|
||||
mFrequentContactsCursor.moveToPosition(-1);
|
||||
while (mFrequentContactsCursor.moveToNext()) {
|
||||
final String lookupKey = mFrequentContactsCursor.getString(
|
||||
ContactUtil.INDEX_LOOKUP_KEY_FREQUENT);
|
||||
lookupKeyToRankMap.put(lookupKey, rank++);
|
||||
}
|
||||
mFrequentContactsCursor.moveToPosition(oldPosition);
|
||||
|
||||
// Second, go through the all contacts cursor once and retrieve all information
|
||||
// (multiple phone numbers etc.) and store that in an array list. Since the all
|
||||
// contacts list only contains phone contacts, this step will ensure that we filter
|
||||
// out any invalid/email contacts in the frequents list.
|
||||
final ArrayList<Object[]> rows = new ArrayList<>(mFrequentContactsCursor.getCount());
|
||||
oldPosition = mAllContactsCursor.getPosition();
|
||||
mAllContactsCursor.moveToPosition(-1);
|
||||
while (mAllContactsCursor.moveToNext()) {
|
||||
final String lookupKey = mAllContactsCursor.getString(ContactUtil.INDEX_LOOKUP_KEY);
|
||||
if (lookupKeyToRankMap.containsKey(lookupKey) || getAllContacts) {
|
||||
final Object[] row = new Object[ContactUtil.PhoneQuery.PROJECTION.length];
|
||||
row[ContactUtil.INDEX_DATA_ID] =
|
||||
mAllContactsCursor.getLong(ContactUtil.INDEX_DATA_ID);
|
||||
row[ContactUtil.INDEX_CONTACT_ID] =
|
||||
mAllContactsCursor.getLong(ContactUtil.INDEX_CONTACT_ID);
|
||||
row[ContactUtil.INDEX_LOOKUP_KEY] =
|
||||
mAllContactsCursor.getString(ContactUtil.INDEX_LOOKUP_KEY);
|
||||
row[ContactUtil.INDEX_DISPLAY_NAME] =
|
||||
mAllContactsCursor.getString(ContactUtil.INDEX_DISPLAY_NAME);
|
||||
row[ContactUtil.INDEX_PHOTO_URI] =
|
||||
mAllContactsCursor.getString(ContactUtil.INDEX_PHOTO_URI);
|
||||
row[ContactUtil.INDEX_PHONE_EMAIL] =
|
||||
mAllContactsCursor.getString(ContactUtil.INDEX_PHONE_EMAIL)
|
||||
.replaceAll("[^\\d+]", "");
|
||||
row[ContactUtil.INDEX_PHONE_EMAIL_TYPE] =
|
||||
mAllContactsCursor.getInt(ContactUtil.INDEX_PHONE_EMAIL_TYPE);
|
||||
row[ContactUtil.INDEX_PHONE_EMAIL_LABEL] =
|
||||
mAllContactsCursor.getString(ContactUtil.INDEX_PHONE_EMAIL_LABEL);
|
||||
|
||||
boolean numberAlreadyAdded = false;
|
||||
for (Object[] oldRow : rows) {
|
||||
final int idxType = ContactUtil.INDEX_PHONE_EMAIL_TYPE;
|
||||
final int idxPhone = ContactUtil.INDEX_PHONE_EMAIL;
|
||||
if (oldRow[idxType] == row[idxType] &&
|
||||
oldRow[idxPhone].toString().equals(row[idxPhone].toString())) {
|
||||
numberAlreadyAdded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!numberAlreadyAdded) {
|
||||
rows.add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
mAllContactsCursor.moveToPosition(oldPosition);
|
||||
|
||||
// We can return all rows at this point, no sorting or further filtering needed
|
||||
if (getAllContacts) {
|
||||
for (final Object[] row : rows) {
|
||||
retCursor.addRow(row);
|
||||
}
|
||||
return retCursor;
|
||||
}
|
||||
|
||||
// Now we have a list of rows containing frequent contacts in alphabetical order.
|
||||
// Therefore, sort all the rows according to their actual ranks in the frequents list.
|
||||
rows.sort((lhs, rhs) -> {
|
||||
final String lookupKeyLhs = (String) lhs[ContactUtil.INDEX_LOOKUP_KEY];
|
||||
final String lookupKeyRhs = (String) rhs[ContactUtil.INDEX_LOOKUP_KEY];
|
||||
Assert.isTrue(lookupKeyToRankMap.containsKey(lookupKeyLhs) &&
|
||||
lookupKeyToRankMap.containsKey(lookupKeyRhs));
|
||||
final int rankLhs = lookupKeyToRankMap.get(lookupKeyLhs);
|
||||
final int rankRhs = lookupKeyToRankMap.get(lookupKeyRhs);
|
||||
if (rankLhs < rankRhs) {
|
||||
return -1;
|
||||
} else if (rankLhs > rankRhs) {
|
||||
return 1;
|
||||
} else {
|
||||
// Same rank, so it's two contact records for the same contact.
|
||||
// Perform secondary sorting on the phone type. Always place
|
||||
// mobile before everything else.
|
||||
final int phoneTypeLhs = (int) lhs[ContactUtil.INDEX_PHONE_EMAIL_TYPE];
|
||||
final int phoneTypeRhs = (int) rhs[ContactUtil.INDEX_PHONE_EMAIL_TYPE];
|
||||
if (phoneTypeLhs == Phone.TYPE_MOBILE &&
|
||||
phoneTypeRhs == Phone.TYPE_MOBILE) {
|
||||
return 0;
|
||||
} else if (phoneTypeLhs == Phone.TYPE_MOBILE) {
|
||||
return -1;
|
||||
} else if (phoneTypeRhs == Phone.TYPE_MOBILE) {
|
||||
return 1;
|
||||
} else {
|
||||
// Use the default sort order, i.e. sort by phoneType value.
|
||||
return Integer.compare(phoneTypeLhs, phoneTypeRhs);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Finally, add all the rows to this cursor.
|
||||
for (final Object[] row : rows) {
|
||||
retCursor.addRow(row);
|
||||
}
|
||||
return retCursor;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.ContactsContract.Contacts;
|
||||
|
||||
import com.android.messaging.util.FallbackStrategies;
|
||||
import com.android.messaging.util.FallbackStrategies.Strategy;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Helper for querying frequent (and/or starred) contacts.
|
||||
*/
|
||||
public class FrequentContactsCursorQueryData extends CursorQueryData {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
private static class FrequentContactsCursorLoader extends BoundCursorLoader {
|
||||
private final Uri mOriginalUri;
|
||||
|
||||
FrequentContactsCursorLoader(String bindingId, Context context, Uri uri,
|
||||
String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
super(bindingId, context, uri, projection, selection, selectionArgs, sortOrder);
|
||||
mOriginalUri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor loadInBackground() {
|
||||
return FallbackStrategies
|
||||
.startWith(new PrimaryStrequentContactsQueryStrategy())
|
||||
.thenTry(new FrequentOnlyContactsQueryStrategy())
|
||||
.thenTry(new PhoneOnlyStrequentContactsQueryStrategy())
|
||||
.execute(null);
|
||||
}
|
||||
|
||||
private abstract class StrequentContactsQueryStrategy implements Strategy<Void, Cursor> {
|
||||
@Override
|
||||
public Cursor execute(Void params) {
|
||||
final Uri uri = getUri();
|
||||
if (uri != null) {
|
||||
setUri(uri);
|
||||
}
|
||||
return FrequentContactsCursorLoader.super.loadInBackground();
|
||||
}
|
||||
protected abstract Uri getUri();
|
||||
}
|
||||
|
||||
private class PrimaryStrequentContactsQueryStrategy extends StrequentContactsQueryStrategy {
|
||||
@Override
|
||||
protected Uri getUri() {
|
||||
// Use the original URI requested.
|
||||
return mOriginalUri;
|
||||
}
|
||||
}
|
||||
|
||||
private class FrequentOnlyContactsQueryStrategy extends StrequentContactsQueryStrategy {
|
||||
@Override
|
||||
protected Uri getUri() {
|
||||
// Some phones have a buggy implementation of the Contacts provider which crashes
|
||||
// when we query for strequent (starred+frequent) contacts (b/17991485).
|
||||
// If this happens, switch to just querying for frequent contacts.
|
||||
return Contacts.CONTENT_FREQUENT_URI;
|
||||
}
|
||||
}
|
||||
|
||||
private class PhoneOnlyStrequentContactsQueryStrategy extends
|
||||
StrequentContactsQueryStrategy {
|
||||
@Override
|
||||
protected Uri getUri() {
|
||||
// Some 3rd party ROMs have content provider
|
||||
// implementation where invalid SQL queries are returned for regular strequent
|
||||
// queries. Using strequent_phone_only query as a fallback to display only phone
|
||||
// contacts. This is the last-ditch effort; if this fails, we will display an
|
||||
// empty frequent list (b/18354836).
|
||||
final String strequentQueryParam = ContactsContract.STREQUENT_PHONE_ONLY;
|
||||
return Contacts.CONTENT_STREQUENT_URI.buildUpon()
|
||||
.appendQueryParameter(strequentQueryParam, "true").build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FrequentContactsCursorQueryData(Context context, String[] projection,
|
||||
String selection, String[] selectionArgs, String sortOrder) {
|
||||
// TODO: Handle enterprise contacts post M once contacts provider supports it
|
||||
super(context, Contacts.CONTENT_STREQUENT_URI, projection, selection, selectionArgs,
|
||||
sortOrder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BoundCursorLoader createBoundCursorLoader(String bindingId) {
|
||||
return new FrequentContactsCursorLoader(bindingId, mContext, mUri, mProjection, mSelection,
|
||||
mSelectionArgs, mSortOrder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.database.MatrixCursor.RowBuilder;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.collection.SimpleArrayMap;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A very simple content provider that can serve media files from our cache directory.
|
||||
*/
|
||||
public class MediaScratchFileProvider extends FileProvider {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
private static final SimpleArrayMap<Uri, String> sUriToDisplayNameMap = new SimpleArrayMap<>();
|
||||
|
||||
public static final String AUTHORITY =
|
||||
"com.android.messaging.datamodel.MediaScratchFileProvider";
|
||||
private static final String MEDIA_SCRATCH_SPACE_DIR = "mediascratchspace";
|
||||
|
||||
public static boolean isMediaScratchSpaceUri(final Uri uri) {
|
||||
if (uri == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final List<String> segments = uri.getPathSegments();
|
||||
return (TextUtils.equals(uri.getScheme(), ContentResolver.SCHEME_CONTENT) &&
|
||||
TextUtils.equals(uri.getAuthority(), AUTHORITY) &&
|
||||
segments.size() == 1 && FileProvider.isValidFileId(segments.get(0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a uri that can be used to access a raw mms file.
|
||||
*
|
||||
* @return the URI for an raw mms file
|
||||
*/
|
||||
public static Uri buildMediaScratchSpaceUri(final String extension) {
|
||||
final Uri uri = FileProvider.buildFileUri(AUTHORITY, extension);
|
||||
final File file = getFileWithExtension(uri.getPath(), extension);
|
||||
if (!ensureFileExists(file)) {
|
||||
LogUtil.e(TAG, "Failed to create temp file " + file.getAbsolutePath());
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
public static File getFileFromUri(final Uri uri) {
|
||||
Assert.equals(AUTHORITY, uri.getAuthority());
|
||||
return getFileWithExtension(uri.getPath(), getExtensionFromUri(uri));
|
||||
}
|
||||
|
||||
public static Uri.Builder getUriBuilder() {
|
||||
return (new Uri.Builder()).authority(AUTHORITY).scheme(ContentResolver.SCHEME_CONTENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
File getFile(final String path, final String extension) {
|
||||
return getFileWithExtension(path, extension);
|
||||
}
|
||||
|
||||
private static File getFileWithExtension(final String path, final String extension) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final File filePath = new File(getDirectory(context),
|
||||
TextUtils.isEmpty(extension) ? path : path + "." + extension);
|
||||
|
||||
try {
|
||||
if (!filePath.getCanonicalPath()
|
||||
.startsWith(getDirectory(context).getCanonicalPath())) {
|
||||
LogUtil.e(TAG, "getFileWithExtension: path "
|
||||
+ filePath.getCanonicalPath()
|
||||
+ " does not start with "
|
||||
+ getDirectory(context).getCanonicalPath());
|
||||
return null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtil.e(TAG, "getFileWithExtension: getCanonicalPath failed ", e);
|
||||
return null;
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private static File getDirectory(final Context context) {
|
||||
return new File(context.getCacheDir(), MEDIA_SCRATCH_SPACE_DIR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull final Uri uri, final String[] projection, final String selection,
|
||||
final String[] selectionArgs, final String sortOrder) {
|
||||
if (projection != null && projection.length > 0 &&
|
||||
TextUtils.equals(projection[0], OpenableColumns.DISPLAY_NAME) &&
|
||||
isMediaScratchSpaceUri(uri)) {
|
||||
// Retrieve the display name associated with a temp file. This is used by the Contacts
|
||||
// ImportVCardActivity to retrieve the name of the contact(s) being imported.
|
||||
String displayName;
|
||||
synchronized (sUriToDisplayNameMap) {
|
||||
displayName = sUriToDisplayNameMap.get(uri);
|
||||
}
|
||||
if (!TextUtils.isEmpty(displayName)) {
|
||||
MatrixCursor cursor =
|
||||
new MatrixCursor(new String[] { OpenableColumns.DISPLAY_NAME });
|
||||
RowBuilder row = cursor.newRow();
|
||||
row.add(displayName);
|
||||
return cursor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void addUriToDisplayNameEntry(final Uri scratchFileUri,
|
||||
final String displayName) {
|
||||
if (TextUtils.isEmpty(displayName)) {
|
||||
return;
|
||||
}
|
||||
synchronized (sUriToDisplayNameMap) {
|
||||
sUriToDisplayNameMap.put(scratchFileUri, displayName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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 com.android.messaging.Factory;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* Utility abstraction which allows MemoryCaches in an application to register and then when there
|
||||
* is memory pressure provide a callback to reclaim the memory in the caches.
|
||||
*/
|
||||
public class MemoryCacheManager {
|
||||
private final HashSet<MemoryCache> mMemoryCaches = new HashSet<>();
|
||||
private final Object mMemoryCacheLock = new Object();
|
||||
|
||||
public static MemoryCacheManager get() {
|
||||
return Factory.get().getMemoryCacheManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend this interface to provide a reclaim method on a memory cache.
|
||||
*/
|
||||
public interface MemoryCache {
|
||||
void reclaim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the memory cache with the application.
|
||||
*/
|
||||
public void registerMemoryCache(final MemoryCache cache) {
|
||||
synchronized (mMemoryCacheLock) {
|
||||
mMemoryCaches.add(cache);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the memory cache with the application.
|
||||
*/
|
||||
public void unregisterMemoryCache(final MemoryCache cache) {
|
||||
synchronized (mMemoryCacheLock) {
|
||||
mMemoryCaches.remove(cache);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reclaim memory in all the memory caches in the application.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void reclaimMemory() {
|
||||
// We're creating a cache copy in the lock to ensure we're not working on a concurrently
|
||||
// modified set, then reclaim outside of the lock to minimize the time within the lock.
|
||||
final HashSet<MemoryCache> shallowCopy;
|
||||
synchronized (mMemoryCacheLock) {
|
||||
shallowCopy = (HashSet<MemoryCache>) mMemoryCaches.clone();
|
||||
}
|
||||
for (final MemoryCache cache : shallowCopy) {
|
||||
cache.reclaim();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.telephony.SmsMessage;
|
||||
|
||||
import com.android.messaging.sms.MmsConfig;
|
||||
|
||||
public class MessageTextStats {
|
||||
private boolean mMessageLengthRequiresMms;
|
||||
private int mMessageCount;
|
||||
private int mCodePointsRemainingInCurrentMessage;
|
||||
|
||||
public MessageTextStats() {
|
||||
mCodePointsRemainingInCurrentMessage = Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
public int getNumMessagesToBeSent() {
|
||||
return mMessageCount;
|
||||
}
|
||||
|
||||
public int getCodePointsRemainingInCurrentMessage() {
|
||||
return mCodePointsRemainingInCurrentMessage;
|
||||
}
|
||||
|
||||
public boolean getMessageLengthRequiresMms() {
|
||||
return mMessageLengthRequiresMms;
|
||||
}
|
||||
|
||||
public void updateMessageTextStats(final int selfSubId, final String messageText) {
|
||||
final int[] params = SmsMessage.calculateLength(messageText, false);
|
||||
/* SmsMessage.calculateLength returns an int[4] with:
|
||||
* int[0] being the number of SMS's required,
|
||||
* int[1] the number of code points used,
|
||||
* int[2] is the number of code points remaining until the next message.
|
||||
* int[3] is the encoding type that should be used for the message.
|
||||
*/
|
||||
mMessageCount = params[0];
|
||||
mCodePointsRemainingInCurrentMessage = params[2];
|
||||
|
||||
final MmsConfig mmsConfig = MmsConfig.get(selfSubId);
|
||||
if (!mmsConfig.getMultipartSmsEnabled() &&
|
||||
!mmsConfig.getSendMultipartSmsAsSeparateMessages()) {
|
||||
// The provider doesn't support multi-part sms's and we should use MMS to
|
||||
// send multi-part sms, so as soon as the user types
|
||||
// an sms longer than one segment, we have to turn the message into an mms.
|
||||
mMessageLengthRequiresMms = mMessageCount > 1;
|
||||
} else {
|
||||
final int threshold = mmsConfig.getSmsToMmsTextThreshold();
|
||||
mMessageLengthRequiresMms = threshold > 0 && mMessageCount > threshold;
|
||||
}
|
||||
// Some carriers require any SMS message longer than 80 to be sent as MMS
|
||||
// see b/12122333
|
||||
int smsToMmsLengthThreshold = mmsConfig.getSmsToMmsTextLengthThreshold();
|
||||
if (smsToMmsLengthThreshold > 0) {
|
||||
final int usedInCurrentMessage = params[1];
|
||||
/*
|
||||
* A little hacky way to find out if we should count characters in double bytes.
|
||||
* SmsMessage.calculateLength counts message code units based on the characters
|
||||
* in input. If all of them are ascii, the max length is
|
||||
* SmsMessage.MAX_USER_DATA_SEPTETS (160). If any of them are double-byte, like
|
||||
* Korean or Chinese, the max length is SmsMessage.MAX_USER_DATA_BYTES (140) bytes
|
||||
* (70 code units).
|
||||
* Here we check if the total code units we can use is smaller than 140. If so,
|
||||
* we know we should count threshold in double-byte, so divide the threshold by 2.
|
||||
* In this way, we will count Korean text correctly with regard to the length threshold.
|
||||
*/
|
||||
if (usedInCurrentMessage + mCodePointsRemainingInCurrentMessage
|
||||
< SmsMessage.MAX_USER_DATA_BYTES) {
|
||||
smsToMmsLengthThreshold /= 2;
|
||||
}
|
||||
if (usedInCurrentMessage > smsToMmsLengthThreshold) {
|
||||
mMessageLengthRequiresMms = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
/*
|
||||
* 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.ContentProvider;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
|
||||
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||
import com.android.messaging.datamodel.data.ConversationMessageData;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
import com.android.messaging.widget.BugleWidgetProvider;
|
||||
import com.android.messaging.widget.WidgetConversationProvider;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
/**
|
||||
* A centralized provider for Uris exposed by Bugle.
|
||||
* */
|
||||
public class MessagingContentProvider extends ContentProvider {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
public static final String AUTHORITY =
|
||||
"com.android.messaging.datamodel.MessagingContentProvider";
|
||||
private static final String CONTENT_AUTHORITY = "content://" + AUTHORITY + '/';
|
||||
|
||||
// Conversations query
|
||||
private static final String CONVERSATIONS_QUERY = "conversations";
|
||||
|
||||
public static final Uri CONVERSATIONS_URI = Uri.parse(CONTENT_AUTHORITY + CONVERSATIONS_QUERY);
|
||||
static final Uri PARTS_URI = Uri.parse(CONTENT_AUTHORITY + DatabaseHelper.PARTS_TABLE);
|
||||
|
||||
// Messages query
|
||||
private static final String MESSAGES_QUERY = "messages";
|
||||
|
||||
static final Uri MESSAGES_URI = Uri.parse(CONTENT_AUTHORITY + MESSAGES_QUERY);
|
||||
|
||||
public static final Uri CONVERSATION_MESSAGES_URI = Uri.parse(CONTENT_AUTHORITY +
|
||||
MESSAGES_QUERY + "/conversation");
|
||||
|
||||
// Conversation participants query
|
||||
private static final String PARTICIPANTS_QUERY = "participants";
|
||||
|
||||
static class ConversationParticipantsQueryColumns extends ParticipantColumns {
|
||||
static final String CONVERSATION_ID = ConversationParticipantsColumns.CONVERSATION_ID;
|
||||
}
|
||||
|
||||
static final Uri CONVERSATION_PARTICIPANTS_URI = Uri.parse(CONTENT_AUTHORITY +
|
||||
PARTICIPANTS_QUERY + "/conversation");
|
||||
|
||||
public static final Uri PARTICIPANTS_URI = Uri.parse(CONTENT_AUTHORITY + PARTICIPANTS_QUERY);
|
||||
|
||||
// Conversation images query
|
||||
private static final String CONVERSATION_IMAGES_QUERY = "conversation_images";
|
||||
|
||||
public static final Uri CONVERSATION_IMAGES_URI = Uri.parse(CONTENT_AUTHORITY +
|
||||
CONVERSATION_IMAGES_QUERY);
|
||||
|
||||
private static final String DRAFT_IMAGES_QUERY = "draft_images";
|
||||
|
||||
public static final Uri DRAFT_IMAGES_URI = Uri.parse(CONTENT_AUTHORITY +
|
||||
DRAFT_IMAGES_QUERY);
|
||||
|
||||
/**
|
||||
* Notifies that <i>all</i> data exposed by the provider needs to be refreshed.
|
||||
* <p>
|
||||
* <b>IMPORTANT!</b> You probably shouldn't be calling this. Prefer to notify more specific
|
||||
* uri's instead. Currently only sync uses this, because sync can potentially update many
|
||||
* different tables at once.
|
||||
*/
|
||||
public static void notifyEverythingChanged() {
|
||||
final Uri uri = Uri.parse(CONTENT_AUTHORITY);
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final ContentResolver cr = context.getContentResolver();
|
||||
cr.notifyChange(uri, null);
|
||||
|
||||
// Notify any conversations widgets the conversation list has changed.
|
||||
BugleWidgetProvider.notifyConversationListChanged(context);
|
||||
|
||||
// Notify all conversation widgets to update.
|
||||
WidgetConversationProvider.notifyMessagesChanged(context, null /*conversationId*/);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a participant uri from the conversation id.
|
||||
*/
|
||||
public static Uri buildConversationParticipantsUri(final String conversationId) {
|
||||
final Uri.Builder builder = CONVERSATION_PARTICIPANTS_URI.buildUpon();
|
||||
builder.appendPath(conversationId);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static void notifyParticipantsChanged(final String conversationId) {
|
||||
final Uri uri = buildConversationParticipantsUri(conversationId);
|
||||
final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
|
||||
cr.notifyChange(uri, null);
|
||||
}
|
||||
|
||||
public static void notifyAllMessagesChanged() {
|
||||
final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
|
||||
cr.notifyChange(CONVERSATION_MESSAGES_URI, null);
|
||||
}
|
||||
|
||||
public static void notifyAllParticipantsChanged() {
|
||||
final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
|
||||
cr.notifyChange(CONVERSATION_PARTICIPANTS_URI, null);
|
||||
}
|
||||
|
||||
// Default value for unknown dimension of image
|
||||
public static final int UNSPECIFIED_SIZE = -1;
|
||||
|
||||
// Internal
|
||||
private static final int CONVERSATIONS_QUERY_CODE = 10;
|
||||
|
||||
private static final int CONVERSATION_QUERY_CODE = 20;
|
||||
private static final int CONVERSATION_MESSAGES_QUERY_CODE = 30;
|
||||
private static final int CONVERSATION_PARTICIPANTS_QUERY_CODE = 40;
|
||||
private static final int CONVERSATION_IMAGES_QUERY_CODE = 50;
|
||||
private static final int DRAFT_IMAGES_QUERY_CODE = 60;
|
||||
private static final int PARTICIPANTS_QUERY_CODE = 70;
|
||||
|
||||
// TODO: Move to a better structured URI namespace.
|
||||
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
static {
|
||||
sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY, CONVERSATIONS_QUERY_CODE);
|
||||
sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY + "/*", CONVERSATION_QUERY_CODE);
|
||||
sURIMatcher.addURI(AUTHORITY, MESSAGES_QUERY + "/conversation/*",
|
||||
CONVERSATION_MESSAGES_QUERY_CODE);
|
||||
sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY + "/conversation/*",
|
||||
CONVERSATION_PARTICIPANTS_QUERY_CODE);
|
||||
sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY, PARTICIPANTS_QUERY_CODE);
|
||||
sURIMatcher.addURI(AUTHORITY, CONVERSATION_IMAGES_QUERY + "/*",
|
||||
CONVERSATION_IMAGES_QUERY_CODE);
|
||||
sURIMatcher.addURI(AUTHORITY, DRAFT_IMAGES_QUERY + "/*",
|
||||
DRAFT_IMAGES_QUERY_CODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a messages uri from the conversation id.
|
||||
*/
|
||||
public static Uri buildConversationMessagesUri(final String conversationId) {
|
||||
final Uri.Builder builder = CONVERSATION_MESSAGES_URI.buildUpon();
|
||||
builder.appendPath(conversationId);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static void notifyMessagesChanged(final String conversationId) {
|
||||
final Uri uri = buildConversationMessagesUri(conversationId);
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final ContentResolver cr = context.getContentResolver();
|
||||
cr.notifyChange(uri, null);
|
||||
notifyConversationListChanged();
|
||||
|
||||
// Notify the widget the messages changed
|
||||
WidgetConversationProvider.notifyMessagesChanged(context, conversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a conversation metadata uri from a conversation id.
|
||||
*/
|
||||
public static Uri buildConversationMetadataUri(final String conversationId) {
|
||||
final Uri.Builder builder = CONVERSATIONS_URI.buildUpon();
|
||||
builder.appendPath(conversationId);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static void notifyConversationMetadataChanged(final String conversationId) {
|
||||
final Uri uri = buildConversationMetadataUri(conversationId);
|
||||
final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
|
||||
cr.notifyChange(uri, null);
|
||||
notifyConversationListChanged();
|
||||
}
|
||||
|
||||
public static void notifyPartsChanged() {
|
||||
final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
|
||||
cr.notifyChange(PARTS_URI, null);
|
||||
}
|
||||
|
||||
public static void notifyConversationListChanged() {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final ContentResolver cr = context.getContentResolver();
|
||||
cr.notifyChange(CONVERSATIONS_URI, null);
|
||||
|
||||
// Notify the widget the conversation list changed
|
||||
BugleWidgetProvider.notifyConversationListChanged(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a conversation images uri from a conversation id.
|
||||
*/
|
||||
public static Uri buildConversationImagesUri(final String conversationId) {
|
||||
final Uri.Builder builder = CONVERSATION_IMAGES_URI.buildUpon();
|
||||
builder.appendPath(conversationId);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a draft images uri from a conversation id.
|
||||
*/
|
||||
public static Uri buildDraftImagesUri(final String conversationId) {
|
||||
final Uri.Builder builder = DRAFT_IMAGES_URI.buildUpon();
|
||||
builder.appendPath(conversationId);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private DatabaseHelper mDatabaseHelper;
|
||||
private DatabaseWrapper mDatabaseWrapper;
|
||||
|
||||
public MessagingContentProvider() {
|
||||
super();
|
||||
}
|
||||
|
||||
private DatabaseWrapper getDatabaseWrapper() {
|
||||
if (mDatabaseWrapper == null) {
|
||||
mDatabaseWrapper = mDatabaseHelper.getDatabase();
|
||||
}
|
||||
return mDatabaseWrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull final Uri uri, final String[] projection, String selection,
|
||||
final String[] selectionArgs, String sortOrder) {
|
||||
final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
||||
|
||||
String[] queryArgs = selectionArgs;
|
||||
final int match = sURIMatcher.match(uri);
|
||||
String groupBy = null;
|
||||
String limit = null;
|
||||
switch (match) {
|
||||
case CONVERSATIONS_QUERY_CODE:
|
||||
queryBuilder.setTables(ConversationListItemData.getConversationListView());
|
||||
// Hide empty conversations (ones with 0 sort_timestamp)
|
||||
queryBuilder.appendWhere(ConversationColumns.SORT_TIMESTAMP + " > 0 ");
|
||||
break;
|
||||
case CONVERSATION_QUERY_CODE:
|
||||
queryBuilder.setTables(ConversationListItemData.getConversationListView());
|
||||
if (uri.getPathSegments().size() == 2) {
|
||||
queryBuilder.appendWhere(ConversationColumns._ID + "=?");
|
||||
// Get the conversation id from the uri
|
||||
queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Malformed URI " + uri);
|
||||
}
|
||||
break;
|
||||
case CONVERSATION_PARTICIPANTS_QUERY_CODE:
|
||||
queryBuilder.setTables(DatabaseHelper.PARTICIPANTS_TABLE);
|
||||
if (uri.getPathSegments().size() == 3 &&
|
||||
TextUtils.equals(uri.getPathSegments().get(1), "conversation")) {
|
||||
queryBuilder.appendWhere(ParticipantColumns._ID + " IN ( " + "SELECT "
|
||||
+ ConversationParticipantsColumns.PARTICIPANT_ID + " AS "
|
||||
+ ParticipantColumns._ID
|
||||
+ " FROM " + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE
|
||||
+ " WHERE " + ConversationParticipantsColumns.CONVERSATION_ID
|
||||
+ " =? UNION SELECT " + ParticipantColumns._ID + " FROM "
|
||||
+ DatabaseHelper.PARTICIPANTS_TABLE + " WHERE "
|
||||
+ ParticipantColumns.SUB_ID + " != "
|
||||
+ ParticipantData.OTHER_THAN_SELF_SUB_ID + " )");
|
||||
// Get the conversation id from the uri
|
||||
queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(2));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Malformed URI " + uri);
|
||||
}
|
||||
break;
|
||||
case PARTICIPANTS_QUERY_CODE:
|
||||
queryBuilder.setTables(DatabaseHelper.PARTICIPANTS_TABLE);
|
||||
if (uri.getPathSegments().size() != 1) {
|
||||
throw new IllegalArgumentException("Malformed URI " + uri);
|
||||
}
|
||||
break;
|
||||
case CONVERSATION_MESSAGES_QUERY_CODE:
|
||||
if (uri.getPathSegments().size() == 3 &&
|
||||
TextUtils.equals(uri.getPathSegments().get(1), "conversation")) {
|
||||
// Get the conversation id from the uri
|
||||
final String conversationId = uri.getPathSegments().get(2);
|
||||
|
||||
// We need to handle this query differently, instead of falling through to the
|
||||
// generic query call at the bottom. For performance reasons, the conversation
|
||||
// messages query is executed as a raw query. It is invalid to specify
|
||||
// selection/sorting for this query.
|
||||
|
||||
if (selection == null && selectionArgs == null && sortOrder == null) {
|
||||
return queryConversationMessages(conversationId, uri);
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot set selection or sort order with this query");
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Malformed URI " + uri);
|
||||
}
|
||||
case CONVERSATION_IMAGES_QUERY_CODE:
|
||||
queryBuilder.setTables(ConversationImagePartsView.getViewName());
|
||||
if (uri.getPathSegments().size() == 2) {
|
||||
// Exclude draft.
|
||||
queryBuilder.appendWhere(
|
||||
ConversationImagePartsView.Columns.CONVERSATION_ID + " =? AND " +
|
||||
ConversationImagePartsView.Columns.STATUS + "<>" +
|
||||
MessageData.BUGLE_STATUS_OUTGOING_DRAFT);
|
||||
// Get the conversation id from the uri
|
||||
queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Malformed URI " + uri);
|
||||
}
|
||||
break;
|
||||
case DRAFT_IMAGES_QUERY_CODE:
|
||||
queryBuilder.setTables(ConversationImagePartsView.getViewName());
|
||||
if (uri.getPathSegments().size() == 2) {
|
||||
// Draft only.
|
||||
queryBuilder.appendWhere(
|
||||
ConversationImagePartsView.Columns.CONVERSATION_ID + " =? AND " +
|
||||
ConversationImagePartsView.Columns.STATUS + "=" +
|
||||
MessageData.BUGLE_STATUS_OUTGOING_DRAFT);
|
||||
// Get the conversation id from the uri
|
||||
queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Malformed URI " + uri);
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
throw new IllegalArgumentException("Unknown URI " + uri);
|
||||
}
|
||||
}
|
||||
|
||||
final Cursor cursor = getDatabaseWrapper().query(queryBuilder, projection, selection,
|
||||
queryArgs, groupBy, null, sortOrder, limit);
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
private Cursor queryConversationMessages(final String conversationId, final Uri notifyUri) {
|
||||
final String[] queryArgs = { conversationId };
|
||||
final Cursor cursor = getDatabaseWrapper().rawQuery(
|
||||
ConversationMessageData.getConversationMessagesQuerySql(), queryArgs);
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), notifyUri);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull final Uri uri) {
|
||||
final StringBuilder sb = new
|
||||
StringBuilder("vnd.android.cursor.dir/vnd.android.messaging.");
|
||||
|
||||
switch (sURIMatcher.match(uri)) {
|
||||
case CONVERSATIONS_QUERY_CODE: {
|
||||
sb.append(CONVERSATIONS_QUERY);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
protected DatabaseHelper getDatabase() {
|
||||
return DatabaseHelper.getInstance(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(@NonNull final Uri uri, @NonNull final String fileMode)
|
||||
throws FileNotFoundException {
|
||||
throw new IllegalArgumentException("openFile not supported: " + uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(@NonNull final Uri uri, final ContentValues values) {
|
||||
throw new IllegalStateException("Insert not supported " + uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull final Uri uri, final String selection, final String[] selectionArgs) {
|
||||
throw new IllegalArgumentException("Delete not supported: " + uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull final Uri uri, final ContentValues values, final String selection,
|
||||
final String[] selectionArgs) {
|
||||
throw new IllegalArgumentException("Update not supported: " + uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends new arguments to the existing argument list.
|
||||
*
|
||||
* @param oldArgList The current list of arguments. May be {@code null}
|
||||
* @param args The new arguments to prepend
|
||||
* @return A new argument list with the given arguments prepended
|
||||
*/
|
||||
private String[] prependArgs(final String[] oldArgList, final String... args) {
|
||||
if (args == null || args.length == 0) {
|
||||
return oldArgList;
|
||||
}
|
||||
final int oldArgCount = (oldArgList == null ? 0 : oldArgList.length);
|
||||
final int newArgCount = args.length;
|
||||
|
||||
final String[] newArgs = new String[oldArgCount + newArgCount];
|
||||
System.arraycopy(args, 0, newArgs, 0, newArgCount);
|
||||
if (oldArgCount > 0) {
|
||||
System.arraycopy(oldArgList, 0, newArgs, newArgCount, oldArgCount);
|
||||
}
|
||||
return newArgs;
|
||||
}
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void dump(final FileDescriptor fd, final PrintWriter writer, final String[] args) {
|
||||
// First dump out the default SMS app package name
|
||||
String defaultSmsApp = PhoneUtils.getDefault().getDefaultSmsApp();
|
||||
if (TextUtils.isEmpty(defaultSmsApp)) {
|
||||
defaultSmsApp = "None";
|
||||
}
|
||||
writer.println("Default SMS app: " + defaultSmsApp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
// This is going to wind up calling into createDatabase() below.
|
||||
mDatabaseHelper = getDatabase();
|
||||
// We cannot initialize mDatabaseWrapper yet as the Factory may not be initialized
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.net.Uri;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A very simple content provider that can serve mms files from our cache directory.
|
||||
*/
|
||||
public class MmsFileProvider extends FileProvider {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
static final String AUTHORITY = "com.android.messaging.datamodel.MmsFileProvider";
|
||||
private static final String RAW_MMS_DIR = "rawmms";
|
||||
|
||||
/**
|
||||
* Returns a uri that can be used to access a raw mms file.
|
||||
*
|
||||
* @return the URI for an raw mms file
|
||||
*/
|
||||
public static Uri buildRawMmsUri() {
|
||||
final Uri uri = FileProvider.buildFileUri(AUTHORITY, null);
|
||||
final File file = getFile(uri.getPath());
|
||||
if (!ensureFileExists(file)) {
|
||||
LogUtil.e(TAG, "Failed to create temp file " + file.getAbsolutePath());
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
File getFile(final String path, final String extension) {
|
||||
return getFile(path);
|
||||
}
|
||||
|
||||
public static File getFile(final Uri uri) {
|
||||
return getFile(uri.getPath());
|
||||
}
|
||||
|
||||
private static File getFile(final String path) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final File filePath = new File(getDirectory(context), path + ".dat");
|
||||
|
||||
try {
|
||||
if (!filePath.getCanonicalPath()
|
||||
.startsWith(getDirectory(context).getCanonicalPath())) {
|
||||
LogUtil.e(TAG, "getFile: path "
|
||||
+ filePath.getCanonicalPath()
|
||||
+ " does not start with "
|
||||
+ getDirectory(context).getCanonicalPath());
|
||||
return null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtil.e(TAG, "getFile: getCanonicalPath failed ", e);
|
||||
return null;
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private static File getDirectory(final Context context) {
|
||||
return new File(context.getCacheDir(), RAW_MMS_DIR);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 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.app.IntentService;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import androidx.core.app.RemoteInput;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.datamodel.action.InsertNewMessageAction;
|
||||
import com.android.messaging.datamodel.action.UpdateMessageNotificationAction;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.ui.conversationlist.ConversationListActivity;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.UriUtil;
|
||||
|
||||
/**
|
||||
* Respond to a special intent and send an SMS message without the user's intervention, unless
|
||||
* the intent extra "showUI" is true.
|
||||
*/
|
||||
public class NoConfirmationSmsSendService extends IntentService {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
private static final String EXTRA_SUBSCRIPTION = "subscription";
|
||||
public static final String EXTRA_SELF_ID = "self_id";
|
||||
|
||||
public NoConfirmationSmsSendService() {
|
||||
// Class name will be the thread name.
|
||||
super(NoConfirmationSmsSendService.class.getName());
|
||||
|
||||
// Intent should be redelivered if the process gets killed before completing the job.
|
||||
setIntentRedelivery(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(final Intent intent) {
|
||||
LogUtil.v(TAG, "NoConfirmationSmsSendService onHandleIntent");
|
||||
|
||||
final String action = intent.getAction();
|
||||
if (!TelephonyManager.ACTION_RESPOND_VIA_MESSAGE.equals(action)) {
|
||||
LogUtil.v(TAG, "NoConfirmationSmsSendService onHandleIntent wrong action: " + action);
|
||||
return;
|
||||
}
|
||||
final Bundle extras = intent.getExtras();
|
||||
if (extras == null) {
|
||||
LogUtil.v(TAG, "Called to send SMS but no extras");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all possible extras from intent
|
||||
final String conversationId =
|
||||
intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID);
|
||||
final String selfId = intent.getStringExtra(EXTRA_SELF_ID);
|
||||
final boolean requiresMms = intent.getBooleanExtra(UIIntents.UI_INTENT_EXTRA_REQUIRES_MMS,
|
||||
false);
|
||||
final String message = getText(intent, Intent.EXTRA_TEXT);
|
||||
final String subject = getText(intent, Intent.EXTRA_SUBJECT);
|
||||
final int subId = extras.getInt(EXTRA_SUBSCRIPTION, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
|
||||
// Get a comma-separated list of recipients
|
||||
final String recipients = UriUtil.parseRecipientsFromSmsMmsUri(intent.getData());
|
||||
|
||||
if (TextUtils.isEmpty(recipients) && TextUtils.isEmpty(conversationId)) {
|
||||
LogUtil.v(TAG, "Both conversationId and recipient(s) cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (extras.getBoolean("showUI", false)) {
|
||||
startActivity(new Intent(this, ConversationListActivity.class));
|
||||
} else {
|
||||
if (TextUtils.isEmpty(message)) {
|
||||
LogUtil.v(TAG, "Message cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: it's possible that a long message would require sending it via mms,
|
||||
// but we're not testing for that here and we're sending the message as an sms.
|
||||
|
||||
if (TextUtils.isEmpty(conversationId)) {
|
||||
InsertNewMessageAction.insertNewMessage(subId, recipients, message, subject);
|
||||
} else {
|
||||
MessageData messageData;
|
||||
if (requiresMms) {
|
||||
LogUtil.v(TAG, "Auto-sending MMS message in conversation: " +
|
||||
conversationId);
|
||||
messageData = MessageData.createDraftMmsMessage(conversationId, selfId, message,
|
||||
subject);
|
||||
} else {
|
||||
LogUtil.v(TAG, "Auto-sending SMS message in conversation: " +
|
||||
conversationId);
|
||||
messageData = MessageData.createDraftSmsMessage(conversationId, selfId,
|
||||
message);
|
||||
}
|
||||
InsertNewMessageAction.insertNewMessage(messageData);
|
||||
}
|
||||
UpdateMessageNotificationAction.updateMessageNotification();
|
||||
}
|
||||
}
|
||||
|
||||
private String getText(final Intent intent, final String textType) {
|
||||
final String message = intent.getStringExtra(textType);
|
||||
if (message == null) {
|
||||
final Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
|
||||
if (remoteInput != null) {
|
||||
final CharSequence extra = remoteInput.getCharSequence(textType);
|
||||
if (extra != null) {
|
||||
return extra.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.app.PendingIntent;
|
||||
import android.net.Uri;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.util.ConversationIdSet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* Base class for representing notifications. The main reason for this class is that in order to
|
||||
* show pictures or avatars they might need to be loaded in the background. This class and
|
||||
* subclasses can do the main work to get the notification ready and then wait until any images
|
||||
* that are needed are ready before posting.
|
||||
*
|
||||
* The creation of a notification is split into two parts. The NotificationState ctor should
|
||||
* setup the basic information including the mContentIntent. A Notification Builder is created in
|
||||
* RealTimeChatNotifications and passed to the build() method of each notification where the
|
||||
* Notification is fully specified.
|
||||
*
|
||||
* TODO: There is still some duplication and inconsistency in the utility functions and
|
||||
* placement of different building blocks across notification types (e.g. summary text for accounts)
|
||||
*/
|
||||
public abstract class NotificationState {
|
||||
private static final int CONTENT_INTENT_REQUEST_CODE_OFFSET = 0;
|
||||
private static final int CLEAR_INTENT_REQUEST_CODE_OFFSET = 1;
|
||||
private static final int NUM_REQUEST_CODES_NEEDED = 2;
|
||||
|
||||
public interface FailedMessageQuery {
|
||||
String FAILED_MESSAGES_WHERE_CLAUSE =
|
||||
"((" + MessageColumns.STATUS + " = " +
|
||||
MessageData.BUGLE_STATUS_OUTGOING_FAILED + " OR " +
|
||||
MessageColumns.STATUS + " = " +
|
||||
MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED + ") AND " +
|
||||
DatabaseHelper.MessageColumns.SEEN + " = 0)";
|
||||
|
||||
String FAILED_ORDER_BY = DatabaseHelper.MessageColumns.CONVERSATION_ID + ", " +
|
||||
DatabaseHelper.MessageColumns.SENT_TIMESTAMP + " asc";
|
||||
}
|
||||
|
||||
public final ConversationIdSet mConversationIds;
|
||||
public final HashSet<String> mPeople;
|
||||
|
||||
public NotificationCompat.Style mNotificationStyle;
|
||||
public NotificationCompat.Builder mNotificationBuilder;
|
||||
public boolean mCanceled;
|
||||
public int mType;
|
||||
public int mBaseRequestCode;
|
||||
public ArrayList<Uri> mParticipantAvatarsUris = null;
|
||||
public ArrayList<Uri> mParticipantContactUris = null;
|
||||
|
||||
NotificationState(final ConversationIdSet conversationIds) {
|
||||
mConversationIds = conversationIds;
|
||||
mPeople = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* The intent to be triggered when the notification is dismissed.
|
||||
*/
|
||||
public abstract PendingIntent getClearIntent();
|
||||
|
||||
protected Uri getAttachmentUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Returns the mime type of the attachment (See ContentType class for definitions)
|
||||
protected String getAttachmentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the notification using the given builder.
|
||||
* @return The style of the notification.
|
||||
*/
|
||||
protected abstract NotificationCompat.Style build(NotificationCompat.Builder builder);
|
||||
|
||||
/**
|
||||
* Reserves request codes for this notification type. By default 2 codes are reserved, one for
|
||||
* the main intent and another for the cancel intent. Override this function to reserve more.
|
||||
*/
|
||||
public int getNumRequestCodesNeeded() {
|
||||
return NUM_REQUEST_CODES_NEEDED;
|
||||
}
|
||||
|
||||
public int getClearIntentRequestCode() {
|
||||
return mBaseRequestCode + CLEAR_INTENT_REQUEST_CODE_OFFSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriate icon needed for notifications.
|
||||
*/
|
||||
public abstract int getIcon();
|
||||
|
||||
/**
|
||||
* @return the notification priority level for this notification.
|
||||
*/
|
||||
public abstract int getPriority();
|
||||
|
||||
public long getLatestReceivedTimestamp() {
|
||||
return Long.MIN_VALUE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,695 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 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.ContentValues;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.graphics.Color;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Phone;
|
||||
import androidx.collection.ArrayMap;
|
||||
import android.telephony.SubscriptionInfo;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData.ParticipantsQuery;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.ContactUtil;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
import com.google.common.base.Joiner;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Utility class for refreshing participant information based on matching contact. This updates
|
||||
* 1. name, photo_uri, matching contact_id of participants.
|
||||
* 2. generated_name of conversations.
|
||||
*
|
||||
* There are two kinds of participant refreshes,
|
||||
* 1. Full refresh, this is triggered at application start or activity resumes after contact
|
||||
* change is detected.
|
||||
* 2. Partial refresh, this is triggered when a participant is added to a conversation. This
|
||||
* normally happens during SMS sync.
|
||||
*/
|
||||
public class ParticipantRefresh {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
/**
|
||||
* Refresh all participants including ones that were resolved before.
|
||||
*/
|
||||
public static final int REFRESH_MODE_FULL = 0;
|
||||
|
||||
/**
|
||||
* Refresh all unresolved participants.
|
||||
*/
|
||||
public static final int REFRESH_MODE_INCREMENTAL = 1;
|
||||
|
||||
/**
|
||||
* Force refresh all self participants.
|
||||
*/
|
||||
public static final int REFRESH_MODE_SELF_ONLY = 2;
|
||||
|
||||
public static class ConversationParticipantsQuery {
|
||||
public static final String[] PROJECTION = new String[] {
|
||||
ConversationParticipantsColumns._ID,
|
||||
ConversationParticipantsColumns.CONVERSATION_ID,
|
||||
ConversationParticipantsColumns.PARTICIPANT_ID
|
||||
};
|
||||
|
||||
public static final int INDEX_ID = 0;
|
||||
public static final int INDEX_CONVERSATION_ID = 1;
|
||||
public static final int INDEX_PARTICIPANT_ID = 2;
|
||||
}
|
||||
|
||||
// Track whether observer is initialized or not.
|
||||
private static volatile boolean sObserverInitialized = false;
|
||||
private static final Object sLock = new Object();
|
||||
private static final AtomicBoolean sFullRefreshScheduled = new AtomicBoolean(false);
|
||||
private static final Runnable sFullRefreshRunnable = () -> {
|
||||
final boolean oldScheduled = sFullRefreshScheduled.getAndSet(false);
|
||||
Assert.isTrue(oldScheduled);
|
||||
refreshParticipants(REFRESH_MODE_FULL);
|
||||
};
|
||||
private static final Runnable sSelfOnlyRefreshRunnable = () ->
|
||||
refreshParticipants(REFRESH_MODE_SELF_ONLY);
|
||||
|
||||
/**
|
||||
* A customized content resolver to track contact changes.
|
||||
*/
|
||||
public static class ContactContentObserver extends ContentObserver {
|
||||
private volatile boolean mContactChanged = false;
|
||||
|
||||
public ContactContentObserver() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(final boolean selfChange) {
|
||||
super.onChange(selfChange);
|
||||
LogUtil.v(TAG, "Contacts changed");
|
||||
mContactChanged = true;
|
||||
}
|
||||
|
||||
public boolean getContactChanged() {
|
||||
return mContactChanged;
|
||||
}
|
||||
|
||||
public void resetContactChanged() {
|
||||
mContactChanged = false;
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
// TODO: Handle enterprise contacts post M once contacts provider supports it
|
||||
Factory.get().getApplicationContext().getContentResolver().registerContentObserver(
|
||||
Phone.CONTENT_URI, true, this);
|
||||
mContactChanged = true; // Force a full refresh on initialization.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh participants only if needed, i.e., application start or contact changed.
|
||||
*/
|
||||
public static void refreshParticipantsIfNeeded() {
|
||||
if (ParticipantRefresh.getNeedFullRefresh() &&
|
||||
sFullRefreshScheduled.compareAndSet(false, true)) {
|
||||
LogUtil.v(TAG, "Started full participant refresh");
|
||||
Executors.newSingleThreadExecutor().execute(sFullRefreshRunnable);
|
||||
} else {
|
||||
LogUtil.v(TAG, "Skipped full participant refresh");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh self participants on subscription or settings change.
|
||||
*/
|
||||
public static void refreshSelfParticipants() {
|
||||
Executors.newSingleThreadExecutor().execute(sSelfOnlyRefreshRunnable);
|
||||
}
|
||||
|
||||
private static boolean getNeedFullRefresh() {
|
||||
final ContactContentObserver observer = Factory.get().getContactContentObserver();
|
||||
if (observer == null) {
|
||||
// If there is no observer (for unittest cases), we don't need to refresh participants.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sObserverInitialized) {
|
||||
synchronized (sLock) {
|
||||
if (!sObserverInitialized) {
|
||||
observer.initialize();
|
||||
sObserverInitialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return observer.getContactChanged();
|
||||
}
|
||||
|
||||
private static void resetNeedFullRefresh() {
|
||||
final ContactContentObserver observer = Factory.get().getContactContentObserver();
|
||||
if (observer != null) {
|
||||
observer.resetContactChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is totally static. Make constructor to be private so that an instance
|
||||
* of this class would not be created by by mistake.
|
||||
*/
|
||||
private ParticipantRefresh() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh participants in Bugle.
|
||||
*
|
||||
* @param refreshMode the refresh mode desired. See {@link #REFRESH_MODE_FULL},
|
||||
* {@link #REFRESH_MODE_INCREMENTAL}, and {@link #REFRESH_MODE_SELF_ONLY}
|
||||
*/
|
||||
static void refreshParticipants(final int refreshMode) {
|
||||
Assert.inRange(refreshMode, REFRESH_MODE_FULL, REFRESH_MODE_SELF_ONLY);
|
||||
switch (refreshMode) {
|
||||
case REFRESH_MODE_FULL:
|
||||
LogUtil.v(TAG, "Start full participant refresh");
|
||||
break;
|
||||
case REFRESH_MODE_INCREMENTAL:
|
||||
LogUtil.v(TAG, "Start partial participant refresh");
|
||||
break;
|
||||
case REFRESH_MODE_SELF_ONLY:
|
||||
LogUtil.v(TAG, "Start self participant refresh");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!ContactUtil.hasReadContactsPermission() || !OsUtil.hasPhonePermission()) {
|
||||
LogUtil.v(TAG, "Skipping participant referesh because of permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
if (refreshMode == REFRESH_MODE_FULL) {
|
||||
// resetNeedFullRefresh right away so that we will skip duplicated full refresh
|
||||
// requests.
|
||||
resetNeedFullRefresh();
|
||||
}
|
||||
|
||||
if (refreshMode == REFRESH_MODE_FULL || refreshMode == REFRESH_MODE_SELF_ONLY) {
|
||||
refreshSelfParticipantList();
|
||||
}
|
||||
|
||||
final ArrayList<String> changedParticipants = new ArrayList<>();
|
||||
|
||||
String selection = null;
|
||||
String[] selectionArgs = null;
|
||||
|
||||
if (refreshMode == REFRESH_MODE_INCREMENTAL) {
|
||||
// In case of incremental refresh, filter out participants that are already resolved.
|
||||
selection = ParticipantColumns.CONTACT_ID + "=?";
|
||||
selectionArgs = new String[] {
|
||||
String.valueOf(ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED) };
|
||||
} else if (refreshMode == REFRESH_MODE_SELF_ONLY) {
|
||||
// In case of self-only refresh, filter out non-self participants.
|
||||
selection = SELF_PARTICIPANTS_CLAUSE;
|
||||
selectionArgs = null;
|
||||
}
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
Cursor cursor = null;
|
||||
boolean selfUpdated = false;
|
||||
try {
|
||||
cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE,
|
||||
ParticipantsQuery.PROJECTION, selection, selectionArgs, null, null, null);
|
||||
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
final ParticipantData participantData =
|
||||
ParticipantData.getFromCursor(cursor);
|
||||
if (refreshParticipant(db, participantData)) {
|
||||
if (participantData.isSelf()) {
|
||||
selfUpdated = true;
|
||||
}
|
||||
updateParticipant(db, participantData);
|
||||
final String id = participantData.getId();
|
||||
changedParticipants.add(id);
|
||||
}
|
||||
} catch (final Exception exception) {
|
||||
// Failure to update one participant shouldn't cancel the entire refresh.
|
||||
// Log the failure so we know what's going on and resume the loop.
|
||||
LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "ParticipantRefresh: Failed to " +
|
||||
"update participant", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
LogUtil.v(TAG, "Number of participants refreshed:" + changedParticipants.size());
|
||||
|
||||
// Refresh conversations for participants that are changed.
|
||||
if (changedParticipants.size() > 0) {
|
||||
BugleDatabaseOperations.refreshConversationsForParticipants(changedParticipants);
|
||||
}
|
||||
if (selfUpdated) {
|
||||
// Boom
|
||||
MessagingContentProvider.notifyAllParticipantsChanged();
|
||||
MessagingContentProvider.notifyAllMessagesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private static final String SELF_PARTICIPANTS_CLAUSE = ParticipantColumns.SUB_ID
|
||||
+ " NOT IN ( "
|
||||
+ ParticipantData.OTHER_THAN_SELF_SUB_ID
|
||||
+ " )";
|
||||
|
||||
private static Set<Integer> getExistingSubIds() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final HashSet<Integer> existingSubIds = new HashSet<>();
|
||||
|
||||
try (Cursor cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE,
|
||||
ParticipantsQuery.PROJECTION,
|
||||
SELF_PARTICIPANTS_CLAUSE, null, null, null, null)) {
|
||||
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
final int subId = cursor.getInt(ParticipantsQuery.INDEX_SUB_ID);
|
||||
existingSubIds.add(subId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return existingSubIds;
|
||||
}
|
||||
|
||||
private static final String UPDATE_SELF_PARTICIPANT_SUBSCRIPTION_SQL =
|
||||
"UPDATE " + DatabaseHelper.PARTICIPANTS_TABLE + " SET "
|
||||
+ ParticipantColumns.SIM_SLOT_ID + " = %d, "
|
||||
+ ParticipantColumns.SUBSCRIPTION_COLOR + " = %d, "
|
||||
+ ParticipantColumns.SUBSCRIPTION_NAME + " = %s "
|
||||
+ " WHERE %s";
|
||||
|
||||
static String getUpdateSelfParticipantSubscriptionInfoSql(final int slotId,
|
||||
final int subscriptionColor, final String subscriptionName, final String where) {
|
||||
return String.format((Locale) null /* construct SQL string without localization */,
|
||||
UPDATE_SELF_PARTICIPANT_SUBSCRIPTION_SQL,
|
||||
slotId, subscriptionColor, subscriptionName, where);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that there is a self participant corresponding to every active SIM. Also, ensure
|
||||
* that any other older SIM self participants are marked as inactive.
|
||||
*/
|
||||
private static void refreshSelfParticipantList() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
final List<SubscriptionInfo> subInfoRecords =
|
||||
PhoneUtils.getDefault().getActiveSubscriptionInfoList();
|
||||
final ArrayMap<Integer, SubscriptionInfo> activeSubscriptionIdToRecordMap =
|
||||
new ArrayMap<>();
|
||||
db.beginTransaction();
|
||||
final Set<Integer> existingSubIds = getExistingSubIds();
|
||||
|
||||
try {
|
||||
if (subInfoRecords != null) {
|
||||
for (final SubscriptionInfo subInfoRecord : subInfoRecords) {
|
||||
final int subId = subInfoRecord.getSubscriptionId();
|
||||
// If its a new subscription, add it to the database.
|
||||
if (!existingSubIds.contains(subId)) {
|
||||
db.execSQL(DatabaseHelper.getCreateSelfParticipantSql(subId));
|
||||
// Add it to the local set to guard against duplicated entries returned
|
||||
// by subscription manager.
|
||||
existingSubIds.add(subId);
|
||||
}
|
||||
activeSubscriptionIdToRecordMap.put(subId, subInfoRecord);
|
||||
|
||||
if (subId == PhoneUtils.getDefault().getDefaultSmsSubscriptionId()) {
|
||||
// This is the system default subscription, so update the default self.
|
||||
activeSubscriptionIdToRecordMap.put(ParticipantData.DEFAULT_SELF_SUB_ID,
|
||||
subInfoRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For subscriptions already in the database, refresh ParticipantColumns.SIM_SLOT_ID.
|
||||
for (final Integer subId : activeSubscriptionIdToRecordMap.keySet()) {
|
||||
final SubscriptionInfo record = activeSubscriptionIdToRecordMap.get(subId);
|
||||
final String displayName =
|
||||
DatabaseUtils.sqlEscapeString(record.getDisplayName().toString());
|
||||
db.execSQL(getUpdateSelfParticipantSubscriptionInfoSql(record.getSimSlotIndex(),
|
||||
record.getIconTint(), displayName,
|
||||
ParticipantColumns.SUB_ID + " = " + subId));
|
||||
}
|
||||
db.execSQL(getUpdateSelfParticipantSubscriptionInfoSql(
|
||||
ParticipantData.INVALID_SLOT_ID, Color.TRANSPARENT, "''",
|
||||
ParticipantColumns.SUB_ID + " NOT IN (" +
|
||||
Joiner.on(", ").join(activeSubscriptionIdToRecordMap.keySet()) + ")"));
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
// Fix up conversation self ids by reverting to default self for conversations whose self
|
||||
// ids are no longer active.
|
||||
refreshConversationSelfIds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh one participant.
|
||||
* @return true if the ParticipantData was changed
|
||||
*/
|
||||
public static boolean refreshParticipant(final DatabaseWrapper db,
|
||||
final ParticipantData participantData) {
|
||||
boolean updated = false;
|
||||
|
||||
if (participantData.isSelf()) {
|
||||
final int selfChange = refreshFromSelfProfile(db, participantData);
|
||||
|
||||
if (selfChange == SELF_PROFILE_EXISTS) {
|
||||
// If a self-profile exists, it takes precedence over Contacts data. So we are done.
|
||||
return true;
|
||||
}
|
||||
|
||||
updated = (selfChange == SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED);
|
||||
|
||||
// Fall-through and try to update based on Contacts data
|
||||
}
|
||||
|
||||
updated |= refreshFromContacts(db, participantData);
|
||||
return updated;
|
||||
}
|
||||
|
||||
private static final int SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED = 1;
|
||||
private static final int SELF_PROFILE_EXISTS = 2;
|
||||
|
||||
private static int refreshFromSelfProfile(final DatabaseWrapper db,
|
||||
final ParticipantData participantData) {
|
||||
int changed = 0;
|
||||
// Refresh the phone number based on information from telephony
|
||||
if (participantData.updatePhoneNumberForSelfIfChanged()) {
|
||||
changed = SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED;
|
||||
}
|
||||
|
||||
// Refresh the subscription info based on information from SubscriptionManager.
|
||||
final SubscriptionInfo subscriptionInfo =
|
||||
PhoneUtils.get(participantData.getSubId()).getActiveSubscriptionInfo();
|
||||
if (participantData.updateSubscriptionInfoForSelfIfChanged(subscriptionInfo)) {
|
||||
changed = SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED;
|
||||
}
|
||||
|
||||
// For self participant, try getting name/avatar from self profile in CP2 first.
|
||||
// TODO: in case of multi-sim, profile would not be able to be used for
|
||||
// different numbers. Need to figure out that.
|
||||
try (Cursor selfCursor = ContactUtil.getSelf(db.getContext()).performSynchronousQuery()) {
|
||||
if (selfCursor != null && selfCursor.getCount() > 0) {
|
||||
selfCursor.moveToNext();
|
||||
final long selfContactId = selfCursor.getLong(ContactUtil.INDEX_CONTACT_ID);
|
||||
participantData.setContactId(selfContactId);
|
||||
participantData.setFullName(selfCursor.getString(
|
||||
ContactUtil.INDEX_DISPLAY_NAME));
|
||||
participantData.setFirstName(
|
||||
ContactUtil.lookupFirstName(db.getContext(), selfContactId));
|
||||
participantData.setProfilePhotoUri(selfCursor.getString(
|
||||
ContactUtil.INDEX_PHOTO_URI));
|
||||
participantData.setLookupKey(selfCursor.getString(
|
||||
ContactUtil.INDEX_SELF_QUERY_LOOKUP_KEY));
|
||||
return SELF_PROFILE_EXISTS;
|
||||
}
|
||||
} catch (final Exception exception) {
|
||||
// It's possible for contact query to fail and we don't want that to crash our app.
|
||||
// However, we need to at least log the exception so we know something was wrong.
|
||||
LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "Participant refresh: failed to refresh " +
|
||||
"participant. exception=" + exception);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static boolean refreshFromContacts(final DatabaseWrapper db,
|
||||
final ParticipantData participantData) {
|
||||
final String normalizedDestination = participantData.getNormalizedDestination();
|
||||
final long currentContactId = participantData.getContactId();
|
||||
final String currentDisplayName = participantData.getFullName();
|
||||
final String currentFirstName = participantData.getFirstName();
|
||||
final String currentPhotoUri = participantData.getProfilePhotoUri();
|
||||
final String currentContactDestination = participantData.getContactDestination();
|
||||
|
||||
Cursor matchingContactCursor = null;
|
||||
long matchingContactId = -1;
|
||||
String matchingDisplayName = null;
|
||||
String matchingFirstName = null;
|
||||
String matchingPhotoUri = null;
|
||||
String matchingLookupKey = null;
|
||||
String matchingDestination = null;
|
||||
boolean updated = false;
|
||||
|
||||
if (TextUtils.isEmpty(normalizedDestination)) {
|
||||
// The normalized destination can be "" for the self id if we can't get it from the
|
||||
// SIM. Some contact providers throw an IllegalArgumentException if you lookup "",
|
||||
// so we early out.
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
matchingContactCursor = ContactUtil.lookupDestination(db.getContext(),
|
||||
normalizedDestination).performSynchronousQuery();
|
||||
if (matchingContactCursor == null || matchingContactCursor.getCount() == 0) {
|
||||
// If there is no match, mark the participant as contact not found.
|
||||
if (currentContactId != ParticipantData.PARTICIPANT_CONTACT_ID_NOT_FOUND) {
|
||||
participantData.setContactId(ParticipantData.PARTICIPANT_CONTACT_ID_NOT_FOUND);
|
||||
participantData.setFullName(null);
|
||||
participantData.setFirstName(null);
|
||||
participantData.setProfilePhotoUri(null);
|
||||
participantData.setLookupKey(null);
|
||||
updated = true;
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
while (matchingContactCursor.moveToNext()) {
|
||||
final long contactId = matchingContactCursor.getLong(ContactUtil.INDEX_CONTACT_ID);
|
||||
// Pick either the first contact or the contact with same id as previous matched
|
||||
// contact id.
|
||||
if (matchingContactId == -1 || currentContactId == contactId) {
|
||||
matchingContactId = contactId;
|
||||
matchingDisplayName = matchingContactCursor.getString(
|
||||
ContactUtil.INDEX_DISPLAY_NAME);
|
||||
matchingFirstName = ContactUtil.lookupFirstName(db.getContext(), contactId);
|
||||
matchingPhotoUri = matchingContactCursor.getString(
|
||||
ContactUtil.INDEX_PHOTO_URI);
|
||||
matchingLookupKey = matchingContactCursor.getString(
|
||||
ContactUtil.INDEX_LOOKUP_KEY);
|
||||
matchingDestination = matchingContactCursor.getString(
|
||||
ContactUtil.INDEX_PHONE_EMAIL);
|
||||
}
|
||||
|
||||
// There is no need to try other contacts if the current contactId was not filled...
|
||||
if (currentContactId < 0
|
||||
// or we found the matching contact id
|
||||
|| currentContactId == contactId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (final Exception exception) {
|
||||
// It's possible for contact query to fail and we don't want that to crash our app.
|
||||
// However, we need to at least log the exception so we know something was wrong.
|
||||
LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "Participant refresh: failed to refresh " +
|
||||
"participant. exception=" + exception);
|
||||
return false;
|
||||
} finally {
|
||||
if (matchingContactCursor != null) {
|
||||
matchingContactCursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Update participant only if something changed.
|
||||
final boolean isContactIdChanged = (matchingContactId != currentContactId);
|
||||
final boolean isDisplayNameChanged =
|
||||
!TextUtils.equals(matchingDisplayName, currentDisplayName);
|
||||
final boolean isFirstNameChanged = !TextUtils.equals(matchingFirstName, currentFirstName);
|
||||
final boolean isPhotoUrlChanged = !TextUtils.equals(matchingPhotoUri, currentPhotoUri);
|
||||
final boolean isDestinationChanged = !TextUtils.equals(matchingDestination,
|
||||
currentContactDestination);
|
||||
|
||||
if (isContactIdChanged || isDisplayNameChanged || isFirstNameChanged || isPhotoUrlChanged
|
||||
|| isDestinationChanged) {
|
||||
participantData.setContactId(matchingContactId);
|
||||
participantData.setFullName(matchingDisplayName);
|
||||
participantData.setFirstName(matchingFirstName);
|
||||
participantData.setProfilePhotoUri(matchingPhotoUri);
|
||||
participantData.setLookupKey(matchingLookupKey);
|
||||
participantData.setContactDestination(matchingDestination);
|
||||
if (isDestinationChanged) {
|
||||
// Update the send destination to the new one entered by user in Contacts.
|
||||
participantData.setSendDestination(matchingDestination);
|
||||
}
|
||||
updated = true;
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update participant with matching contact's contactId, displayName and photoUri.
|
||||
*/
|
||||
private static void updateParticipant(final DatabaseWrapper db,
|
||||
final ParticipantData participantData) {
|
||||
final ContentValues values = new ContentValues();
|
||||
if (participantData.isSelf()) {
|
||||
// Self participants can refresh their normalized phone numbers
|
||||
values.put(ParticipantColumns.NORMALIZED_DESTINATION,
|
||||
participantData.getNormalizedDestination());
|
||||
values.put(ParticipantColumns.DISPLAY_DESTINATION,
|
||||
participantData.getDisplayDestination());
|
||||
}
|
||||
values.put(ParticipantColumns.CONTACT_ID, participantData.getContactId());
|
||||
values.put(ParticipantColumns.LOOKUP_KEY, participantData.getLookupKey());
|
||||
values.put(ParticipantColumns.FULL_NAME, participantData.getFullName());
|
||||
values.put(ParticipantColumns.FIRST_NAME, participantData.getFirstName());
|
||||
values.put(ParticipantColumns.PROFILE_PHOTO_URI, participantData.getProfilePhotoUri());
|
||||
values.put(ParticipantColumns.CONTACT_DESTINATION, participantData.getContactDestination());
|
||||
values.put(ParticipantColumns.SEND_DESTINATION, participantData.getSendDestination());
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.update(DatabaseHelper.PARTICIPANTS_TABLE, values, ParticipantColumns._ID + "=?",
|
||||
new String[] { participantData.getId() });
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of inactive self ids in the participants table.
|
||||
*/
|
||||
private static List<String> getInactiveSelfParticipantIds() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final List<String> inactiveSelf = new ArrayList<>();
|
||||
|
||||
final String selection = ParticipantColumns.SIM_SLOT_ID + "=? AND " +
|
||||
SELF_PARTICIPANTS_CLAUSE;
|
||||
try (Cursor cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE,
|
||||
new String[]{ParticipantColumns._ID},
|
||||
selection, new String[]{String.valueOf(ParticipantData.INVALID_SLOT_ID)},
|
||||
null, null, null)) {
|
||||
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
final String participantId = cursor.getString(0);
|
||||
inactiveSelf.add(participantId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return inactiveSelf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of conversations with the given self ids.
|
||||
*/
|
||||
private static List<String> getConversationsWithSelfParticipantIds(final List<String> selfIds) {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final List<String> conversationIds = new ArrayList<>();
|
||||
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
final StringBuilder selectionList = new StringBuilder();
|
||||
for (int i = 0; i < selfIds.size(); i++) {
|
||||
selectionList.append('?');
|
||||
if (i < selfIds.size() - 1) {
|
||||
selectionList.append(',');
|
||||
}
|
||||
}
|
||||
final String selection =
|
||||
ConversationColumns.CURRENT_SELF_ID + " IN (" + selectionList + ")";
|
||||
cursor = db.query(DatabaseHelper.CONVERSATIONS_TABLE,
|
||||
new String[] { ConversationColumns._ID },
|
||||
selection, selfIds.toArray(new String[0]),
|
||||
null, null, null);
|
||||
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
final String conversationId = cursor.getString(0);
|
||||
conversationIds.add(conversationId);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return conversationIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh one conversation's self id.
|
||||
*/
|
||||
private static void updateConversationSelfId(final String conversationId,
|
||||
final String selfId) {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
BugleDatabaseOperations.updateConversationSelfIdInTransaction(db, conversationId,
|
||||
selfId);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
|
||||
UIIntents.get().broadcastConversationSelfIdChange(db.getContext(), conversationId, selfId);
|
||||
}
|
||||
|
||||
/**
|
||||
* After refreshing the self participant list, find all conversations with inactive self ids,
|
||||
* and switch them back to system default.
|
||||
*/
|
||||
private static void refreshConversationSelfIds() {
|
||||
final List<String> inactiveSelfs = getInactiveSelfParticipantIds();
|
||||
if (inactiveSelfs.size() == 0) {
|
||||
return;
|
||||
}
|
||||
final List<String> conversationsToRefresh =
|
||||
getConversationsWithSelfParticipantIds(inactiveSelfs);
|
||||
if (conversationsToRefresh.size() == 0) {
|
||||
return;
|
||||
}
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final ParticipantData defaultSelf =
|
||||
BugleDatabaseOperations.getOrCreateSelf(db, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
|
||||
if (defaultSelf != null) {
|
||||
for (final String conversationId : conversationsToRefresh) {
|
||||
updateConversationSelfId(conversationId, defaultSelf.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 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) {
|
||||
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.
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.action.ActionMonitor.ActionCompletedListener;
|
||||
import com.android.messaging.datamodel.action.ActionMonitor.ActionExecutedListener;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base class for operations that perform application business logic off the main UI thread while
|
||||
* holding a wake lock.
|
||||
* .
|
||||
* Note all derived classes need to provide real implementation of Parcelable (this is abstract)
|
||||
*/
|
||||
public abstract class Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
// Members holding the parameters common to all actions - no action state
|
||||
public final String actionKey;
|
||||
|
||||
// If derived classes keep their data in actionParameters then parcelable is trivial
|
||||
protected final Bundle actionParameters;
|
||||
|
||||
// This does not get written to the parcel
|
||||
private final List<Action> mBackgroundActions = new LinkedList<>();
|
||||
|
||||
/**
|
||||
* Process the action locally - runs on action service thread.
|
||||
* TODO: Currently, there is no way for this method to indicate failure
|
||||
* @return result to be passed in to {@link ActionExecutedListener#onActionExecuted}. It is
|
||||
* also the result passed in to {@link ActionCompletedListener#onActionSucceeded} if
|
||||
* there is no background work.
|
||||
*/
|
||||
protected Object executeAction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues up background work ie. {@link #doBackgroundWork} will be called on the
|
||||
* background worker thread.
|
||||
*/
|
||||
protected void requestBackgroundWork() {
|
||||
mBackgroundActions.add(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues up background actions for background processing after the current action has
|
||||
* completed its processing ({@link #executeAction} or {@link #processBackgroundFailure})
|
||||
* on the Action thread.
|
||||
*/
|
||||
protected void requestBackgroundWork(final Action backgroundAction) {
|
||||
mBackgroundActions.add(backgroundAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return flag indicating if any actions have been queued
|
||||
*/
|
||||
public boolean hasBackgroundActions() {
|
||||
return !mBackgroundActions.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send queued actions to the background worker provided
|
||||
*/
|
||||
public void sendBackgroundActions(final BackgroundWorker worker) {
|
||||
worker.queueBackgroundWork(mBackgroundActions);
|
||||
mBackgroundActions.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do work in a long running background worker thread.
|
||||
* {@link #requestBackgroundWork} needs to be called for this method to
|
||||
* be called.
|
||||
* @return response that is to be passed to {@link #processBackgroundResponse}
|
||||
*/
|
||||
protected Bundle doBackgroundWork() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the success response from the background worker. Runs on action service thread.
|
||||
* @param response the response returned by {@link #doBackgroundWork}
|
||||
* @return result to be passed in to {@link ActionCompletedListener#onActionSucceeded}
|
||||
*/
|
||||
protected Object processBackgroundResponse(final Bundle response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in case of failures when sending background actions. Runs on action service thread
|
||||
* @return result to be passed in to {@link ActionCompletedListener#onActionFailed}
|
||||
*/
|
||||
protected Object processBackgroundFailure() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
protected Action(final String key) {
|
||||
this.actionKey = key;
|
||||
this.actionParameters = new Bundle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
protected Action() {
|
||||
this.actionKey = generateUniqueActionKey(getClass().getSimpleName());
|
||||
this.actionParameters = new Bundle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an action and monitor for processing by the ActionService via the factory helper
|
||||
*/
|
||||
protected void start(final ActionMonitor monitor) {
|
||||
ActionMonitor.registerActionMonitor(this.actionKey, monitor);
|
||||
DataModel.startActionService(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an action for processing by the ActionService via the factory helper
|
||||
*/
|
||||
public void start() {
|
||||
DataModel.startActionService(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an action for delayed processing by the ActionService via the factory helper
|
||||
*/
|
||||
public void schedule(final int requestCode, final long delayMs) {
|
||||
DataModel.scheduleAction(this, requestCode, delayMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when action queues ActionService intent
|
||||
*/
|
||||
protected final void markStart() {
|
||||
ActionMonitor.setState(this, ActionMonitor.STATE_CREATED,
|
||||
ActionMonitor.STATE_QUEUED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the beginning of local action execution
|
||||
*/
|
||||
protected final void markBeginExecute() {
|
||||
ActionMonitor.setState(this, ActionMonitor.STATE_QUEUED,
|
||||
ActionMonitor.STATE_EXECUTING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the end of local action execution - either completes the action or queues
|
||||
* background actions
|
||||
*/
|
||||
protected final void markEndExecute(final Object result) {
|
||||
final boolean hasBackgroundActions = hasBackgroundActions();
|
||||
ActionMonitor.setExecutedState(this, ActionMonitor.STATE_EXECUTING,
|
||||
hasBackgroundActions, result);
|
||||
if (!hasBackgroundActions) {
|
||||
ActionMonitor.setCompleteState(this, ActionMonitor.STATE_EXECUTING,
|
||||
result, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update action state to indicate that the background worker is starting
|
||||
*/
|
||||
protected final void markBackgroundWorkStarting() {
|
||||
ActionMonitor.setState(this,
|
||||
ActionMonitor.STATE_BACKGROUND_ACTIONS_QUEUED,
|
||||
ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update action state to indicate that the background worker has posted its response
|
||||
* (or failure) to the Action service
|
||||
*/
|
||||
protected final void markBackgroundCompletionQueued() {
|
||||
ActionMonitor.setState(this,
|
||||
ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION,
|
||||
ActionMonitor.STATE_BACKGROUND_COMPLETION_QUEUED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update action state to indicate the background action failed but is being re-queued for retry
|
||||
*/
|
||||
protected final void markBackgroundWorkQueued() {
|
||||
ActionMonitor.setState(this,
|
||||
ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION,
|
||||
ActionMonitor.STATE_BACKGROUND_ACTIONS_QUEUED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by ActionService to process a response from the background worker
|
||||
* @param response the response returned by {@link #doBackgroundWork}
|
||||
*/
|
||||
protected final void processBackgroundWorkResponse(final Bundle response) {
|
||||
ActionMonitor.setState(this,
|
||||
ActionMonitor.STATE_BACKGROUND_COMPLETION_QUEUED,
|
||||
ActionMonitor.STATE_PROCESSING_BACKGROUND_RESPONSE);
|
||||
final Object result = processBackgroundResponse(response);
|
||||
ActionMonitor.setCompleteState(this,
|
||||
ActionMonitor.STATE_PROCESSING_BACKGROUND_RESPONSE, result, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by ActionService when a background action fails
|
||||
*/
|
||||
protected final void processBackgroundWorkFailure() {
|
||||
final Object result = processBackgroundFailure();
|
||||
ActionMonitor.setCompleteState(this, ActionMonitor.STATE_UNDEFINED,
|
||||
result, false);
|
||||
}
|
||||
|
||||
private static final Object sLock = new Object();
|
||||
private static long sActionIdx = System.currentTimeMillis() * 1000;
|
||||
|
||||
/**
|
||||
* Helper method to generate a unique operation index
|
||||
*/
|
||||
protected static long getActionIdx() {
|
||||
long idx;
|
||||
synchronized (sLock) {
|
||||
idx = ++sActionIdx;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper can be used to generate a unique key used to identify an action.
|
||||
* @param baseKey - key generated to identify the action parameters
|
||||
* @return - composite key generated by appending unique index
|
||||
*/
|
||||
protected static String generateUniqueActionKey(final String baseKey) {
|
||||
final StringBuilder key = new StringBuilder();
|
||||
if (!TextUtils.isEmpty(baseKey)) {
|
||||
key.append(baseKey);
|
||||
}
|
||||
key.append(":");
|
||||
key.append(getActionIdx());
|
||||
return key.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Most derived classes use this base implementation (unless they include files handles)
|
||||
*/
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derived classes need to implement writeToParcel (but typically should call this method
|
||||
* to parcel Action member variables before they parcel their member variables).
|
||||
*/
|
||||
public void writeActionToParcel(final Parcel parcel, final int flags) {
|
||||
parcel.writeString(this.actionKey);
|
||||
parcel.writeBundle(this.actionParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for derived classes to implement parcelable
|
||||
*/
|
||||
public Action(final Parcel in) {
|
||||
this.actionKey = in.readString();
|
||||
// Note: Need to set classloader to ensure we can un-parcel classes from this package
|
||||
this.actionParameters = in.readBundle(Action.class.getClassLoader());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 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.action;
|
||||
|
||||
import android.os.Handler;
|
||||
import androidx.collection.SimpleArrayMap;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.util.Assert.RunsOnAnyThread;
|
||||
import com.android.messaging.util.Assert.RunsOnMainThread;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.ThreadUtil;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Base class for action monitors
|
||||
* Actions come in various flavors but
|
||||
* o) Fire and forget - no monitor
|
||||
* o) Immediate local processing only - will trigger ActionCompletedListener when done
|
||||
* o) Background worker processing only - will trigger ActionCompletedListener when done
|
||||
* o) Immediate local processing followed by background work followed by more local processing
|
||||
* - will trigger ActionExecutedListener once local processing complete and
|
||||
* ActionCompletedListener when second set of local process (dealing with background
|
||||
* worker response) is complete
|
||||
*/
|
||||
public class ActionMonitor {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
/**
|
||||
* Interface used to notify on completion of local execution for an action
|
||||
*/
|
||||
public interface ActionExecutedListener {
|
||||
/**
|
||||
* @param result value returned by {@link Action#executeAction}
|
||||
*/
|
||||
@RunsOnMainThread
|
||||
void onActionExecuted(ActionMonitor monitor, final Action action,
|
||||
final Object data, final Object result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface used to notify action completion
|
||||
*/
|
||||
public interface ActionCompletedListener {
|
||||
/**
|
||||
* @param result object returned from processing the action. This is the value returned by
|
||||
* {@link Action#executeAction} if there is no background work, or
|
||||
* else the value returned by
|
||||
* {@link Action#processBackgroundResponse}
|
||||
*/
|
||||
@RunsOnMainThread
|
||||
void onActionSucceeded(ActionMonitor monitor,
|
||||
final Action action, final Object data, final Object result);
|
||||
/**
|
||||
* @param result value returned by {@link Action#processBackgroundFailure}
|
||||
*/
|
||||
@RunsOnMainThread
|
||||
void onActionFailed(ActionMonitor monitor, final Action action,
|
||||
final Object data, final Object result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for being notified of action state changes - used for profiling, testing only
|
||||
*/
|
||||
protected interface ActionStateChangedListener {
|
||||
/**
|
||||
* @param action the action that is changing state
|
||||
* @param state the new state of the action
|
||||
*/
|
||||
@RunsOnAnyThread
|
||||
void onActionStateChanged(Action action, int state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Operations always start out as STATE_CREATED and finish as STATE_COMPLETE.
|
||||
* Some common state transition sequences in between include:
|
||||
* <ul>
|
||||
* <li>Local data change only : STATE_QUEUED - STATE_EXECUTING
|
||||
* <li>Background worker request only : STATE_BACKGROUND_ACTIONS_QUEUED
|
||||
* - STATE_EXECUTING_BACKGROUND_ACTION
|
||||
* - STATE_BACKGROUND_COMPLETION_QUEUED
|
||||
* - STATE_PROCESSING_BACKGROUND_RESPONSE
|
||||
* <li>Local plus background worker request : STATE_QUEUED - STATE_EXECUTING
|
||||
* - STATE_BACKGROUND_ACTIONS_QUEUED
|
||||
* - STATE_EXECUTING_BACKGROUND_ACTION
|
||||
* - STATE_BACKGROUND_COMPLETION_QUEUED
|
||||
* - STATE_PROCESSING_BACKGROUND_RESPONSE
|
||||
* </ul>
|
||||
*/
|
||||
protected static final int STATE_UNDEFINED = 0;
|
||||
protected static final int STATE_CREATED = 1; // Just created
|
||||
protected static final int STATE_QUEUED = 2; // Action queued for processing
|
||||
protected static final int STATE_EXECUTING = 3; // Action processing on datamodel thread
|
||||
protected static final int STATE_BACKGROUND_ACTIONS_QUEUED = 4;
|
||||
protected static final int STATE_EXECUTING_BACKGROUND_ACTION = 5;
|
||||
// The background work has completed, either returning a success response or resulting in a
|
||||
// failure
|
||||
protected static final int STATE_BACKGROUND_COMPLETION_QUEUED = 6;
|
||||
protected static final int STATE_PROCESSING_BACKGROUND_RESPONSE = 7;
|
||||
protected static final int STATE_COMPLETE = 8; // Action complete
|
||||
|
||||
/**
|
||||
* Lock used to protect access to state and listeners
|
||||
*/
|
||||
private final Object mLock = new Object();
|
||||
|
||||
/**
|
||||
* Current state of action
|
||||
*/
|
||||
protected int mState;
|
||||
|
||||
/**
|
||||
* Listener which is notified on action completion
|
||||
*/
|
||||
private ActionCompletedListener mCompletedListener;
|
||||
|
||||
/**
|
||||
* Listener which is notified on action executed
|
||||
*/
|
||||
private ActionExecutedListener mExecutedListener;
|
||||
|
||||
/**
|
||||
* Listener which is notified of state changes
|
||||
*/
|
||||
private ActionStateChangedListener mStateChangedListener;
|
||||
|
||||
/**
|
||||
* Handler used to post results back to caller
|
||||
*/
|
||||
private final Handler mHandler;
|
||||
|
||||
/**
|
||||
* Data passed back to listeners (associated with the action when it is created)
|
||||
*/
|
||||
private final Object mData;
|
||||
|
||||
/**
|
||||
* The action key is used to determine equivalence of operations and their requests
|
||||
*/
|
||||
private final String mActionKey;
|
||||
|
||||
/**
|
||||
* Get action key identifying associated action
|
||||
*/
|
||||
public String getActionKey() {
|
||||
return mActionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister listeners so that they will not be called back - override this method if needed
|
||||
*/
|
||||
public void unregister() {
|
||||
clearListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister listeners so that they will not be called
|
||||
*/
|
||||
protected final void clearListeners() {
|
||||
synchronized (mLock) {
|
||||
mCompletedListener = null;
|
||||
mExecutedListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a monitor associated with a particular action instance
|
||||
*/
|
||||
protected ActionMonitor(final int initialState, final String actionKey,
|
||||
final Object data) {
|
||||
mHandler = ThreadUtil.getMainThreadHandler();
|
||||
mActionKey = actionKey;
|
||||
mState = initialState;
|
||||
mData = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return flag to indicate if action is complete
|
||||
*/
|
||||
public boolean isComplete() {
|
||||
boolean complete;
|
||||
synchronized (mLock) {
|
||||
complete = (mState == STATE_COMPLETE);
|
||||
}
|
||||
return complete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set listener that will be called with action completed result
|
||||
*/
|
||||
protected final void setCompletedListener(final ActionCompletedListener listener) {
|
||||
synchronized (mLock) {
|
||||
mCompletedListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set listener that will be called with local execution result
|
||||
*/
|
||||
protected final void setExecutedListener(final ActionExecutedListener listener) {
|
||||
synchronized (mLock) {
|
||||
mExecutedListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set listener that will be called with local execution result
|
||||
*/
|
||||
protected final void setStateChangedListener(final ActionStateChangedListener listener) {
|
||||
synchronized (mLock) {
|
||||
mStateChangedListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a state update transition
|
||||
* @param action - action whose state is updating
|
||||
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
|
||||
* @param newState - new state which will be set
|
||||
*/
|
||||
protected void updateState(final Action action, final int expectedOldState,
|
||||
final int newState) {
|
||||
ActionStateChangedListener listener = null;
|
||||
synchronized (mLock) {
|
||||
if (expectedOldState != STATE_UNDEFINED &&
|
||||
mState != expectedOldState) {
|
||||
throw new IllegalStateException("On updateState to " + newState + " was " + mState
|
||||
+ " expecting " + expectedOldState);
|
||||
}
|
||||
if (newState != mState) {
|
||||
mState = newState;
|
||||
listener = mStateChangedListener;
|
||||
}
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onActionStateChanged(action, newState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a state update transition
|
||||
* @param action - action whose state is updating
|
||||
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
|
||||
* @param newState - new state which will be set
|
||||
*/
|
||||
static void setState(final Action action, final int expectedOldState,
|
||||
final int newState) {
|
||||
int oldMonitorState = expectedOldState;
|
||||
int newMonitorState = newState;
|
||||
final ActionMonitor monitor
|
||||
= ActionMonitor.lookupActionMonitor(action.actionKey);
|
||||
if (monitor != null) {
|
||||
oldMonitorState = monitor.mState;
|
||||
monitor.updateState(action, expectedOldState, newState);
|
||||
newMonitorState = monitor.mState;
|
||||
}
|
||||
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS",
|
||||
Locale.getDefault());
|
||||
df.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
LogUtil.v(TAG, "Operation-" + action.actionKey + ": @" + df.format(new Date())
|
||||
+ "UTC State = " + oldMonitorState + " - " + newMonitorState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark action complete
|
||||
* @param action - action whose state is updating
|
||||
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
|
||||
* @param result - object returned from processing the action. This is the value returned by
|
||||
* {@link Action#executeAction} if there is no background work, or
|
||||
* else the value returned by {@link Action#processBackgroundResponse}
|
||||
* or {@link Action#processBackgroundFailure}
|
||||
*/
|
||||
private void complete(final Action action, final int expectedOldState, final Object result,
|
||||
final boolean succeeded) {
|
||||
ActionCompletedListener completedListener;
|
||||
synchronized (mLock) {
|
||||
setState(action, expectedOldState, STATE_COMPLETE);
|
||||
completedListener = mCompletedListener;
|
||||
mExecutedListener = null;
|
||||
mStateChangedListener = null;
|
||||
}
|
||||
if (completedListener != null) {
|
||||
// Marshal to UI thread
|
||||
mHandler.post(() -> {
|
||||
ActionCompletedListener listener = null;
|
||||
synchronized (mLock) {
|
||||
if (mCompletedListener != null) {
|
||||
listener = mCompletedListener;
|
||||
}
|
||||
mCompletedListener = null;
|
||||
}
|
||||
if (listener != null) {
|
||||
if (succeeded) {
|
||||
listener.onActionSucceeded(ActionMonitor.this,
|
||||
action, mData, result);
|
||||
} else {
|
||||
listener.onActionFailed(ActionMonitor.this,
|
||||
action, mData, result);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark action complete
|
||||
* @param action - action whose state is updating
|
||||
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
|
||||
* @param result - object returned from processing the action. This is the value returned by
|
||||
* {@link Action#executeAction} if there is no background work, or
|
||||
* else the value returned by {@link Action#processBackgroundResponse}
|
||||
* or {@link Action#processBackgroundFailure}
|
||||
*/
|
||||
static void setCompleteState(final Action action, final int expectedOldState,
|
||||
final Object result, final boolean succeeded) {
|
||||
int oldMonitorState = expectedOldState;
|
||||
final ActionMonitor monitor
|
||||
= ActionMonitor.lookupActionMonitor(action.actionKey);
|
||||
if (monitor != null) {
|
||||
oldMonitorState = monitor.mState;
|
||||
monitor.complete(action, expectedOldState, result, succeeded);
|
||||
unregisterActionMonitorIfComplete(action.actionKey, monitor);
|
||||
}
|
||||
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS",
|
||||
Locale.getDefault());
|
||||
df.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
LogUtil.v(TAG, "Operation-" + action.actionKey + ": @" + df.format(new Date())
|
||||
+ "UTC State = " + oldMonitorState + " - " + STATE_COMPLETE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark action complete
|
||||
* @param action - action whose state is updating
|
||||
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
|
||||
* @param hasBackgroundActions - has the completing action requested background work
|
||||
* @param result - the return value of {@link Action#executeAction}
|
||||
*/
|
||||
final void executed(final Action action,
|
||||
final int expectedOldState, final boolean hasBackgroundActions, final Object result) {
|
||||
ActionExecutedListener executedListener;
|
||||
synchronized (mLock) {
|
||||
if (hasBackgroundActions) {
|
||||
setState(action, expectedOldState, STATE_BACKGROUND_ACTIONS_QUEUED);
|
||||
}
|
||||
executedListener = mExecutedListener;
|
||||
}
|
||||
if (executedListener != null) {
|
||||
// Marshal to UI thread
|
||||
mHandler.post(() -> {
|
||||
ActionExecutedListener listener = null;
|
||||
synchronized (mLock) {
|
||||
if (mExecutedListener != null) {
|
||||
listener = mExecutedListener;
|
||||
mExecutedListener = null;
|
||||
}
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onActionExecuted(ActionMonitor.this,
|
||||
action, mData, result);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark action complete
|
||||
* @param action - action whose state is updating
|
||||
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
|
||||
* @param hasBackgroundActions - has the completing action requested background work
|
||||
* @param result - the return value of {@link Action#executeAction}
|
||||
*/
|
||||
static void setExecutedState(final Action action,
|
||||
final int expectedOldState, final boolean hasBackgroundActions, final Object result) {
|
||||
int oldMonitorState = expectedOldState;
|
||||
final ActionMonitor monitor
|
||||
= ActionMonitor.lookupActionMonitor(action.actionKey);
|
||||
if (monitor != null) {
|
||||
oldMonitorState = monitor.mState;
|
||||
monitor.executed(action, expectedOldState, hasBackgroundActions, result);
|
||||
}
|
||||
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS",
|
||||
Locale.getDefault());
|
||||
df.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
LogUtil.v(TAG, "Operation-" + action.actionKey + ": @" + df.format(new Date())
|
||||
+ "UTC State = " + oldMonitorState + " - EXECUTED");
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of action monitors indexed by actionKey
|
||||
*/
|
||||
static final SimpleArrayMap<String, ActionMonitor> sActionMonitors = new SimpleArrayMap<>();
|
||||
|
||||
/**
|
||||
* Insert new monitor into map
|
||||
*/
|
||||
static void registerActionMonitor(final String actionKey,
|
||||
final ActionMonitor monitor) {
|
||||
if (monitor != null
|
||||
&& (TextUtils.isEmpty(monitor.getActionKey())
|
||||
|| TextUtils.isEmpty(actionKey)
|
||||
|| !actionKey.equals(monitor.getActionKey()))) {
|
||||
throw new IllegalArgumentException("Monitor key " + monitor.getActionKey()
|
||||
+ " not compatible with action key " + actionKey);
|
||||
}
|
||||
synchronized (sActionMonitors) {
|
||||
sActionMonitors.put(actionKey, monitor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find monitor associated with particular action
|
||||
*/
|
||||
private static ActionMonitor lookupActionMonitor(final String actionKey) {
|
||||
ActionMonitor monitor;
|
||||
synchronized (sActionMonitors) {
|
||||
monitor = sActionMonitors.get(actionKey);
|
||||
}
|
||||
return monitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove monitor from map if the action is complete
|
||||
*/
|
||||
static void unregisterActionMonitorIfComplete(final String actionKey,
|
||||
final ActionMonitor monitor) {
|
||||
if (monitor != null && monitor.isComplete()) {
|
||||
synchronized (sActionMonitors) {
|
||||
sActionMonitors.remove(actionKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
* Class providing interface for the ActionService - can be stubbed for testing
|
||||
*/
|
||||
public class ActionService {
|
||||
protected static PendingIntent makeStartActionPendingIntent(final Context context,
|
||||
final Action action, final int requestCode, final boolean launchesAnActivity) {
|
||||
return ActionServiceImpl.makeStartActionPendingIntent(context, action, requestCode,
|
||||
launchesAnActivity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an action by posting it over the the ActionService
|
||||
*/
|
||||
public void startAction(final Action action) {
|
||||
ActionServiceImpl.startAction(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a delayed action by posting it over the the ActionService
|
||||
*/
|
||||
public void scheduleAction(final Action action, final int code,
|
||||
final long delayMs) {
|
||||
ActionServiceImpl.scheduleAction(action, code, delayMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a response from the BackgroundWorker in the ActionService
|
||||
*/
|
||||
protected void handleResponseFromBackgroundWorker(
|
||||
final Action action, final Bundle response) {
|
||||
ActionServiceImpl.handleResponseFromBackgroundWorker(action, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a failure from the BackgroundWorker in the ActionService
|
||||
*/
|
||||
protected void handleFailureFromBackgroundWorker(final Action action,
|
||||
final Exception exception) {
|
||||
ActionServiceImpl.handleFailureFromBackgroundWorker(action, exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 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.action;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.JobIntentService;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.LoggingTimer;
|
||||
|
||||
/**
|
||||
* ActionService used to perform background processing for data model
|
||||
*/
|
||||
public class ActionServiceImpl extends JobIntentService {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
/**
|
||||
* Unique job ID for this service.
|
||||
*/
|
||||
public static final int JOB_ID = 1000;
|
||||
|
||||
public ActionServiceImpl() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start action by sending intent to the service
|
||||
* @param action - action to start
|
||||
*/
|
||||
protected static void startAction(final Action action) {
|
||||
final Intent intent = makeIntent(OP_START_ACTION);
|
||||
final Bundle actionBundle = new Bundle();
|
||||
actionBundle.putParcelable(BUNDLE_ACTION, action);
|
||||
intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
|
||||
action.markStart();
|
||||
startServiceWithIntent(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an action to run after specified delay using alarm manager to send pendingintent
|
||||
* @param action - action to start
|
||||
* @param requestCode - request code used to collapse requests
|
||||
* @param delayMs - delay in ms (from now) before action will start
|
||||
*/
|
||||
protected static void scheduleAction(final Action action, final int requestCode,
|
||||
final long delayMs) {
|
||||
final Intent intent = PendingActionReceiver.makeIntent(OP_START_ACTION);
|
||||
final Bundle actionBundle = new Bundle();
|
||||
actionBundle.putParcelable(BUNDLE_ACTION, action);
|
||||
intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
|
||||
|
||||
PendingActionReceiver.scheduleAlarm(intent, requestCode, delayMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle response returned by BackgroundWorker
|
||||
* @param response - response from service
|
||||
*/
|
||||
protected static void handleResponseFromBackgroundWorker(final Action action,
|
||||
final Bundle response) {
|
||||
final Intent intent = makeIntent(OP_RECEIVE_BACKGROUND_RESPONSE);
|
||||
|
||||
final Bundle actionBundle = new Bundle();
|
||||
actionBundle.putParcelable(BUNDLE_ACTION, action);
|
||||
intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
|
||||
intent.putExtra(EXTRA_WORKER_RESPONSE, response);
|
||||
|
||||
startServiceWithIntent(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle response returned by BackgroundWorker
|
||||
*/
|
||||
protected static void handleFailureFromBackgroundWorker(final Action action,
|
||||
final Exception exception) {
|
||||
final Intent intent = makeIntent(OP_RECEIVE_BACKGROUND_FAILURE);
|
||||
|
||||
final Bundle actionBundle = new Bundle();
|
||||
actionBundle.putParcelable(BUNDLE_ACTION, action);
|
||||
intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
|
||||
intent.putExtra(EXTRA_WORKER_EXCEPTION, exception);
|
||||
|
||||
startServiceWithIntent(intent);
|
||||
}
|
||||
|
||||
// ops
|
||||
protected static final int OP_START_ACTION = 200;
|
||||
protected static final int OP_RECEIVE_BACKGROUND_RESPONSE = 201;
|
||||
protected static final int OP_RECEIVE_BACKGROUND_FAILURE = 202;
|
||||
|
||||
// extras
|
||||
protected static final String EXTRA_OP_CODE = "op";
|
||||
protected static final String EXTRA_ACTION_BUNDLE = "datamodel_action_bundle";
|
||||
protected static final String EXTRA_WORKER_EXCEPTION = "worker_exception";
|
||||
protected static final String EXTRA_WORKER_RESPONSE = "worker_response";
|
||||
protected static final String EXTRA_WORKER_UPDATE = "worker_update";
|
||||
protected static final String BUNDLE_ACTION = "bundle_action";
|
||||
|
||||
private BackgroundWorker mBackgroundWorker;
|
||||
|
||||
/**
|
||||
* Allocate an intent with a specific opcode.
|
||||
*/
|
||||
private static Intent makeIntent(final int opcode) {
|
||||
final Intent intent = new Intent(Factory.get().getApplicationContext(),
|
||||
ActionServiceImpl.class);
|
||||
intent.putExtra(EXTRA_OP_CODE, opcode);
|
||||
return intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast receiver for alarms scheduled through ActionService.
|
||||
*/
|
||||
public static class PendingActionReceiver extends BroadcastReceiver {
|
||||
static final String ACTION = "com.android.messaging.datamodel.PENDING_ACTION";
|
||||
|
||||
/**
|
||||
* Allocate an intent with a specific opcode and alarm action.
|
||||
*/
|
||||
public static Intent makeIntent(final int opcode) {
|
||||
final Intent intent = new Intent(Factory.get().getApplicationContext(),
|
||||
PendingActionReceiver.class);
|
||||
intent.setAction(ACTION);
|
||||
intent.putExtra(EXTRA_OP_CODE, opcode);
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static void scheduleAlarm(final Intent intent, final int requestCode,
|
||||
final long delayMs) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final PendingIntent pendingIntent = PendingIntent.getBroadcast(
|
||||
context, requestCode, intent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
final AlarmManager mgr =
|
||||
(AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
|
||||
if (delayMs < Long.MAX_VALUE) {
|
||||
mgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
||||
SystemClock.elapsedRealtime() + delayMs, pendingIntent);
|
||||
} else {
|
||||
mgr.cancel(pendingIntent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
ActionServiceImpl.startServiceWithIntent(intent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pending intent that will trigger a data model action when the intent is
|
||||
* triggered
|
||||
*/
|
||||
public static PendingIntent makeStartActionPendingIntent(final Context context,
|
||||
final Action action, final int requestCode, final boolean launchesAnActivity) {
|
||||
final Intent intent = PendingActionReceiver.makeIntent(OP_START_ACTION);
|
||||
final Bundle actionBundle = new Bundle();
|
||||
actionBundle.putParcelable(BUNDLE_ACTION, action);
|
||||
intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
|
||||
if (launchesAnActivity) {
|
||||
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, requestCode, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mBackgroundWorker = DataModel.get().getBackgroundWorkerForActionService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue intent to the ActionService.
|
||||
*/
|
||||
private static void startServiceWithIntent(final Intent intent) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
intent.setClass(context, ActionServiceImpl.class);
|
||||
enqueueWork(context, intent);
|
||||
}
|
||||
|
||||
public static void enqueueWork(Context context, Intent work) {
|
||||
enqueueWork(context, ActionServiceImpl.class, JOB_ID, work);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onHandleWork(@NonNull final Intent intent) {
|
||||
final int opcode = intent.getIntExtra(EXTRA_OP_CODE, 0);
|
||||
|
||||
Action action;
|
||||
final Bundle actionBundle = intent.getBundleExtra(EXTRA_ACTION_BUNDLE);
|
||||
actionBundle.setClassLoader(getClassLoader());
|
||||
switch(opcode) {
|
||||
case OP_START_ACTION: {
|
||||
action = actionBundle.getParcelable(BUNDLE_ACTION, Action.class);
|
||||
executeAction(action);
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_RECEIVE_BACKGROUND_RESPONSE: {
|
||||
action = actionBundle.getParcelable(BUNDLE_ACTION, Action.class);
|
||||
final Bundle response = intent.getBundleExtra(EXTRA_WORKER_RESPONSE);
|
||||
processBackgroundResponse(action, response);
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_RECEIVE_BACKGROUND_FAILURE: {
|
||||
action = actionBundle.getParcelable(BUNDLE_ACTION, Action.class);
|
||||
processBackgroundFailure(action);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Unrecognized opcode in ActionServiceImpl");
|
||||
}
|
||||
|
||||
action.sendBackgroundActions(mBackgroundWorker);
|
||||
}
|
||||
|
||||
private static final long EXECUTION_TIME_WARN_LIMIT_MS = 1000; // 1 second
|
||||
/**
|
||||
* Local execution of action on ActionService thread
|
||||
*/
|
||||
private void executeAction(final Action action) {
|
||||
action.markBeginExecute();
|
||||
|
||||
final LoggingTimer timer = createLoggingTimer(action, "#executeAction");
|
||||
timer.start();
|
||||
|
||||
final Object result = action.executeAction();
|
||||
|
||||
timer.stopAndLog();
|
||||
|
||||
action.markEndExecute(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process response on ActionService thread
|
||||
*/
|
||||
private void processBackgroundResponse(final Action action, final Bundle response) {
|
||||
final LoggingTimer timer = createLoggingTimer(action, "#processBackgroundResponse");
|
||||
timer.start();
|
||||
|
||||
action.processBackgroundWorkResponse(response);
|
||||
|
||||
timer.stopAndLog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process failure on ActionService thread
|
||||
*/
|
||||
private void processBackgroundFailure(final Action action) {
|
||||
final LoggingTimer timer = createLoggingTimer(action, "#processBackgroundFailure");
|
||||
timer.start();
|
||||
|
||||
action.processBackgroundWorkFailure();
|
||||
|
||||
timer.stopAndLog();
|
||||
}
|
||||
|
||||
private static LoggingTimer createLoggingTimer(
|
||||
final Action action, final String methodName) {
|
||||
return new LoggingTimer(TAG, action.getClass().getSimpleName() + methodName,
|
||||
EXECUTION_TIME_WARN_LIMIT_MS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface between action service and its workers
|
||||
*/
|
||||
public class BackgroundWorker {
|
||||
|
||||
/**
|
||||
* Send list of requests from action service to a worker
|
||||
*/
|
||||
public void queueBackgroundWork(final List<Action> backgroundActions) {
|
||||
BackgroundWorkerService.queueBackgroundWork(backgroundActions);
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 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.action;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.JobIntentService;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.LoggingTimer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Background worker service is an initial example of a background work queue handler
|
||||
* Used to actually "send" messages which may take some time and should not block ActionService
|
||||
* or UI
|
||||
*/
|
||||
public class BackgroundWorkerService extends JobIntentService {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
/**
|
||||
* Unique job ID for this service.
|
||||
*/
|
||||
public static final int JOB_ID = 1001;
|
||||
|
||||
private final ActionService mHost;
|
||||
|
||||
public BackgroundWorkerService() {
|
||||
super();
|
||||
mHost = DataModel.get().getActionService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a list of requests from action service to this worker
|
||||
*/
|
||||
public static void queueBackgroundWork(final List<Action> actions) {
|
||||
for (final Action action : actions) {
|
||||
startServiceWithAction(action, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ops
|
||||
protected static final int OP_PROCESS_REQUEST = 400;
|
||||
|
||||
// extras
|
||||
protected static final String EXTRA_OP_CODE = "op";
|
||||
protected static final String EXTRA_ACTION = "action";
|
||||
protected static final String EXTRA_ATTEMPT = "retry_attempt";
|
||||
|
||||
/**
|
||||
* Queue action intent to the BackgroundWorkerService.
|
||||
*/
|
||||
private static void startServiceWithAction(final Action action,
|
||||
final int retryCount) {
|
||||
final Intent intent = new Intent();
|
||||
intent.putExtra(EXTRA_ACTION, action);
|
||||
intent.putExtra(EXTRA_ATTEMPT, retryCount);
|
||||
startServiceWithIntent(OP_PROCESS_REQUEST, intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue intent to the BackgroundWorkerService.
|
||||
*/
|
||||
private static void startServiceWithIntent(final int opcode, final Intent intent) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
|
||||
intent.setClass(context, BackgroundWorkerService.class);
|
||||
intent.putExtra(EXTRA_OP_CODE, opcode);
|
||||
|
||||
enqueueWork(context, intent);
|
||||
}
|
||||
|
||||
public static void enqueueWork(Context context, Intent work) {
|
||||
enqueueWork(context, BackgroundWorkerService.class, JOB_ID, work);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleWork(@NonNull final Intent intent) {
|
||||
final int opcode = intent.getIntExtra(EXTRA_OP_CODE, 0);
|
||||
|
||||
if (opcode == OP_PROCESS_REQUEST) {
|
||||
final Action action = intent.getParcelableExtra(EXTRA_ACTION);
|
||||
final int attempt = intent.getIntExtra(EXTRA_ATTEMPT, -1);
|
||||
doBackgroundWork(action, attempt);
|
||||
} else {
|
||||
LogUtil.w(TAG, "Unrecognized opcode in BackgroundWorkerService " + opcode);
|
||||
throw new RuntimeException("Unrecognized opcode in BackgroundWorkerService");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Local execution of background work for action on ActionService thread
|
||||
*/
|
||||
private void doBackgroundWork(final Action action, final int attempt) {
|
||||
action.markBackgroundWorkStarting();
|
||||
Bundle response;
|
||||
try {
|
||||
final LoggingTimer timer = new LoggingTimer(
|
||||
TAG, action.getClass().getSimpleName() + "#doBackgroundWork");
|
||||
timer.start();
|
||||
|
||||
response = action.doBackgroundWork();
|
||||
|
||||
timer.stopAndLog();
|
||||
action.markBackgroundCompletionQueued();
|
||||
mHost.handleResponseFromBackgroundWorker(action, response);
|
||||
} catch (final Exception exception) {
|
||||
final boolean retry = false;
|
||||
LogUtil.e(TAG, "Error in background worker", exception);
|
||||
Assert.fail("Unexpected error in background worker - abort");
|
||||
action.markBackgroundCompletionQueued();
|
||||
mHost.handleFailureFromBackgroundWorker(action, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.AccessibilityUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
import com.android.messaging.util.ThreadUtil;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Shows one-time, transient notifications in response to action failures (i.e. permanent failures
|
||||
* when sending a message) by showing toasts.
|
||||
*/
|
||||
public class BugleActionToasts {
|
||||
/**
|
||||
* Called when SendMessageAction or DownloadMmsAction finishes
|
||||
* @param conversationId the conversation of the sent or downloaded message
|
||||
* @param success did the action succeed
|
||||
* @param status the message sending status
|
||||
* @param isSms whether the message is sent using SMS
|
||||
* @param subId the subId of the SIM related to this send
|
||||
* @param isSend whether it is a send (false for download)
|
||||
*/
|
||||
static void onSendMessageOrManualDownloadActionCompleted(
|
||||
final String conversationId,
|
||||
final boolean success,
|
||||
final int status,
|
||||
final boolean isSms,
|
||||
final int subId,
|
||||
final boolean isSend) {
|
||||
// We only show notifications for two cases, i.e. when mobile data is off or when we are
|
||||
// in airplane mode, both of which fail fast with permanent failures.
|
||||
if (!success && status == MmsUtils.MMS_REQUEST_MANUAL_RETRY) {
|
||||
final PhoneUtils phoneUtils = PhoneUtils.get(subId);
|
||||
if (phoneUtils.isAirplaneModeOn()) {
|
||||
if (isSend) {
|
||||
showToast(R.string.send_message_failure_airplane_mode);
|
||||
} else {
|
||||
showToast(R.string.download_message_failure_airplane_mode);
|
||||
}
|
||||
return;
|
||||
} else if (!isSms && !phoneUtils.isMobileDataEnabled()) {
|
||||
if (isSend) {
|
||||
showToast(R.string.send_message_failure_no_data);
|
||||
} else {
|
||||
showToast(R.string.download_message_failure_no_data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (AccessibilityUtil.isTouchExplorationEnabled(Factory.get().getApplicationContext())) {
|
||||
final boolean isFocusedConversation = DataModel.get().isFocusedConversation(conversationId);
|
||||
if (isFocusedConversation && success) {
|
||||
// Using View.announceForAccessibility may be preferable, but we do not have a
|
||||
// View, and so we use a toast instead.
|
||||
showToast(isSend ? R.string.send_message_success
|
||||
: R.string.download_message_success);
|
||||
return;
|
||||
}
|
||||
|
||||
// {@link MessageNotificationState#checkFailedMessages} does not post a notification for
|
||||
// failures in observable conversations. For accessibility, we provide an indication
|
||||
// here.
|
||||
final boolean isObservableConversation = DataModel.get().isNewMessageObservable(
|
||||
conversationId);
|
||||
if (isObservableConversation && !success) {
|
||||
showToast(isSend ? R.string.send_message_failure
|
||||
: R.string.download_message_failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void onMessageReceived(final String conversationId,
|
||||
@Nullable final ParticipantData sender, @Nullable final MessageData message) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
if (AccessibilityUtil.isTouchExplorationEnabled(context)) {
|
||||
final boolean isFocusedConversation = DataModel.get().isFocusedConversation(
|
||||
conversationId);
|
||||
if (isFocusedConversation) {
|
||||
final Resources res = context.getResources();
|
||||
final String senderDisplayName = (sender == null)
|
||||
? res.getString(R.string.unknown_sender) : sender.getDisplayName(false);
|
||||
final String announcement = res.getString(
|
||||
R.string.incoming_message_announcement, senderDisplayName,
|
||||
(message == null) ? "" : message.getMessageText());
|
||||
showToast(announcement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void showToast(final int messageResId) {
|
||||
ThreadUtil.getMainThreadHandler().post(() -> Toast.makeText(getApplicationContext(),
|
||||
getApplicationContext().getString(messageResId), Toast.LENGTH_LONG).show());
|
||||
}
|
||||
|
||||
private static void showToast(final String message) {
|
||||
ThreadUtil.getMainThreadHandler().post(() ->
|
||||
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show());
|
||||
}
|
||||
|
||||
private static Context getApplicationContext() {
|
||||
return Factory.get().getApplicationContext();
|
||||
}
|
||||
|
||||
private static class UpdateDestinationBlockedActionToast
|
||||
implements UpdateDestinationBlockedAction.UpdateDestinationBlockedActionListener {
|
||||
private final Context mContext;
|
||||
|
||||
UpdateDestinationBlockedActionToast(final Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdateDestinationBlockedAction(
|
||||
final UpdateDestinationBlockedAction action,
|
||||
final boolean success,
|
||||
final boolean block,
|
||||
final String destination) {
|
||||
if (success) {
|
||||
Toast.makeText(mContext,
|
||||
block
|
||||
? R.string.update_destination_blocked
|
||||
: R.string.update_destination_unblocked,
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static UpdateDestinationBlockedAction.UpdateDestinationBlockedActionListener
|
||||
makeUpdateDestinationBlockedActionListener(final Context context) {
|
||||
return new UpdateDestinationBlockedActionToast(context);
|
||||
}
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.NotificationsUtil;
|
||||
import com.android.messaging.widget.WidgetConversationProvider;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Action used to delete a conversation.
|
||||
*/
|
||||
public class DeleteConversationAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
public static void deleteConversation(final String conversationId, final long cutoffTimestamp) {
|
||||
final DeleteConversationAction action = new DeleteConversationAction(conversationId,
|
||||
cutoffTimestamp);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
private static final String KEY_CUTOFF_TIMESTAMP = "cutoff_timestamp";
|
||||
|
||||
private DeleteConversationAction(final String conversationId, final long cutoffTimestamp) {
|
||||
super();
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
// TODO: Should we set cuttoff timestamp to prevent us deleting new messages?
|
||||
actionParameters.putLong(KEY_CUTOFF_TIMESTAMP, cutoffTimestamp);
|
||||
}
|
||||
|
||||
// Delete conversation from both the local DB and telephony in the background so sync cannot
|
||||
// run concurrently and incorrectly try to recreate the conversation's messages locally. The
|
||||
// telephony database can sometimes be quite slow to delete conversations, so we delete from
|
||||
// the local DB first, notify the UI, and then delete from telephony.
|
||||
@Override
|
||||
protected Bundle doBackgroundWork() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final long cutoffTimestamp = actionParameters.getLong(KEY_CUTOFF_TIMESTAMP);
|
||||
|
||||
if (!TextUtils.isEmpty(conversationId)) {
|
||||
// First find the thread id for this conversation.
|
||||
final long threadId = BugleDatabaseOperations.getThreadId(db, conversationId);
|
||||
|
||||
if (BugleDatabaseOperations.deleteConversation(db, conversationId, cutoffTimestamp)) {
|
||||
LogUtil.i(TAG, "DeleteConversationAction: Deleted local conversation "
|
||||
+ conversationId);
|
||||
|
||||
// Remove notifications if necessary
|
||||
BugleNotifications.update(true /* silent */, null /* conversationId */,
|
||||
BugleNotifications.UPDATE_MESSAGES);
|
||||
|
||||
// We have changed the conversation list
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
|
||||
// Notify the widget the conversation is deleted so it can go into its configure state.
|
||||
WidgetConversationProvider.notifyConversationDeleted(
|
||||
Factory.get().getApplicationContext(),
|
||||
conversationId);
|
||||
} else {
|
||||
LogUtil.w(TAG, "DeleteConversationAction: Could not delete local conversation "
|
||||
+ conversationId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now delete from telephony DB. MmsSmsProvider throws an exception if the thread id is
|
||||
// less than 0. If it's greater than zero, it will delete all messages with that thread
|
||||
// id, even if there's no corresponding row in the threads table.
|
||||
if (threadId >= 0) {
|
||||
final int count = MmsUtils.deleteThread(threadId, cutoffTimestamp);
|
||||
if (count > 0) {
|
||||
LogUtil.i(TAG, "DeleteConversationAction: Deleted telephony thread "
|
||||
+ threadId + " (cutoffTimestamp = " + cutoffTimestamp + ")");
|
||||
} else {
|
||||
LogUtil.w(TAG, "DeleteConversationAction: Could not delete thread from "
|
||||
+ "telephony: conversationId = " + conversationId + ", thread id = "
|
||||
+ threadId);
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(TAG, "DeleteConversationAction: Local conversation " + conversationId
|
||||
+ " has an invalid telephony thread id; will delete messages individually");
|
||||
deleteConversationMessagesFromTelephony();
|
||||
}
|
||||
|
||||
// Finally, remove the conversation specific notification channel
|
||||
NotificationsUtil.deleteNotificationChannel(Factory.get().getApplicationContext(),
|
||||
conversationId);
|
||||
} else {
|
||||
LogUtil.e(TAG, "DeleteConversationAction: conversationId is empty");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all the telephony messages for the local conversation being deleted.
|
||||
* <p>
|
||||
* This is a fallback used when the conversation is not associated with any telephony thread,
|
||||
* or its thread id is invalid (e.g. negative). This is not common, but can happen sometimes
|
||||
* (e.g. the Unknown Sender conversation). In the usual case of deleting a conversation, we
|
||||
* don't need this because the telephony provider automatically deletes messages when a thread
|
||||
* is deleted.
|
||||
*/
|
||||
private void deleteConversationMessagesFromTelephony() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
Assert.notNull(conversationId);
|
||||
|
||||
final List<Uri> messageUris = new ArrayList<>();
|
||||
try (Cursor cursor = db.query(DatabaseHelper.MESSAGES_TABLE,
|
||||
new String[]{MessageColumns.SMS_MESSAGE_URI},
|
||||
MessageColumns.CONVERSATION_ID + "=?",
|
||||
new String[]{conversationId},
|
||||
null, null, null)) {
|
||||
while (cursor.moveToNext()) {
|
||||
String messageUri = cursor.getString(0);
|
||||
try {
|
||||
messageUris.add(Uri.parse(messageUri));
|
||||
} catch (Exception e) {
|
||||
LogUtil.e(TAG, "DeleteConversationAction: Could not parse message uri "
|
||||
+ messageUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Uri messageUri : messageUris) {
|
||||
int count = MmsUtils.deleteMessage(messageUri);
|
||||
if (count > 0) {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "DeleteConversationAction: Deleted telephony message "
|
||||
+ messageUri);
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(TAG, "DeleteConversationAction: Could not delete telephony message "
|
||||
+ messageUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
requestBackgroundWork();
|
||||
return null;
|
||||
}
|
||||
|
||||
private DeleteConversationAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<DeleteConversationAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public DeleteConversationAction createFromParcel(final Parcel in) {
|
||||
return new DeleteConversationAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeleteConversationAction[] newArray(final int size) {
|
||||
return new DeleteConversationAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Action used to delete a single message.
|
||||
*/
|
||||
public class DeleteMessageAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
public static void deleteMessage(final String messageId) {
|
||||
final DeleteMessageAction action = new DeleteMessageAction(messageId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
|
||||
private DeleteMessageAction(final String messageId) {
|
||||
super();
|
||||
actionParameters.putString(KEY_MESSAGE_ID, messageId);
|
||||
}
|
||||
|
||||
// Doing this work in the background so that we're not competing with sync
|
||||
// which could bring the deleted message back to life between the time we deleted
|
||||
// it locally and deleted it in telephony (sync is also done on doBackgroundWork).
|
||||
//
|
||||
// Previously this block of code deleted from telephony first but that can be very
|
||||
// slow (on the order of seconds) so this was modified to first delete locally, trigger
|
||||
// the UI update, then delete from telephony.
|
||||
@Override
|
||||
protected Bundle doBackgroundWork() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// First find the thread id for this conversation.
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
|
||||
if (!TextUtils.isEmpty(messageId)) {
|
||||
// Check message still exists
|
||||
final MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
|
||||
if (message != null) {
|
||||
// Delete from local DB
|
||||
int count = BugleDatabaseOperations.deleteMessage(db, messageId);
|
||||
if (count > 0) {
|
||||
LogUtil.i(TAG, "DeleteMessageAction: Deleted local message "
|
||||
+ messageId);
|
||||
} else {
|
||||
LogUtil.w(TAG, "DeleteMessageAction: Could not delete local message "
|
||||
+ messageId);
|
||||
}
|
||||
MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
|
||||
// We may have changed the conversation list
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
|
||||
final Uri messageUri = message.getSmsMessageUri();
|
||||
if (messageUri != null) {
|
||||
// Delete from telephony DB
|
||||
count = MmsUtils.deleteMessage(messageUri);
|
||||
if (count > 0) {
|
||||
LogUtil.i(TAG, "DeleteMessageAction: Deleted telephony message "
|
||||
+ messageUri);
|
||||
} else {
|
||||
LogUtil.w(TAG, "DeleteMessageAction: Could not delete message from "
|
||||
+ "telephony: messageId = " + messageId + ", telephony uri = "
|
||||
+ messageUri);
|
||||
}
|
||||
} else {
|
||||
LogUtil.i(TAG, "DeleteMessageAction: Local message " + messageId
|
||||
+ " has no telephony uri.");
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(TAG, "DeleteMessageAction: Message " + messageId + " no longer exists");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the message.
|
||||
*/
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
requestBackgroundWork();
|
||||
return null;
|
||||
}
|
||||
|
||||
private DeleteMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<DeleteMessageAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public DeleteMessageAction createFromParcel(final Parcel in) {
|
||||
return new DeleteMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeleteMessageAction[] newArray(final int size) {
|
||||
return new DeleteMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.RunsOnMainThread;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Downloads an MMS message.
|
||||
* <p>
|
||||
* This class is public (not package-private) because the SMS/MMS (e.g. MmsUtils) classes need to
|
||||
* access the EXTRA_* fields for setting up the 'downloaded' pending intent.
|
||||
*/
|
||||
public class DownloadMmsAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
/**
|
||||
* Interface for DownloadMmsAction listeners
|
||||
*/
|
||||
public interface DownloadMmsActionListener {
|
||||
@RunsOnMainThread
|
||||
void onDownloadMessageStarting(final ActionMonitor monitor,
|
||||
final Object data, final MessageData message);
|
||||
@RunsOnMainThread
|
||||
void onDownloadMessageSucceeded(final ActionMonitor monitor,
|
||||
final Object data, final MessageData message);
|
||||
@RunsOnMainThread
|
||||
void onDownloadMessageFailed(final ActionMonitor monitor,
|
||||
final Object data, final MessageData message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue download of an mms notification message (can only be called during execute of action)
|
||||
*/
|
||||
static boolean queueMmsForDownloadInBackground(final String messageId,
|
||||
final Action processingAction) {
|
||||
// When this method is being called, it is always from auto download
|
||||
final DownloadMmsAction action = new DownloadMmsAction();
|
||||
// This could queue nothing
|
||||
return action.queueAction(messageId, processingAction);
|
||||
}
|
||||
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
private static final String KEY_PARTICIPANT_ID = "participant_id";
|
||||
private static final String KEY_CONTENT_LOCATION = "content_location";
|
||||
private static final String KEY_TRANSACTION_ID = "transaction_id";
|
||||
private static final String KEY_NOTIFICATION_URI = "notification_uri";
|
||||
private static final String KEY_SUB_ID = "sub_id";
|
||||
private static final String KEY_SUB_PHONE_NUMBER = "sub_phone_number";
|
||||
private static final String KEY_AUTO_DOWNLOAD = "auto_download";
|
||||
private static final String KEY_FAILURE_STATUS = "failure_status";
|
||||
private static final String KEY_EXPIRY = "expiry";
|
||||
|
||||
// Values we attach to the pending intent that's fired when the message is downloaded.
|
||||
// Only applicable when downloading via the platform APIs on L+.
|
||||
public static final String EXTRA_MESSAGE_ID = "message_id";
|
||||
public static final String EXTRA_CONTENT_URI = "content_uri";
|
||||
public static final String EXTRA_NOTIFICATION_URI = "notification_uri";
|
||||
public static final String EXTRA_SUB_ID = "sub_id";
|
||||
public static final String EXTRA_SUB_PHONE_NUMBER = "sub_phone_number";
|
||||
public static final String EXTRA_TRANSACTION_ID = "transaction_id";
|
||||
public static final String EXTRA_CONTENT_LOCATION = "content_location";
|
||||
public static final String EXTRA_AUTO_DOWNLOAD = "auto_download";
|
||||
public static final String EXTRA_RECEIVED_TIMESTAMP = "received_timestamp";
|
||||
public static final String EXTRA_CONVERSATION_ID = "conversation_id";
|
||||
public static final String EXTRA_PARTICIPANT_ID = "participant_id";
|
||||
public static final String EXTRA_STATUS_IF_FAILED = "status_if_failed";
|
||||
public static final String EXTRA_EXPIRY = "expiry";
|
||||
|
||||
private DownloadMmsAction() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
Assert.fail("DownloadMmsAction must be queued rather than started");
|
||||
return null;
|
||||
}
|
||||
|
||||
protected boolean queueAction(final String messageId, final Action processingAction) {
|
||||
actionParameters.putString(KEY_MESSAGE_ID, messageId);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
// Read the message from local db
|
||||
final MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
|
||||
if (message != null && message.canDownloadMessage()) {
|
||||
final Uri notificationUri = message.getSmsMessageUri();
|
||||
final String conversationId = message.getConversationId();
|
||||
final int status = message.getStatus();
|
||||
|
||||
final String selfId = message.getSelfId();
|
||||
final ParticipantData self = BugleDatabaseOperations
|
||||
.getExistingParticipant(db, selfId);
|
||||
final int subId = self.getSubId();
|
||||
actionParameters.putInt(KEY_SUB_ID, subId);
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
actionParameters.putString(KEY_PARTICIPANT_ID, message.getParticipantId());
|
||||
actionParameters.putString(KEY_CONTENT_LOCATION, message.getMmsContentLocation());
|
||||
actionParameters.putString(KEY_TRANSACTION_ID, message.getMmsTransactionId());
|
||||
actionParameters.putParcelable(KEY_NOTIFICATION_URI, notificationUri);
|
||||
actionParameters.putBoolean(KEY_AUTO_DOWNLOAD, isAutoDownload(status));
|
||||
actionParameters.putLong(KEY_EXPIRY, message.getMmsExpiry());
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
if (message.getInDownloadWindow(now)) {
|
||||
// We can still retry
|
||||
actionParameters.putString(KEY_SUB_PHONE_NUMBER, self.getNormalizedDestination());
|
||||
|
||||
final int downloadingStatus = getDownloadingStatus(status);
|
||||
// Update message status to indicate downloading.
|
||||
updateMessageStatus(notificationUri, messageId, conversationId,
|
||||
downloadingStatus, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED);
|
||||
// Pre-compute the next status when failed so we don't have to load from db again
|
||||
actionParameters.putInt(KEY_FAILURE_STATUS, getFailureStatus(downloadingStatus));
|
||||
|
||||
// Actual download happens in background
|
||||
processingAction.requestBackgroundWork(this);
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG,
|
||||
"DownloadMmsAction: Queued download of MMS message " + messageId);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
LogUtil.w(TAG, "DownloadMmsAction: Download of MMS message " + messageId
|
||||
+ " failed (outside download window)");
|
||||
|
||||
// Retries depleted and we failed. Update the message status so we won't retry again
|
||||
updateMessageStatus(notificationUri, messageId, conversationId,
|
||||
MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED,
|
||||
MessageData.RAW_TELEPHONY_STATUS_UNDEFINED);
|
||||
if (status == MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD) {
|
||||
// For auto download failure, we should send a DEFERRED NotifyRespInd
|
||||
// to carrier to indicate we will manual download later
|
||||
ProcessDownloadedMmsAction.sendDeferredRespStatus(
|
||||
messageId, message.getMmsTransactionId(),
|
||||
message.getMmsContentLocation(), subId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out the auto download state of this message based on its starting status
|
||||
*
|
||||
* @param status The starting status of the message.
|
||||
* @return True if this is a message doing auto downloading, false otherwise
|
||||
*/
|
||||
private static boolean isAutoDownload(final int status) {
|
||||
switch (status) {
|
||||
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD:
|
||||
return false;
|
||||
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD:
|
||||
return true;
|
||||
default:
|
||||
Assert.fail("isAutoDownload: invalid input status " + status);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the corresponding downloading status based on the starting status of the message
|
||||
*
|
||||
* @param status The starting status of the message.
|
||||
* @return The downloading status
|
||||
*/
|
||||
private static int getDownloadingStatus(final int status) {
|
||||
switch (status) {
|
||||
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD:
|
||||
return MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING;
|
||||
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD:
|
||||
return MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING;
|
||||
default:
|
||||
Assert.fail("isAutoDownload: invalid input status " + status);
|
||||
return MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the corresponding failed status based on the current downloading status
|
||||
*
|
||||
* @param status The downloading status
|
||||
* @return The status the message should have if downloading failed
|
||||
*/
|
||||
private static int getFailureStatus(final int status) {
|
||||
switch (status) {
|
||||
case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING:
|
||||
return MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD;
|
||||
case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING:
|
||||
return MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD;
|
||||
default:
|
||||
Assert.fail("isAutoDownload: invalid input status " + status);
|
||||
return MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bundle doBackgroundWork() {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID);
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
final Uri notificationUri = actionParameters.getParcelable(KEY_NOTIFICATION_URI, Uri.class);
|
||||
final String subPhoneNumber = actionParameters.getString(KEY_SUB_PHONE_NUMBER);
|
||||
final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
|
||||
final String contentLocation = actionParameters.getString(KEY_CONTENT_LOCATION);
|
||||
final boolean autoDownload = actionParameters.getBoolean(KEY_AUTO_DOWNLOAD);
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final String participantId = actionParameters.getString(KEY_PARTICIPANT_ID);
|
||||
final int statusIfFailed = actionParameters.getInt(KEY_FAILURE_STATUS);
|
||||
final long expiry = actionParameters.getLong(KEY_EXPIRY);
|
||||
|
||||
final long receivedTimestampRoundedToSecond =
|
||||
1000 * ((System.currentTimeMillis() + 500) / 1000);
|
||||
|
||||
LogUtil.i(TAG, "DownloadMmsAction: Downloading MMS message " + messageId
|
||||
+ " (" + (autoDownload ? "auto" : "manual") + ")");
|
||||
|
||||
// Bundle some values we'll need after the message is downloaded (via platform APIs)
|
||||
final Bundle extras = new Bundle();
|
||||
extras.putString(EXTRA_MESSAGE_ID, messageId);
|
||||
extras.putString(EXTRA_CONVERSATION_ID, conversationId);
|
||||
extras.putString(EXTRA_PARTICIPANT_ID, participantId);
|
||||
extras.putInt(EXTRA_STATUS_IF_FAILED, statusIfFailed);
|
||||
|
||||
// Start the download
|
||||
final MmsUtils.StatusPlusUri status = MmsUtils.downloadMmsMessage(context,
|
||||
notificationUri, subId, subPhoneNumber, transactionId, contentLocation,
|
||||
autoDownload, receivedTimestampRoundedToSecond / 1000L, expiry / 1000L, extras);
|
||||
if (status == MmsUtils.STATUS_PENDING) {
|
||||
// Async download; no status yet
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "DownloadMmsAction: Downloading MMS message " + messageId
|
||||
+ " asynchronously; waiting for pending intent to signal completion");
|
||||
}
|
||||
} else {
|
||||
// Inform sync that message has been added at local received timestamp
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(receivedTimestampRoundedToSecond);
|
||||
// Handle downloaded message
|
||||
ProcessDownloadedMmsAction.processMessageDownloadFastFailed(messageId,
|
||||
notificationUri, conversationId, participantId, contentLocation, subId,
|
||||
subPhoneNumber, statusIfFailed, autoDownload, transactionId,
|
||||
status.resultCode);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object processBackgroundResponse(final Bundle response) {
|
||||
// Nothing to do here; post-download actions handled by ProcessDownloadedMmsAction
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object processBackgroundFailure() {
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final String participantId = actionParameters.getString(KEY_PARTICIPANT_ID);
|
||||
final int statusIfFailed = actionParameters.getInt(KEY_FAILURE_STATUS);
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID);
|
||||
|
||||
ProcessDownloadedMmsAction.processDownloadActionFailure(messageId,
|
||||
MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED,
|
||||
conversationId, participantId, statusIfFailed, subId, transactionId);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static void updateMessageStatus(final Uri messageUri, final String messageId,
|
||||
final String conversationId, final int status, final int rawStatus) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
// Downloading status just kept in local DB but need to fix up telephony DB first
|
||||
if (status == MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING ||
|
||||
status == MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING) {
|
||||
MmsUtils.clearMmsStatus(context, messageUri);
|
||||
}
|
||||
// Then mark downloading status in our local DB
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(MessageColumns.STATUS, status);
|
||||
values.put(MessageColumns.RAW_TELEPHONY_STATUS, rawStatus);
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
BugleDatabaseOperations.updateMessageRowIfExists(db, messageId, values);
|
||||
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
}
|
||||
|
||||
private DownloadMmsAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<DownloadMmsAction> CREATOR = new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public DownloadMmsAction createFromParcel(final Parcel in) {
|
||||
return new DownloadMmsAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadMmsAction[] newArray(final int size) {
|
||||
return new DownloadMmsAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Action used to fixup actively downloading or sending status at startup - just in case we
|
||||
* crash - never run this when a message might actually be sending or downloading.
|
||||
*/
|
||||
public class FixupMessageStatusOnStartupAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
public static void fixupMessageStatus() {
|
||||
final FixupMessageStatusOnStartupAction action = new FixupMessageStatusOnStartupAction();
|
||||
action.start();
|
||||
}
|
||||
|
||||
private FixupMessageStatusOnStartupAction() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
// Now mark any messages in active sending or downloading state as inactive
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
int downloadFailedCnt;
|
||||
int sendFailedCnt;
|
||||
try {
|
||||
// For both sending and downloading messages, let's assume they failed.
|
||||
// For MMS sent/downloaded via platform, the sent/downloaded pending intent
|
||||
// may come back. That will update the message. User may see the message
|
||||
// in wrong status within a short window if that happens. But this should
|
||||
// rarely happen. This is a simple solution to situations like app gets killed
|
||||
// while the pending intent is still in the fly. Alternatively, we could
|
||||
// keep the status for platform sent/downloaded MMS and timeout these messages.
|
||||
// But that is much more complex.
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(DatabaseHelper.MessageColumns.STATUS,
|
||||
MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED);
|
||||
downloadFailedCnt = db.update(DatabaseHelper.MESSAGES_TABLE, values,
|
||||
DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)",
|
||||
new String[]{
|
||||
Integer.toString(MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING),
|
||||
Integer.toString(MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING)
|
||||
});
|
||||
|
||||
values.clear();
|
||||
values.put(DatabaseHelper.MessageColumns.STATUS,
|
||||
MessageData.BUGLE_STATUS_OUTGOING_FAILED);
|
||||
sendFailedCnt = db.update(DatabaseHelper.MESSAGES_TABLE, values,
|
||||
DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)",
|
||||
new String[]{
|
||||
Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_SENDING),
|
||||
Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_RESENDING)
|
||||
});
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
LogUtil.i(TAG, "Fixup: Send failed - " + sendFailedCnt
|
||||
+ " Download failed - " + downloadFailedCnt);
|
||||
|
||||
// Don't send contentObserver notifications as displayed text should not change
|
||||
return null;
|
||||
}
|
||||
|
||||
private FixupMessageStatusOnStartupAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<FixupMessageStatusOnStartupAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public FixupMessageStatusOnStartupAction createFromParcel(final Parcel in) {
|
||||
return new FixupMessageStatusOnStartupAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FixupMessageStatusOnStartupAction[] newArray(final int size) {
|
||||
return new FixupMessageStatusOnStartupAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.action.ActionMonitor.ActionCompletedListener;
|
||||
import com.android.messaging.datamodel.data.LaunchConversationData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.RunsOnMainThread;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Action used to get or create a conversation for a list of conversation participants.
|
||||
*/
|
||||
public class GetOrCreateConversationAction extends Action implements Parcelable {
|
||||
/**
|
||||
* Interface for GetOrCreateConversationAction listeners
|
||||
*/
|
||||
public interface GetOrCreateConversationActionListener {
|
||||
@RunsOnMainThread
|
||||
void onGetOrCreateConversationSucceeded(final ActionMonitor monitor,
|
||||
final Object data, final String conversationId);
|
||||
|
||||
@RunsOnMainThread
|
||||
void onGetOrCreateConversationFailed(final ActionMonitor monitor,
|
||||
final Object data);
|
||||
}
|
||||
|
||||
public static GetOrCreateConversationActionMonitor getOrCreateConversation(
|
||||
final ArrayList<ParticipantData> participants, final Object data,
|
||||
final GetOrCreateConversationActionListener listener) {
|
||||
final GetOrCreateConversationActionMonitor monitor = new
|
||||
GetOrCreateConversationActionMonitor(data, listener);
|
||||
final GetOrCreateConversationAction action = new GetOrCreateConversationAction(participants,
|
||||
monitor.getActionKey());
|
||||
action.start(monitor);
|
||||
return monitor;
|
||||
}
|
||||
|
||||
|
||||
public static GetOrCreateConversationActionMonitor getOrCreateConversation(
|
||||
final String[] recipients, final Object data, final LaunchConversationData listener) {
|
||||
final ArrayList<ParticipantData> participants = new ArrayList<>();
|
||||
for (String recipient : recipients) {
|
||||
recipient = recipient.trim();
|
||||
if (!TextUtils.isEmpty(recipient)) {
|
||||
participants.add(ParticipantData.getFromRawPhoneBySystemLocale(recipient));
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "getOrCreateConversation hit empty recipient");
|
||||
}
|
||||
}
|
||||
return getOrCreateConversation(participants, data, listener);
|
||||
}
|
||||
|
||||
private static final String KEY_PARTICIPANTS_LIST = "participants_list";
|
||||
|
||||
private GetOrCreateConversationAction(final ArrayList<ParticipantData> participants,
|
||||
final String actionKey) {
|
||||
super(actionKey);
|
||||
actionParameters.putParcelableArrayList(KEY_PARTICIPANTS_LIST, participants);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup the conversation or create a new one.
|
||||
*/
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// First find the thread id for this list of participants.
|
||||
final ArrayList<ParticipantData> participants =
|
||||
actionParameters.getParcelableArrayList(KEY_PARTICIPANTS_LIST,
|
||||
ParticipantData.class);
|
||||
BugleDatabaseOperations.sanitizeConversationParticipants(participants);
|
||||
final ArrayList<String> recipients =
|
||||
BugleDatabaseOperations.getRecipientsFromConversationParticipants(participants);
|
||||
|
||||
final long threadId = MmsUtils.getOrCreateThreadId(Factory.get().getApplicationContext(),
|
||||
recipients);
|
||||
|
||||
if (threadId < 0) {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Couldn't create a threadId in SMS db for numbers : " +
|
||||
LogUtil.sanitizePII(recipients.toString()));
|
||||
// TODO: Add a better way to indicate an error from executeAction.
|
||||
return null;
|
||||
}
|
||||
|
||||
final String conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
|
||||
false, participants);
|
||||
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* A monitor that notifies a listener upon completion
|
||||
*/
|
||||
public static class GetOrCreateConversationActionMonitor extends ActionMonitor
|
||||
implements ActionCompletedListener {
|
||||
private final GetOrCreateConversationActionListener mListener;
|
||||
|
||||
GetOrCreateConversationActionMonitor(final Object data,
|
||||
final GetOrCreateConversationActionListener listener) {
|
||||
super(STATE_CREATED, generateUniqueActionKey("GetOrCreateConversationAction"), data);
|
||||
setCompletedListener(this);
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionSucceeded(final ActionMonitor monitor,
|
||||
final Action action, final Object data, final Object result) {
|
||||
if (result == null) {
|
||||
mListener.onGetOrCreateConversationFailed(monitor, data);
|
||||
} else {
|
||||
mListener.onGetOrCreateConversationSucceeded(monitor, data, (String) result);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionFailed(final ActionMonitor monitor,
|
||||
final Action action, final Object data, final Object result) {
|
||||
// TODO: Currently onActionFailed is only called if there is an error in
|
||||
// processing requests, not for errors in the local processing.
|
||||
Assert.fail("Unreachable");
|
||||
mListener.onGetOrCreateConversationFailed(monitor, data);
|
||||
}
|
||||
}
|
||||
|
||||
private GetOrCreateConversationAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<GetOrCreateConversationAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public GetOrCreateConversationAction createFromParcel(final Parcel in) {
|
||||
return new GetOrCreateConversationAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetOrCreateConversationAction[] newArray(final int size) {
|
||||
return new GetOrCreateConversationAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 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.action;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.Telephony;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.MessagePartData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Action used to convert a draft message to an outgoing message. Its writes SMS messages to
|
||||
* the telephony db, but {@link SendMessageAction} is responsible for inserting MMS message into
|
||||
* the telephony DB. The latter also does the actual sending of the message in the background.
|
||||
* The latter is also responsible for re-sending a failed message.
|
||||
*/
|
||||
public class InsertNewMessageAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
private static long sLastSentMessageTimestamp = -1;
|
||||
|
||||
/**
|
||||
* Insert message (no listener)
|
||||
*/
|
||||
public static void insertNewMessage(final MessageData message) {
|
||||
final InsertNewMessageAction action = new InsertNewMessageAction(message);
|
||||
action.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert message (no listener) with a given non-default subId.
|
||||
*/
|
||||
public static void insertNewMessage(final MessageData message, final int subId) {
|
||||
Assert.isFalse(subId == ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
final InsertNewMessageAction action = new InsertNewMessageAction(message, subId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert message (no listener)
|
||||
*/
|
||||
public static void insertNewMessage(final int subId, final String recipients,
|
||||
final String messageText, final String subject) {
|
||||
final InsertNewMessageAction action = new InsertNewMessageAction(
|
||||
subId, recipients, messageText, subject);
|
||||
action.start();
|
||||
}
|
||||
|
||||
public static long getLastSentMessageTimestamp() {
|
||||
return sLastSentMessageTimestamp;
|
||||
}
|
||||
|
||||
private static final String KEY_SUB_ID = "sub_id";
|
||||
private static final String KEY_MESSAGE = "message";
|
||||
private static final String KEY_RECIPIENTS = "recipients";
|
||||
private static final String KEY_MESSAGE_TEXT = "message_text";
|
||||
private static final String KEY_SUBJECT_TEXT = "subject_text";
|
||||
|
||||
private InsertNewMessageAction(final MessageData message) {
|
||||
this(message, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
actionParameters.putParcelable(KEY_MESSAGE, message);
|
||||
}
|
||||
|
||||
private InsertNewMessageAction(final MessageData message, final int subId) {
|
||||
super();
|
||||
actionParameters.putParcelable(KEY_MESSAGE, message);
|
||||
actionParameters.putInt(KEY_SUB_ID, subId);
|
||||
}
|
||||
|
||||
private InsertNewMessageAction(final int subId, final String recipients,
|
||||
final String messageText, final String subject) {
|
||||
super();
|
||||
if (TextUtils.isEmpty(recipients) || TextUtils.isEmpty(messageText)) {
|
||||
Assert.fail("InsertNewMessageAction: Can't have empty recipients or message");
|
||||
}
|
||||
actionParameters.putInt(KEY_SUB_ID, subId);
|
||||
actionParameters.putString(KEY_RECIPIENTS, recipients);
|
||||
actionParameters.putString(KEY_MESSAGE_TEXT, messageText);
|
||||
actionParameters.putString(KEY_SUBJECT_TEXT, subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add message to database in pending state and queue actual sending
|
||||
*/
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
MessageData message = actionParameters.getParcelable(KEY_MESSAGE, MessageData.class);
|
||||
if (message == null) {
|
||||
LogUtil.i(TAG, "InsertNewMessageAction: Creating MessageData with provided data");
|
||||
message = createMessage();
|
||||
if (message == null) {
|
||||
LogUtil.w(TAG, "InsertNewMessageAction: Could not create MessageData");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final String conversationId = message.getConversationId();
|
||||
|
||||
final ParticipantData self = getSelf(db, conversationId, message);
|
||||
if (self == null) {
|
||||
return null;
|
||||
}
|
||||
message.bindSelfId(self.getId());
|
||||
// If the user taps the Send button before the conversation draft is created/loaded by
|
||||
// ReadDraftDataAction (maybe the action service thread was busy), the MessageData may not
|
||||
// have the participant id set. It should be equal to the self id, so we'll use that.
|
||||
if (message.getParticipantId() == null) {
|
||||
message.bindParticipantId(self.getId());
|
||||
}
|
||||
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final ArrayList<String> recipients =
|
||||
BugleDatabaseOperations.getRecipientsForConversation(db, conversationId);
|
||||
if (recipients.size() < 1) {
|
||||
LogUtil.w(TAG, "InsertNewMessageAction: message recipients is empty");
|
||||
return null;
|
||||
}
|
||||
final int subId = self.getSubId();
|
||||
LogUtil.i(TAG, "InsertNewMessageAction: inserting new message for subId " + subId);
|
||||
actionParameters.putInt(KEY_SUB_ID, subId);
|
||||
|
||||
// TODO: Work out whether to send with SMS or MMS (taking into account recipients)?
|
||||
final boolean isSms = (message.getProtocol() == MessageData.PROTOCOL_SMS);
|
||||
if (isSms) {
|
||||
String sendingConversationId = conversationId;
|
||||
if (recipients.size() > 1) {
|
||||
// Broadcast SMS - put message in "fake conversation" before farming out to real 1:1
|
||||
final long laterTimestamp = timestamp + 1;
|
||||
// Send a single message
|
||||
insertBroadcastSmsMessage(conversationId, message, subId,
|
||||
laterTimestamp, recipients);
|
||||
|
||||
sendingConversationId = null;
|
||||
}
|
||||
|
||||
for (final String recipient : recipients) {
|
||||
// Start actual sending
|
||||
insertSendingSmsMessage(message, subId, recipient,
|
||||
timestamp, sendingConversationId);
|
||||
}
|
||||
|
||||
// Can now clear draft from conversation (deleting attachments if necessary)
|
||||
BugleDatabaseOperations.updateDraftMessageData(db, conversationId,
|
||||
null /* message */, BugleDatabaseOperations.UPDATE_MODE_CLEAR_DRAFT);
|
||||
} else {
|
||||
final long timestampRoundedToSecond = 1000 * ((timestamp + 500) / 1000);
|
||||
// Write place holder message directly referencing parts from the draft
|
||||
final MessageData messageToSend = insertSendingMmsMessage(conversationId,
|
||||
message, timestampRoundedToSecond);
|
||||
|
||||
// Can now clear draft from conversation (preserving attachments which are now
|
||||
// referenced by messageToSend)
|
||||
BugleDatabaseOperations.updateDraftMessageData(db, conversationId,
|
||||
messageToSend, BugleDatabaseOperations.UPDATE_MODE_CLEAR_DRAFT);
|
||||
}
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private ParticipantData getSelf(
|
||||
final DatabaseWrapper db, final String conversationId, final MessageData message) {
|
||||
ParticipantData self;
|
||||
// Check if we are asked to bind to a non-default subId. This is directly passed in from
|
||||
// the UI thread so that the sub id may be locked as soon as the user clicks on the Send
|
||||
// button.
|
||||
final int requestedSubId = actionParameters.getInt(
|
||||
KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
if (requestedSubId != ParticipantData.DEFAULT_SELF_SUB_ID) {
|
||||
self = BugleDatabaseOperations.getOrCreateSelf(db, requestedSubId);
|
||||
} else {
|
||||
String selfId = message.getSelfId();
|
||||
if (selfId == null) {
|
||||
// The conversation draft provides no self id hint, meaning that 1) conversation
|
||||
// self id was not loaded AND 2) the user didn't pick a SIM from the SIM selector.
|
||||
// In this case, use the conversation's self id.
|
||||
final ConversationListItemData conversation =
|
||||
ConversationListItemData.getExistingConversation(db, conversationId);
|
||||
if (conversation != null) {
|
||||
selfId = conversation.getSelfId();
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_DATAMODEL_TAG, "Conversation " + conversationId +
|
||||
"already deleted before sending draft message " +
|
||||
message.getMessageId() + ". Aborting InsertNewMessageAction.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// We do not use SubscriptionManager.DEFAULT_SUB_ID for sending a message, so we need
|
||||
// to bind the message to the system default subscription if it's unbound.
|
||||
final ParticipantData unboundSelf = BugleDatabaseOperations.getExistingParticipant(
|
||||
db, selfId);
|
||||
if (unboundSelf.getSubId() == ParticipantData.DEFAULT_SELF_SUB_ID) {
|
||||
final int defaultSubId = PhoneUtils.getDefault().getDefaultSmsSubscriptionId();
|
||||
self = BugleDatabaseOperations.getOrCreateSelf(db, defaultSubId);
|
||||
} else {
|
||||
self = unboundSelf;
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/** Create MessageData using KEY_RECIPIENTS, KEY_MESSAGE_TEXT and KEY_SUBJECT */
|
||||
private MessageData createMessage() {
|
||||
// First find the thread id for this list of participants.
|
||||
final String recipientsList = actionParameters.getString(KEY_RECIPIENTS);
|
||||
final String messageText = actionParameters.getString(KEY_MESSAGE_TEXT);
|
||||
final String subjectText = actionParameters.getString(KEY_SUBJECT_TEXT);
|
||||
final int subId = actionParameters.getInt(
|
||||
KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
|
||||
final ArrayList<ParticipantData> participants = new ArrayList<>();
|
||||
for (final String recipient : recipientsList.split(",")) {
|
||||
participants.add(ParticipantData.getFromRawPhoneBySimLocale(recipient, subId));
|
||||
}
|
||||
if (participants.size() == 0) {
|
||||
Assert.fail("InsertNewMessage: Empty participants");
|
||||
return null;
|
||||
}
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
BugleDatabaseOperations.sanitizeConversationParticipants(participants);
|
||||
final ArrayList<String> recipients =
|
||||
BugleDatabaseOperations.getRecipientsFromConversationParticipants(participants);
|
||||
if (recipients.size() == 0) {
|
||||
Assert.fail("InsertNewMessage: Empty recipients");
|
||||
return null;
|
||||
}
|
||||
|
||||
final long threadId = MmsUtils.getOrCreateThreadId(Factory.get().getApplicationContext(),
|
||||
recipients);
|
||||
|
||||
if (threadId < 0) {
|
||||
Assert.fail("InsertNewMessage: Couldn't get threadId in SMS db for these recipients: "
|
||||
+ recipients);
|
||||
// TODO: How do we fail the action?
|
||||
return null;
|
||||
}
|
||||
|
||||
final String conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
|
||||
false, participants);
|
||||
|
||||
final ParticipantData self = BugleDatabaseOperations.getOrCreateSelf(db, subId);
|
||||
|
||||
if (TextUtils.isEmpty(subjectText)) {
|
||||
return MessageData.createDraftSmsMessage(conversationId, self.getId(), messageText);
|
||||
} else {
|
||||
return MessageData.createDraftMmsMessage(conversationId, self.getId(), messageText,
|
||||
subjectText);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertBroadcastSmsMessage(final String conversationId,
|
||||
final MessageData message, final int subId, final long laterTimestamp,
|
||||
final ArrayList<String> recipients) {
|
||||
LogUtil.v(TAG, "InsertNewMessageAction: Inserting broadcast SMS message "
|
||||
+ message.getMessageId());
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// Inform sync that message is being added at timestamp
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(laterTimestamp);
|
||||
|
||||
final long threadId = BugleDatabaseOperations.getThreadId(db, conversationId);
|
||||
final String address = TextUtils.join(" ", recipients);
|
||||
|
||||
final String messageText = message.getMessageText();
|
||||
// Insert message into telephony database sms message table
|
||||
final Uri messageUri = MmsUtils.insertSmsMessage(context,
|
||||
Telephony.Sms.CONTENT_URI,
|
||||
subId,
|
||||
address,
|
||||
messageText,
|
||||
laterTimestamp,
|
||||
Telephony.Sms.STATUS_COMPLETE,
|
||||
Telephony.Sms.MESSAGE_TYPE_SENT, threadId);
|
||||
if (messageUri != null && !TextUtils.isEmpty(messageUri.toString())) {
|
||||
db.beginTransaction();
|
||||
try {
|
||||
message.updateSendingMessage(conversationId, messageUri, laterTimestamp);
|
||||
message.markMessageSent(laterTimestamp);
|
||||
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
|
||||
BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
|
||||
conversationId, message.getMessageId(), laterTimestamp,
|
||||
false /* senderBlocked */, false /* shouldAutoSwitchSelfId */);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "InsertNewMessageAction: Inserted broadcast SMS message "
|
||||
+ message.getMessageId() + ", uri = " + message.getSmsMessageUri());
|
||||
}
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
} else {
|
||||
// Ignore error as we only really care about the individual messages?
|
||||
LogUtil.e(TAG,
|
||||
"InsertNewMessageAction: No uri for broadcast SMS " + message.getMessageId()
|
||||
+ " inserted into telephony DB");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert SMS messaging into our database and telephony db.
|
||||
*/
|
||||
private MessageData insertSendingSmsMessage(final MessageData content, final int subId,
|
||||
final String recipient, final long timestamp, final String sendingConversationId) {
|
||||
sLastSentMessageTimestamp = timestamp;
|
||||
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
|
||||
// Inform sync that message is being added at timestamp
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(timestamp);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// Send a single message
|
||||
long threadId;
|
||||
String conversationId;
|
||||
if (sendingConversationId == null) {
|
||||
// For 1:1 message generated sending broadcast need to look up threadId+conversationId
|
||||
threadId = MmsUtils.getOrCreateSmsThreadId(context, recipient);
|
||||
conversationId = BugleDatabaseOperations.getOrCreateConversationFromRecipient(
|
||||
db, threadId, false /* sender blocked */,
|
||||
ParticipantData.getFromRawPhoneBySimLocale(recipient, subId));
|
||||
} else {
|
||||
// Otherwise just look up threadId
|
||||
threadId = BugleDatabaseOperations.getThreadId(db, sendingConversationId);
|
||||
conversationId = sendingConversationId;
|
||||
}
|
||||
|
||||
final String messageText = content.getMessageText();
|
||||
|
||||
// Insert message into telephony database sms message table
|
||||
final Uri messageUri = MmsUtils.insertSmsMessage(context,
|
||||
Telephony.Sms.CONTENT_URI,
|
||||
subId,
|
||||
recipient,
|
||||
messageText,
|
||||
timestamp,
|
||||
Telephony.Sms.STATUS_NONE,
|
||||
Telephony.Sms.MESSAGE_TYPE_SENT, threadId);
|
||||
|
||||
MessageData message = null;
|
||||
if (messageUri != null && !TextUtils.isEmpty(messageUri.toString())) {
|
||||
db.beginTransaction();
|
||||
try {
|
||||
message = MessageData.createDraftSmsMessage(conversationId,
|
||||
content.getSelfId(), messageText);
|
||||
message.updateSendingMessage(conversationId, messageUri, timestamp);
|
||||
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
|
||||
// Do not update the conversation summary to reflect autogenerated 1:1 messages
|
||||
if (sendingConversationId != null) {
|
||||
BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
|
||||
conversationId, message.getMessageId(), timestamp,
|
||||
false /* senderBlocked */, false /* shouldAutoSwitchSelfId */);
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "InsertNewMessageAction: Inserted SMS message "
|
||||
+ message.getMessageId() + " (uri = " + message.getSmsMessageUri()
|
||||
+ ", timestamp = " + message.getReceivedTimeStamp() + ")");
|
||||
}
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
} else {
|
||||
LogUtil.e(TAG, "InsertNewMessageAction: No uri for SMS inserted into telephony DB");
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert MMS messaging into our database.
|
||||
*/
|
||||
private MessageData insertSendingMmsMessage(final String conversationId,
|
||||
final MessageData message, final long timestamp) {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
final List<MessagePartData> attachmentsUpdated = new ArrayList<>();
|
||||
try {
|
||||
sLastSentMessageTimestamp = timestamp;
|
||||
|
||||
// Insert "draft" message as placeholder until the final message is written to
|
||||
// the telephony db
|
||||
message.updateSendingMessage(conversationId, null/*messageUri*/, timestamp);
|
||||
|
||||
// No need to inform SyncManager as message currently has no Uri...
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
|
||||
BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
|
||||
conversationId, message.getMessageId(), timestamp,
|
||||
false /* senderBlocked */, false /* shouldAutoSwitchSelfId */);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "InsertNewMessageAction: Inserted MMS message "
|
||||
+ message.getMessageId() + " (timestamp = " + timestamp + ")");
|
||||
}
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private InsertNewMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<InsertNewMessageAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public InsertNewMessageAction createFromParcel(final Parcel in) {
|
||||
return new InsertNewMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InsertNewMessageAction[] newArray(final int size) {
|
||||
return new InsertNewMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Action used to mark all the messages in a conversation as read
|
||||
*/
|
||||
public class MarkAsReadAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
|
||||
/**
|
||||
* Mark all the messages as read for a particular conversation.
|
||||
*/
|
||||
public static void markAsRead(final String conversationId) {
|
||||
final MarkAsReadAction action = new MarkAsReadAction(conversationId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private MarkAsReadAction(final String conversationId) {
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
|
||||
// TODO: Consider doing this in background service to avoid delaying other actions
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// Mark all messages in thread as read in telephony
|
||||
final long threadId = BugleDatabaseOperations.getThreadId(db, conversationId);
|
||||
if (threadId != -1) {
|
||||
MmsUtils.updateSmsReadStatus(threadId, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
// Update local db
|
||||
db.beginTransaction();
|
||||
try {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(MessageColumns.CONVERSATION_ID, conversationId);
|
||||
values.put(MessageColumns.READ, 1);
|
||||
values.put(MessageColumns.SEEN, 1); // if they read it, they saw it
|
||||
|
||||
final int count = db.update(DatabaseHelper.MESSAGES_TABLE, values,
|
||||
"(" + MessageColumns.READ + " !=1 OR " +
|
||||
MessageColumns.SEEN + " !=1 ) AND " +
|
||||
MessageColumns.CONVERSATION_ID + "=?",
|
||||
new String[] { conversationId });
|
||||
if (count > 0) {
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
// After marking messages as read, update the notifications. This will
|
||||
// clear the now stale notifications.
|
||||
BugleNotifications.update(false/*silent*/, BugleNotifications.UPDATE_ALL);
|
||||
return null;
|
||||
}
|
||||
|
||||
private MarkAsReadAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<MarkAsReadAction> CREATOR = new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public MarkAsReadAction createFromParcel(final Parcel in) {
|
||||
return new MarkAsReadAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MarkAsReadAction[] newArray(final int size) {
|
||||
return new MarkAsReadAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Action used to mark all messages as seen
|
||||
*/
|
||||
public class MarkAsSeenAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
|
||||
/**
|
||||
* Mark all messages as seen.
|
||||
*/
|
||||
public static void markAllAsSeen() {
|
||||
final MarkAsSeenAction action = new MarkAsSeenAction((String) null/*conversationId*/);
|
||||
action.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all messages of a given conversation as seen.
|
||||
*/
|
||||
public static void markAsSeen(final String conversationId) {
|
||||
final MarkAsSeenAction action = new MarkAsSeenAction(conversationId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* ctor for MarkAsSeenAction.
|
||||
* @param conversationId the conversation id for which to mark as seen, or null to mark all
|
||||
* messages as seen
|
||||
*/
|
||||
public MarkAsSeenAction(final String conversationId) {
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String conversationId =
|
||||
actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final boolean hasSpecificConversation = !TextUtils.isEmpty(conversationId);
|
||||
|
||||
// Everything in telephony should already have the seen bit set.
|
||||
// Possible exception are messages which did not have seen set and
|
||||
// were sync'ed into bugle.
|
||||
|
||||
// Now mark the messages as seen in the bugle db
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(MessageColumns.SEEN, 1);
|
||||
|
||||
if (hasSpecificConversation) {
|
||||
final int count = db.update(DatabaseHelper.MESSAGES_TABLE, values,
|
||||
MessageColumns.SEEN + " != 1 AND " +
|
||||
MessageColumns.CONVERSATION_ID + "=?",
|
||||
new String[] { conversationId });
|
||||
if (count > 0) {
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
}
|
||||
} else {
|
||||
db.update(DatabaseHelper.MESSAGES_TABLE, values,
|
||||
MessageColumns.SEEN + " != 1", null/*selectionArgs*/);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
// After marking messages as seen, update the notifications. This will
|
||||
// clear the now stale notifications.
|
||||
BugleNotifications.update(false/*silent*/, BugleNotifications.UPDATE_ALL);
|
||||
return null;
|
||||
}
|
||||
|
||||
private MarkAsSeenAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<MarkAsSeenAction> CREATOR = new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public MarkAsSeenAction createFromParcel(final Parcel in) {
|
||||
return new MarkAsSeenAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MarkAsSeenAction[] newArray(final int size) {
|
||||
return new MarkAsSeenAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.Telephony;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ProcessDeliveryReportAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
private static final String KEY_URI = "uri";
|
||||
private static final String KEY_STATUS = "status";
|
||||
|
||||
private ProcessDeliveryReportAction(final Uri uri, final int status) {
|
||||
actionParameters.putParcelable(KEY_URI, uri);
|
||||
actionParameters.putInt(KEY_STATUS, status);
|
||||
}
|
||||
|
||||
public static void deliveryReportReceived(final Uri uri, final int status) {
|
||||
final ProcessDeliveryReportAction action = new ProcessDeliveryReportAction(uri, status);
|
||||
action.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final Uri smsMessageUri = actionParameters.getParcelable(KEY_URI, Uri.class);
|
||||
final int status = actionParameters.getInt(KEY_STATUS);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
final long messageRowId = ContentUris.parseId(smsMessageUri);
|
||||
if (messageRowId < 0) {
|
||||
LogUtil.e(TAG, "ProcessDeliveryReportAction: can't find message");
|
||||
return null;
|
||||
}
|
||||
final long timeSentInMillis = System.currentTimeMillis();
|
||||
// Update telephony provider
|
||||
if (smsMessageUri != null) {
|
||||
MmsUtils.updateSmsStatusAndDateSent(smsMessageUri, status, timeSentInMillis);
|
||||
}
|
||||
|
||||
// Update local message
|
||||
db.beginTransaction();
|
||||
try {
|
||||
final ContentValues values = new ContentValues();
|
||||
final int bugleStatus = SyncMessageBatch.bugleStatusForSms(true /*outgoing*/,
|
||||
Telephony.Sms.MESSAGE_TYPE_SENT /* type */, status);
|
||||
values.put(DatabaseHelper.MessageColumns.STATUS, bugleStatus);
|
||||
values.put(DatabaseHelper.MessageColumns.SENT_TIMESTAMP,
|
||||
TimeUnit.MILLISECONDS.toMicros(timeSentInMillis));
|
||||
|
||||
final MessageData messageData =
|
||||
BugleDatabaseOperations.readMessageData(db, smsMessageUri);
|
||||
|
||||
// Check the message was not removed before the delivery report comes in
|
||||
if (messageData != null) {
|
||||
Assert.isTrue(smsMessageUri.equals(messageData.getSmsMessageUri()));
|
||||
|
||||
// Row must exist as was just loaded above (on ActionService thread)
|
||||
BugleDatabaseOperations.updateMessageRow(db, messageData.getMessageId(), values);
|
||||
|
||||
MessagingContentProvider.notifyMessagesChanged(messageData.getConversationId());
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ProcessDeliveryReportAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ProcessDeliveryReportAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public ProcessDeliveryReportAction createFromParcel(final Parcel in) {
|
||||
return new ProcessDeliveryReportAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProcessDeliveryReportAction[] newArray(final int size) {
|
||||
return new ProcessDeliveryReportAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
+583
@@ -0,0 +1,583 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 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.action;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.Telephony.Mms;
|
||||
import android.support.v7.mms.pdu.PduHeaders;
|
||||
import android.support.v7.mms.pdu.RetrieveConf;
|
||||
import android.telephony.SmsManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.MmsFileProvider;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.mmslib.SqliteWrapper;
|
||||
import com.android.messaging.sms.DatabaseMessages;
|
||||
import com.android.messaging.sms.MmsSender;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.google.common.io.Files;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Processes an MMS message after it has been downloaded.
|
||||
* NOTE: This action must queue a ProcessPendingMessagesAction when it is done (success or failure).
|
||||
*/
|
||||
public class ProcessDownloadedMmsAction extends Action {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
// Always set when message downloaded
|
||||
private static final String KEY_DOWNLOADED_BY_PLATFORM = "downloaded_by_platform";
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_NOTIFICATION_URI = "notification_uri";
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
private static final String KEY_PARTICIPANT_ID = "participant_id";
|
||||
private static final String KEY_STATUS_IF_FAILED = "status_if_failed";
|
||||
|
||||
// Set when message downloaded by platform (L+)
|
||||
private static final String KEY_RESULT_CODE = "result_code";
|
||||
private static final String KEY_HTTP_STATUS_CODE = "http_status_code";
|
||||
private static final String KEY_CONTENT_URI = "content_uri";
|
||||
private static final String KEY_SUB_ID = "sub_id";
|
||||
private static final String KEY_SUB_PHONE_NUMBER = "sub_phone_number";
|
||||
private static final String KEY_TRANSACTION_ID = "transaction_id";
|
||||
private static final String KEY_CONTENT_LOCATION = "content_location";
|
||||
private static final String KEY_AUTO_DOWNLOAD = "auto_download";
|
||||
private static final String KEY_RECEIVED_TIMESTAMP = "received_timestamp";
|
||||
private static final String KEY_EXPIRY = "expiry";
|
||||
|
||||
// Set when message downloaded by us (legacy)
|
||||
private static final String KEY_STATUS = "status";
|
||||
private static final String KEY_RAW_STATUS = "raw_status";
|
||||
private static final String KEY_MMS_URI = "mms_uri";
|
||||
|
||||
// Used to send a deferred response in response to auto-download failure
|
||||
private static final String KEY_SEND_DEFERRED_RESP_STATUS = "send_deferred_resp_status";
|
||||
|
||||
// Results passed from background worker to processCompletion
|
||||
private static final String BUNDLE_REQUEST_STATUS = "request_status";
|
||||
private static final String BUNDLE_RAW_TELEPHONY_STATUS = "raw_status";
|
||||
private static final String BUNDLE_MMS_URI = "mms_uri";
|
||||
|
||||
// This is called when MMS lib API returns via PendingIntent
|
||||
public static void processMessageDownloaded(final int resultCode, final Bundle extras) {
|
||||
final String messageId = extras.getString(DownloadMmsAction.EXTRA_MESSAGE_ID);
|
||||
final Uri contentUri = extras.getParcelable(DownloadMmsAction.EXTRA_CONTENT_URI, Uri.class);
|
||||
final Uri notificationUri = extras.getParcelable(DownloadMmsAction.EXTRA_NOTIFICATION_URI,
|
||||
Uri.class);
|
||||
final String conversationId = extras.getString(DownloadMmsAction.EXTRA_CONVERSATION_ID);
|
||||
final String participantId = extras.getString(DownloadMmsAction.EXTRA_PARTICIPANT_ID);
|
||||
Assert.notNull(messageId);
|
||||
Assert.notNull(contentUri);
|
||||
Assert.notNull(notificationUri);
|
||||
Assert.notNull(conversationId);
|
||||
Assert.notNull(participantId);
|
||||
|
||||
final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
|
||||
final Bundle params = action.actionParameters;
|
||||
params.putBoolean(KEY_DOWNLOADED_BY_PLATFORM, true);
|
||||
params.putString(KEY_MESSAGE_ID, messageId);
|
||||
params.putInt(KEY_RESULT_CODE, resultCode);
|
||||
params.putInt(KEY_HTTP_STATUS_CODE,
|
||||
extras.getInt(SmsManager.EXTRA_MMS_HTTP_STATUS, 0));
|
||||
params.putParcelable(KEY_CONTENT_URI, contentUri);
|
||||
params.putParcelable(KEY_NOTIFICATION_URI, notificationUri);
|
||||
params.putInt(KEY_SUB_ID,
|
||||
extras.getInt(DownloadMmsAction.EXTRA_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID));
|
||||
params.putString(KEY_SUB_PHONE_NUMBER,
|
||||
extras.getString(DownloadMmsAction.EXTRA_SUB_PHONE_NUMBER));
|
||||
params.putString(KEY_TRANSACTION_ID,
|
||||
extras.getString(DownloadMmsAction.EXTRA_TRANSACTION_ID));
|
||||
params.putString(KEY_CONTENT_LOCATION,
|
||||
extras.getString(DownloadMmsAction.EXTRA_CONTENT_LOCATION));
|
||||
params.putBoolean(KEY_AUTO_DOWNLOAD,
|
||||
extras.getBoolean(DownloadMmsAction.EXTRA_AUTO_DOWNLOAD));
|
||||
params.putLong(KEY_RECEIVED_TIMESTAMP,
|
||||
extras.getLong(DownloadMmsAction.EXTRA_RECEIVED_TIMESTAMP));
|
||||
params.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
params.putString(KEY_PARTICIPANT_ID, participantId);
|
||||
params.putInt(KEY_STATUS_IF_FAILED,
|
||||
extras.getInt(DownloadMmsAction.EXTRA_STATUS_IF_FAILED));
|
||||
params.putLong(KEY_EXPIRY, extras.getLong(DownloadMmsAction.EXTRA_EXPIRY));
|
||||
action.start();
|
||||
}
|
||||
|
||||
// This is called for fast failing downloading (due to airplane mode or mobile data )
|
||||
public static void processMessageDownloadFastFailed(final String messageId,
|
||||
final Uri notificationUri, final String conversationId, final String participantId,
|
||||
final String contentLocation, final int subId, final String subPhoneNumber,
|
||||
final int statusIfFailed, final boolean autoDownload, final String transactionId,
|
||||
final int resultCode) {
|
||||
Assert.notNull(messageId);
|
||||
Assert.notNull(notificationUri);
|
||||
Assert.notNull(conversationId);
|
||||
Assert.notNull(participantId);
|
||||
|
||||
final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
|
||||
final Bundle params = action.actionParameters;
|
||||
params.putBoolean(KEY_DOWNLOADED_BY_PLATFORM, true);
|
||||
params.putString(KEY_MESSAGE_ID, messageId);
|
||||
params.putInt(KEY_RESULT_CODE, resultCode);
|
||||
params.putParcelable(KEY_NOTIFICATION_URI, notificationUri);
|
||||
params.putInt(KEY_SUB_ID, subId);
|
||||
params.putString(KEY_SUB_PHONE_NUMBER, subPhoneNumber);
|
||||
params.putString(KEY_CONTENT_LOCATION, contentLocation);
|
||||
params.putBoolean(KEY_AUTO_DOWNLOAD, autoDownload);
|
||||
params.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
params.putString(KEY_PARTICIPANT_ID, participantId);
|
||||
params.putInt(KEY_STATUS_IF_FAILED, statusIfFailed);
|
||||
params.putString(KEY_TRANSACTION_ID, transactionId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
public static void processDownloadActionFailure(final String messageId, final int status,
|
||||
final int rawStatus, final String conversationId, final String participantId,
|
||||
final int statusIfFailed, final int subId, final String transactionId) {
|
||||
Assert.notNull(messageId);
|
||||
Assert.notNull(conversationId);
|
||||
Assert.notNull(participantId);
|
||||
|
||||
final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
|
||||
final Bundle params = action.actionParameters;
|
||||
params.putBoolean(KEY_DOWNLOADED_BY_PLATFORM, false);
|
||||
params.putString(KEY_MESSAGE_ID, messageId);
|
||||
params.putInt(KEY_STATUS, status);
|
||||
params.putInt(KEY_RAW_STATUS, rawStatus);
|
||||
params.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
params.putString(KEY_PARTICIPANT_ID, participantId);
|
||||
params.putInt(KEY_STATUS_IF_FAILED, statusIfFailed);
|
||||
params.putInt(KEY_SUB_ID, subId);
|
||||
params.putString(KEY_TRANSACTION_ID, transactionId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
public static void sendDeferredRespStatus(final String messageId, final String transactionId,
|
||||
final String contentLocation, final int subId) {
|
||||
final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
|
||||
final Bundle params = action.actionParameters;
|
||||
params.putString(KEY_MESSAGE_ID, messageId);
|
||||
params.putString(KEY_TRANSACTION_ID, transactionId);
|
||||
params.putString(KEY_CONTENT_LOCATION, contentLocation);
|
||||
params.putBoolean(KEY_SEND_DEFERRED_RESP_STATUS, true);
|
||||
params.putInt(KEY_SUB_ID, subId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private ProcessDownloadedMmsAction() {
|
||||
// Callers must use one of the static methods above
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
// Fire up the background worker
|
||||
requestBackgroundWork();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bundle doBackgroundWork() {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
|
||||
final String contentLocation = actionParameters.getString(KEY_CONTENT_LOCATION);
|
||||
final boolean sendDeferredRespStatus =
|
||||
actionParameters.getBoolean(KEY_SEND_DEFERRED_RESP_STATUS, false);
|
||||
|
||||
// Send a response indicating that auto-download failed
|
||||
if (sendDeferredRespStatus) {
|
||||
LogUtil.v(TAG, "DownloadMmsAction: Auto-download of message " + messageId
|
||||
+ " failed; sending DEFERRED NotifyRespInd");
|
||||
MmsUtils.sendNotifyResponseForMmsDownload(
|
||||
context,
|
||||
subId,
|
||||
MmsUtils.stringToBytes(transactionId, "UTF-8"),
|
||||
contentLocation,
|
||||
PduHeaders.STATUS_DEFERRED);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Processing a real MMS download
|
||||
final boolean downloadedByPlatform = actionParameters.getBoolean(
|
||||
KEY_DOWNLOADED_BY_PLATFORM);
|
||||
|
||||
final int status;
|
||||
int rawStatus = MmsUtils.PDU_HEADER_VALUE_UNDEFINED;
|
||||
Uri mmsUri = null;
|
||||
|
||||
if (downloadedByPlatform) {
|
||||
final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
final Uri contentUri = actionParameters.getParcelable(KEY_CONTENT_URI, Uri.class);
|
||||
final File downloadedFile = MmsFileProvider.getFile(contentUri);
|
||||
byte[] downloadedData = null;
|
||||
try {
|
||||
downloadedData = Files.toByteArray(downloadedFile);
|
||||
} catch (final FileNotFoundException e) {
|
||||
LogUtil.e(TAG, "ProcessDownloadedMmsAction: MMS download file not found: "
|
||||
+ downloadedFile.getAbsolutePath());
|
||||
} catch (final IOException e) {
|
||||
LogUtil.e(TAG, "ProcessDownloadedMmsAction: Error reading MMS download file: "
|
||||
+ downloadedFile.getAbsolutePath(), e);
|
||||
}
|
||||
|
||||
// Can delete the temp file now
|
||||
if (downloadedFile.exists()) {
|
||||
downloadedFile.delete();
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "ProcessDownloadedMmsAction: Deleted temp file with "
|
||||
+ "downloaded MMS pdu: " + downloadedFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadedData != null) {
|
||||
final RetrieveConf retrieveConf =
|
||||
MmsSender.parseRetrieveConf(downloadedData, subId);
|
||||
if (retrieveConf != null) {
|
||||
// Insert the downloaded MMS into telephony
|
||||
final Uri notificationUri = actionParameters.getParcelable(
|
||||
KEY_NOTIFICATION_URI, Uri.class);
|
||||
final String subPhoneNumber = actionParameters.getString(
|
||||
KEY_SUB_PHONE_NUMBER);
|
||||
final boolean autoDownload = actionParameters.getBoolean(
|
||||
KEY_AUTO_DOWNLOAD);
|
||||
final long receivedTimestampInSeconds =
|
||||
actionParameters.getLong(KEY_RECEIVED_TIMESTAMP);
|
||||
final long expiry = actionParameters.getLong(KEY_EXPIRY);
|
||||
|
||||
// Inform sync we're adding a message to telephony
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(receivedTimestampInSeconds * 1000L);
|
||||
|
||||
final MmsUtils.StatusPlusUri result =
|
||||
MmsUtils.insertDownloadedMessageAndSendResponse(context,
|
||||
notificationUri, subId, subPhoneNumber, transactionId,
|
||||
contentLocation, autoDownload, receivedTimestampInSeconds,
|
||||
expiry, retrieveConf);
|
||||
status = result.status;
|
||||
rawStatus = result.rawStatus;
|
||||
mmsUri = result.uri;
|
||||
} else {
|
||||
// Invalid response PDU
|
||||
status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
|
||||
}
|
||||
} else {
|
||||
// Failed to read download file
|
||||
status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(TAG, "ProcessDownloadedMmsAction: Platform returned error resultCode: "
|
||||
+ resultCode);
|
||||
final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
|
||||
status = MmsSender.getErrorResultStatus(resultCode, httpStatusCode);
|
||||
}
|
||||
} else {
|
||||
// Message was already processed by the internal API, or the download action failed.
|
||||
// In either case, we just need to copy the status to the response bundle.
|
||||
status = actionParameters.getInt(KEY_STATUS);
|
||||
rawStatus = actionParameters.getInt(KEY_RAW_STATUS);
|
||||
mmsUri = actionParameters.getParcelable(KEY_MMS_URI, Uri.class);
|
||||
}
|
||||
|
||||
final Bundle response = new Bundle();
|
||||
response.putInt(BUNDLE_REQUEST_STATUS, status);
|
||||
response.putInt(BUNDLE_RAW_TELEPHONY_STATUS, rawStatus);
|
||||
response.putParcelable(BUNDLE_MMS_URI, mmsUri);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object processBackgroundResponse(final Bundle response) {
|
||||
if (response == null) {
|
||||
// No message download to process; doBackgroundWork sent a notify deferred response
|
||||
Assert.isTrue(actionParameters.getBoolean(KEY_SEND_DEFERRED_RESP_STATUS));
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(
|
||||
true /* failed */, this);
|
||||
return null;
|
||||
}
|
||||
|
||||
final int status = response.getInt(BUNDLE_REQUEST_STATUS);
|
||||
final int rawStatus = response.getInt(BUNDLE_RAW_TELEPHONY_STATUS);
|
||||
final Uri messageUri = response.getParcelable(BUNDLE_MMS_URI, Uri.class);
|
||||
final boolean autoDownload = actionParameters.getBoolean(KEY_AUTO_DOWNLOAD);
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
|
||||
// Do post-processing on downloaded message
|
||||
final MessageData message = processResult(status, rawStatus, messageUri);
|
||||
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
// If we were trying to auto-download but have failed need to send the deferred response
|
||||
final boolean needToSendDeferredResp =
|
||||
autoDownload && (status == MmsUtils.MMS_REQUEST_MANUAL_RETRY);
|
||||
if (needToSendDeferredResp) {
|
||||
final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
|
||||
final String contentLocation = actionParameters.getString(KEY_CONTENT_LOCATION);
|
||||
sendDeferredRespStatus(messageId, transactionId, contentLocation, subId);
|
||||
}
|
||||
|
||||
if (autoDownload) {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
MessageData toastMessage = message;
|
||||
if (toastMessage == null) {
|
||||
// If the downloaded failed (message is null), then we should announce the
|
||||
// receiving of the wap push message. Load the wap push message here instead.
|
||||
toastMessage = BugleDatabaseOperations.readMessageData(db, messageId);
|
||||
}
|
||||
if (toastMessage != null) {
|
||||
final ParticipantData sender = ParticipantData.getFromId(
|
||||
db, toastMessage.getParticipantId());
|
||||
BugleActionToasts.onMessageReceived(
|
||||
toastMessage.getConversationId(), sender, toastMessage);
|
||||
}
|
||||
} else {
|
||||
final boolean success = message != null && status == MmsUtils.MMS_REQUEST_SUCCEEDED;
|
||||
BugleActionToasts.onSendMessageOrManualDownloadActionCompleted(
|
||||
// If download failed, use the wap push message's conversation instead
|
||||
success ? message.getConversationId()
|
||||
: actionParameters.getString(KEY_CONVERSATION_ID),
|
||||
success, status, false/*isSms*/, subId, false /*isSend*/);
|
||||
}
|
||||
|
||||
final boolean failed = (messageUri == null);
|
||||
// Scheduling pending messages. If auto downloading is failed and it needs to send the
|
||||
// deferred response, Skip it here and it will be scheduled after sending the response.
|
||||
if (!needToSendDeferredResp) {
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(failed, this);
|
||||
}
|
||||
if (failed) {
|
||||
BugleNotifications.update(false, BugleNotifications.UPDATE_ERRORS);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object processBackgroundFailure() {
|
||||
if (actionParameters.getBoolean(KEY_SEND_DEFERRED_RESP_STATUS)) {
|
||||
// We can early-out for these failures. processResult is only designed to handle
|
||||
// post-processing of MMS downloads (whether successful or not).
|
||||
LogUtil.w(TAG,
|
||||
"ProcessDownloadedMmsAction: Exception while sending deferred NotifyRespInd");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Background worker threw an exception; require manual retry
|
||||
processResult(MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED,
|
||||
null /* mmsUri */);
|
||||
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(true /* failed */,
|
||||
this);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private MessageData processResult(final int status, final int rawStatus, final Uri mmsUri) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
final Uri mmsNotificationUri = actionParameters.getParcelable(KEY_NOTIFICATION_URI,
|
||||
Uri.class);
|
||||
final String notificationConversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final String notificationParticipantId = actionParameters.getString(KEY_PARTICIPANT_ID);
|
||||
final int statusIfFailed = actionParameters.getInt(KEY_STATUS_IF_FAILED);
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
|
||||
Assert.notNull(messageId);
|
||||
|
||||
LogUtil.i(TAG, "ProcessDownloadedMmsAction: Processed MMS download of message " + messageId
|
||||
+ "; status is " + MmsUtils.getRequestStatusDescription(status));
|
||||
|
||||
DatabaseMessages.MmsMessage mms = null;
|
||||
if (status == MmsUtils.MMS_REQUEST_SUCCEEDED && mmsUri != null) {
|
||||
// Delete the initial M-Notification.ind from telephony
|
||||
SqliteWrapper.delete(context, context.getContentResolver(),
|
||||
mmsNotificationUri, null, null);
|
||||
|
||||
// Read the sent MMS from the telephony provider
|
||||
mms = MmsUtils.loadMms(mmsUri);
|
||||
}
|
||||
|
||||
boolean messageInFocusedConversation;
|
||||
boolean messageInObservableConversation;
|
||||
String conversationId = null;
|
||||
MessageData message = null;
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
if (mms != null) {
|
||||
final ParticipantData self = ParticipantData.getSelfParticipant(mms.getSubId());
|
||||
final String selfId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
|
||||
|
||||
final List<String> recipients = MmsUtils.getRecipientsByThread(mms.mThreadId);
|
||||
String from = MmsUtils.getMmsSender(recipients, mms.getUri());
|
||||
if (from == null) {
|
||||
LogUtil.w(TAG,
|
||||
"Downloaded an MMS without sender address; using unknown sender.");
|
||||
from = ParticipantData.getUnknownSenderDestination();
|
||||
}
|
||||
final ParticipantData sender = ParticipantData.getFromRawPhoneBySimLocale(from,
|
||||
subId);
|
||||
final String senderParticipantId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, sender);
|
||||
if (!senderParticipantId.equals(notificationParticipantId)) {
|
||||
LogUtil.e(TAG, "ProcessDownloadedMmsAction: Downloaded MMS message "
|
||||
+ messageId + " has different sender (participantId = "
|
||||
+ senderParticipantId + ") than notification ("
|
||||
+ notificationParticipantId + ")");
|
||||
}
|
||||
final boolean blockedSender = BugleDatabaseOperations.isBlockedDestination(
|
||||
db, sender.getNormalizedDestination());
|
||||
conversationId = BugleDatabaseOperations.getOrCreateConversationFromThreadId(db,
|
||||
mms.mThreadId, blockedSender, subId);
|
||||
|
||||
messageInFocusedConversation =
|
||||
DataModel.get().isFocusedConversation(conversationId);
|
||||
messageInObservableConversation =
|
||||
DataModel.get().isNewMessageObservable(conversationId);
|
||||
|
||||
// TODO: Also write these values to the telephony provider
|
||||
mms.mRead = messageInFocusedConversation;
|
||||
mms.mSeen = messageInObservableConversation;
|
||||
|
||||
// Translate to our format
|
||||
message = MmsUtils.createMmsMessage(mms, conversationId, senderParticipantId,
|
||||
selfId, MessageData.BUGLE_STATUS_INCOMING_COMPLETE);
|
||||
// Update image sizes.
|
||||
message.updateSizesForImageParts();
|
||||
// Inform sync that message has been added at local received timestamp
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(message.getReceivedTimeStamp());
|
||||
final MessageData current = BugleDatabaseOperations.readMessageData(db, messageId);
|
||||
if (current == null) {
|
||||
LogUtil.w(TAG, "Message deleted prior to update");
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
} else {
|
||||
// Overwrite existing notification message
|
||||
message.updateMessageId(messageId);
|
||||
// Write message
|
||||
BugleDatabaseOperations.updateMessageInTransaction(db, message);
|
||||
}
|
||||
|
||||
if (!TextUtils.equals(notificationConversationId, conversationId)) {
|
||||
// If this is a group conversation, the message is moved. So the original
|
||||
// 1v1 conversation (as referenced by notificationConversationId) could
|
||||
// be left with no non-draft message. Delete the conversation if that
|
||||
// happens. See the comment for the method below for why we need to do this.
|
||||
if (!BugleDatabaseOperations.deleteConversationIfEmptyInTransaction(
|
||||
db, notificationConversationId)) {
|
||||
BugleDatabaseOperations.maybeRefreshConversationMetadataInTransaction(
|
||||
db, notificationConversationId, messageId,
|
||||
true /*shouldAutoSwitchSelfId*/, blockedSender /*keepArchived*/);
|
||||
}
|
||||
}
|
||||
|
||||
BugleDatabaseOperations.refreshConversationMetadataInTransaction(db, conversationId,
|
||||
true /*shouldAutoSwitchSelfId*/, blockedSender /*keepArchived*/);
|
||||
} else {
|
||||
messageInFocusedConversation =
|
||||
DataModel.get().isFocusedConversation(notificationConversationId);
|
||||
|
||||
// Default to retry status unless status indicates otherwise
|
||||
int bugleStatus = statusIfFailed;
|
||||
if (status == MmsUtils.MMS_REQUEST_MANUAL_RETRY) {
|
||||
bugleStatus = MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED;
|
||||
} else if (status == MmsUtils.MMS_REQUEST_NO_RETRY) {
|
||||
bugleStatus = MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE;
|
||||
}
|
||||
DownloadMmsAction.updateMessageStatus(mmsNotificationUri, messageId,
|
||||
notificationConversationId, bugleStatus, rawStatus);
|
||||
|
||||
// Log MMS download failed
|
||||
final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
|
||||
final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
|
||||
|
||||
// Just in case this was the latest message update the summary data
|
||||
BugleDatabaseOperations.refreshConversationMetadataInTransaction(db,
|
||||
notificationConversationId, true /*shouldAutoSwitchSelfId*/,
|
||||
false /*keepArchived*/);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
if (mmsUri != null) {
|
||||
// Update mms table with read status now we know the conversation id
|
||||
final ContentValues values = new ContentValues(1);
|
||||
values.put(Mms.READ, messageInFocusedConversation);
|
||||
SqliteWrapper.update(context, context.getContentResolver(), mmsUri, values,
|
||||
null, null);
|
||||
}
|
||||
|
||||
// Show a notification to let the user know a new message has arrived
|
||||
BugleNotifications.update(false /*silent*/, conversationId, BugleNotifications.UPDATE_ALL);
|
||||
|
||||
// Messages may have changed in two conversations
|
||||
if (conversationId != null) {
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
}
|
||||
MessagingContentProvider.notifyMessagesChanged(notificationConversationId);
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private ProcessDownloadedMmsAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ProcessDownloadedMmsAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public ProcessDownloadedMmsAction createFromParcel(final Parcel in) {
|
||||
return new ProcessDownloadedMmsAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProcessDownloadedMmsAction[] newArray(final int size) {
|
||||
return new ProcessDownloadedMmsAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
+465
@@ -0,0 +1,465 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 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.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.telephony.ServiceState;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DataModelImpl;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.util.BugleGservicesKeys;
|
||||
import com.android.messaging.util.BuglePrefs;
|
||||
import com.android.messaging.util.BuglePrefsKeys;
|
||||
import com.android.messaging.util.ConnectivityUtil;
|
||||
import com.android.messaging.util.ConnectivityUtil.ConnectivityListener;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
|
||||
/**
|
||||
* Action used to lookup any messages in the pending send/download state and either fail them or
|
||||
* retry their action based on subscriptions. This action only initiates one retry at a time for
|
||||
* both sending/downloading. Further retries should be triggered by successful sending/downloading
|
||||
* of a message, network status change or exponential backoff timer.
|
||||
*/
|
||||
public class ProcessPendingMessagesAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
// PENDING_INTENT_BASE_REQUEST_CODE + subId(-1 for pre-L_MR1) is used per subscription uniquely.
|
||||
private static final int PENDING_INTENT_BASE_REQUEST_CODE = 103;
|
||||
|
||||
private static final String KEY_SUB_ID = "sub_id";
|
||||
|
||||
public static void processFirstPendingMessage() {
|
||||
PhoneUtils.forEachActiveSubscription(subId -> {
|
||||
// Clear any pending alarms or connectivity events
|
||||
unregister(subId);
|
||||
// Clear retry count
|
||||
setRetry(0, subId);
|
||||
// Start action
|
||||
final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
|
||||
action.actionParameters.putInt(KEY_SUB_ID, subId);
|
||||
action.start();
|
||||
});
|
||||
}
|
||||
|
||||
public static void scheduleProcessPendingMessagesAction(final boolean failed,
|
||||
final Action processingAction) {
|
||||
final int subId = processingAction.actionParameters
|
||||
.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
LogUtil.i(TAG, "ProcessPendingMessagesAction: Scheduling pending messages"
|
||||
+ (failed ? "(message failed)" : "") + " for subId " + subId);
|
||||
// Can safely clear any pending alarms or connectivity events as either an action
|
||||
// is currently running or we will run now or register if pending actions possible.
|
||||
unregister(subId);
|
||||
|
||||
final boolean isDefaultSmsApp = PhoneUtils.getDefault().isDefaultSmsApp();
|
||||
boolean scheduleAlarm = false;
|
||||
// If message succeeded and if Bugle is default SMS app just carry on with next message
|
||||
if (!failed && isDefaultSmsApp) {
|
||||
// Clear retry attempt count as something just succeeded
|
||||
setRetry(0, subId);
|
||||
|
||||
// Lookup and queue next message for each sending/downloading for immediate processing
|
||||
// by background worker. If there are no pending messages, this will do nothing and
|
||||
// return true.
|
||||
final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
|
||||
if (action.queueActions(processingAction)) {
|
||||
if (processingAction.hasBackgroundActions()) {
|
||||
LogUtil.v(TAG, "ProcessPendingMessagesAction: Action queued");
|
||||
} else {
|
||||
LogUtil.v(TAG, "ProcessPendingMessagesAction: No actions to queue");
|
||||
}
|
||||
// Have queued next action if needed, nothing more to do
|
||||
return;
|
||||
}
|
||||
// In case of error queuing schedule a retry
|
||||
scheduleAlarm = true;
|
||||
LogUtil.w(TAG, "ProcessPendingMessagesAction: Action failed to queue; retrying");
|
||||
}
|
||||
if (getHavePendingMessages(subId) || scheduleAlarm) {
|
||||
// Still have a pending message that needs to be queued for processing
|
||||
final ConnectivityListener listener = serviceState -> {
|
||||
if (serviceState == ServiceState.STATE_IN_SERVICE) {
|
||||
LogUtil.i(TAG, "ProcessPendingMessagesAction: Now connected for subId "
|
||||
+ subId + ", starting action");
|
||||
|
||||
// Clear any pending alarms or connectivity events but leave attempt count
|
||||
// alone
|
||||
unregister(subId);
|
||||
|
||||
// Start action
|
||||
final ProcessPendingMessagesAction action =
|
||||
new ProcessPendingMessagesAction();
|
||||
action.actionParameters.putInt(KEY_SUB_ID, subId);
|
||||
action.start();
|
||||
}
|
||||
};
|
||||
// Read and increment attempt number from shared prefs
|
||||
final int retryAttempt = getNextRetry(subId);
|
||||
register(listener, retryAttempt, subId);
|
||||
} else {
|
||||
// No more pending messages (presumably the message that failed has expired) or it
|
||||
// may be possible that a send and a download are already in process.
|
||||
// Clear retry attempt count.
|
||||
// TODO Might be premature if send and download in process...
|
||||
// but worst case means we try to send a bit more often.
|
||||
setRetry(0, subId);
|
||||
LogUtil.i(TAG, "ProcessPendingMessagesAction: No more pending messages");
|
||||
}
|
||||
}
|
||||
|
||||
private static void register(final ConnectivityListener listener, final int retryAttempt,
|
||||
int subId) {
|
||||
int retryNumber = retryAttempt;
|
||||
|
||||
// Register to be notified about connectivity changes
|
||||
ConnectivityUtil connectivityUtil = DataModelImpl.getConnectivityUtil(subId);
|
||||
if (connectivityUtil != null) {
|
||||
connectivityUtil.register(listener);
|
||||
}
|
||||
|
||||
final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
|
||||
action.actionParameters.putInt(KEY_SUB_ID, subId);
|
||||
final long initialBackoffMs = BugleGservicesKeys.INITIAL_MESSAGE_RESEND_DELAY_MS_DEFAULT;
|
||||
final long maxDelayMs = BugleGservicesKeys.MAX_MESSAGE_RESEND_DELAY_MS_DEFAULT;
|
||||
long delayMs;
|
||||
long nextDelayMs = initialBackoffMs;
|
||||
do {
|
||||
delayMs = nextDelayMs;
|
||||
retryNumber--;
|
||||
nextDelayMs = delayMs * 2;
|
||||
}
|
||||
while (retryNumber > 0 && nextDelayMs < maxDelayMs);
|
||||
|
||||
LogUtil.i(TAG, "ProcessPendingMessagesAction: Registering for retry #" + retryAttempt
|
||||
+ " in " + delayMs + " ms for subId " + subId);
|
||||
|
||||
action.schedule(PENDING_INTENT_BASE_REQUEST_CODE + subId, delayMs);
|
||||
}
|
||||
|
||||
private static void unregister(final int subId) {
|
||||
// Clear any pending alarms or connectivity events
|
||||
ConnectivityUtil connectivityUtil = DataModelImpl.getConnectivityUtil(subId);
|
||||
if (connectivityUtil != null) {
|
||||
connectivityUtil.unregister();
|
||||
}
|
||||
|
||||
final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
|
||||
action.schedule(PENDING_INTENT_BASE_REQUEST_CODE + subId, Long.MAX_VALUE);
|
||||
|
||||
LogUtil.v(TAG, "ProcessPendingMessagesAction: Unregistering for connectivity changed "
|
||||
+ "events and clearing scheduled alarm for subId " + subId);
|
||||
}
|
||||
|
||||
private static void setRetry(final int retryAttempt, int subId) {
|
||||
final BuglePrefs prefs = Factory.get().getSubscriptionPrefs(subId);
|
||||
prefs.putInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, retryAttempt);
|
||||
}
|
||||
|
||||
private static int getNextRetry(int subId) {
|
||||
final BuglePrefs prefs = Factory.get().getSubscriptionPrefs(subId);
|
||||
final int retryAttempt =
|
||||
prefs.getInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, 0) + 1;
|
||||
prefs.putInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, retryAttempt);
|
||||
return retryAttempt;
|
||||
}
|
||||
|
||||
private ProcessPendingMessagesAction() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from the DB and determine if there are any messages we should process
|
||||
*
|
||||
* @param subId the subId
|
||||
* @return true if we have pending messages
|
||||
*/
|
||||
private static boolean getHavePendingMessages(final int subId) {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final long now = System.currentTimeMillis();
|
||||
final String selfId = ParticipantData.getParticipantId(db, subId);
|
||||
if (selfId == null) {
|
||||
// This could be happened before refreshing participant.
|
||||
LogUtil.w(TAG, "ProcessPendingMessagesAction: selfId is null for subId " + subId);
|
||||
return false;
|
||||
}
|
||||
|
||||
final String toSendMessageId = findNextMessageToSend(db, now, selfId);
|
||||
if (toSendMessageId != null) {
|
||||
return true;
|
||||
} else {
|
||||
final String toDownloadMessageId = findNextMessageToDownload(db, now, selfId);
|
||||
if (toDownloadMessageId != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Messages may be in the process of sending/downloading even when there are no pending
|
||||
// messages...
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue any pending actions
|
||||
*
|
||||
* @return true if action queued (or no actions to queue) else false
|
||||
*/
|
||||
private boolean queueActions(final Action processingAction) {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final long now = System.currentTimeMillis();
|
||||
boolean succeeded = true;
|
||||
final int subId = processingAction.actionParameters
|
||||
.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
|
||||
LogUtil.i(TAG, "ProcessPendingMessagesAction: Start queueing for subId " + subId);
|
||||
|
||||
final String selfId = ParticipantData.getParticipantId(db, subId);
|
||||
if (selfId == null) {
|
||||
// This could be happened before refreshing participant.
|
||||
LogUtil.w(TAG, "ProcessPendingMessagesAction: selfId is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Will queue no more than one message to send plus one message to download
|
||||
// This keeps outgoing messages "in order" but allow downloads to happen even if sending
|
||||
// gets blocked until messages time out. Manual resend bumps messages to head of queue.
|
||||
final String toSendMessageId = findNextMessageToSend(db, now, selfId);
|
||||
final String toDownloadMessageId = findNextMessageToDownload(db, now, selfId);
|
||||
if (toSendMessageId != null) {
|
||||
LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toSendMessageId
|
||||
+ " for sending");
|
||||
// This could queue nothing
|
||||
if (!SendMessageAction.queueForSendInBackground(toSendMessageId, processingAction)) {
|
||||
LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message "
|
||||
+ toSendMessageId + " for sending");
|
||||
succeeded = false;
|
||||
}
|
||||
}
|
||||
if (toDownloadMessageId != null) {
|
||||
LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toDownloadMessageId
|
||||
+ " for download");
|
||||
// This could queue nothing
|
||||
if (!DownloadMmsAction.queueMmsForDownloadInBackground(toDownloadMessageId,
|
||||
processingAction)) {
|
||||
LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message "
|
||||
+ toDownloadMessageId + " for download");
|
||||
succeeded = false;
|
||||
}
|
||||
}
|
||||
if (toSendMessageId == null && toDownloadMessageId == null) {
|
||||
LogUtil.i(TAG, "ProcessPendingMessagesAction: No messages to send or download");
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
// If triggered by alarm will not have unregistered yet
|
||||
unregister(subId);
|
||||
|
||||
if (PhoneUtils.getDefault().isDefaultSmsApp()) {
|
||||
if (!queueActions(this)) {
|
||||
LogUtil.v(TAG, "ProcessPendingMessagesAction: rescheduling");
|
||||
// TODO: Need to clear retry count here?
|
||||
scheduleProcessPendingMessagesAction(true /* failed */, this);
|
||||
}
|
||||
} else {
|
||||
LogUtil.v(TAG, "ProcessPendingMessagesAction: Not default SMS app; rescheduling");
|
||||
scheduleProcessPendingMessagesAction(true /* failed */, this);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String findNextMessageToSend(final DatabaseWrapper db, final long now,
|
||||
final String selfId) {
|
||||
String toSendMessageId = null;
|
||||
Cursor cursor = null;
|
||||
int sendingCnt;
|
||||
int pendingCnt;
|
||||
int failedCnt = 0;
|
||||
db.beginTransaction();
|
||||
try {
|
||||
// First check to see if we have any messages already sending
|
||||
sendingCnt = (int) db.queryNumEntries(DatabaseHelper.MESSAGES_TABLE,
|
||||
DatabaseHelper.MessageColumns.STATUS + " IN (?, ?) AND "
|
||||
+ DatabaseHelper.MessageColumns.SELF_PARTICIPANT_ID + " =? ",
|
||||
new String[] {
|
||||
Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_SENDING),
|
||||
Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_RESENDING),
|
||||
selfId}
|
||||
);
|
||||
|
||||
// Look for messages we could send
|
||||
cursor = db.query(DatabaseHelper.MESSAGES_TABLE,
|
||||
MessageData.getProjection(),
|
||||
DatabaseHelper.MessageColumns.STATUS + " IN (?, ?) AND "
|
||||
+ DatabaseHelper.MessageColumns.SELF_PARTICIPANT_ID + " =? ",
|
||||
new String[] {
|
||||
Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND),
|
||||
Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY),
|
||||
selfId
|
||||
},
|
||||
null,
|
||||
null,
|
||||
DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC");
|
||||
pendingCnt = cursor.getCount();
|
||||
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(DatabaseHelper.MessageColumns.STATUS,
|
||||
MessageData.BUGLE_STATUS_OUTGOING_FAILED);
|
||||
|
||||
// Prior to L_MR1, isActiveSubscription is true always
|
||||
boolean isActiveSubscription = true;
|
||||
final ParticipantData messageSelf =
|
||||
BugleDatabaseOperations.getExistingParticipant(db, selfId);
|
||||
if (messageSelf == null || !messageSelf.isActiveSubscription()) {
|
||||
isActiveSubscription = false;
|
||||
}
|
||||
while (cursor.moveToNext()) {
|
||||
final MessageData message = new MessageData();
|
||||
message.bind(cursor);
|
||||
|
||||
// Mark this message as failed if the message's self is inactive or the message is
|
||||
// outside of resend window
|
||||
if (!isActiveSubscription || !message.getInResendWindow(now)) {
|
||||
failedCnt++;
|
||||
|
||||
// Mark message as failed
|
||||
BugleDatabaseOperations.updateMessageRow(db, message.getMessageId(), values);
|
||||
MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
|
||||
} else {
|
||||
// If no messages currently sending
|
||||
if (sendingCnt == 0) {
|
||||
// Send this message
|
||||
toSendMessageId = message.getMessageId();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "ProcessPendingMessagesAction: "
|
||||
+ sendingCnt + " messages already sending, "
|
||||
+ pendingCnt + " messages to send, "
|
||||
+ failedCnt + " failed messages");
|
||||
}
|
||||
|
||||
return toSendMessageId;
|
||||
}
|
||||
|
||||
private static String findNextMessageToDownload(final DatabaseWrapper db, final long now,
|
||||
final String selfId) {
|
||||
String toDownloadMessageId = null;
|
||||
Cursor cursor = null;
|
||||
int downloadingCnt;
|
||||
int pendingCnt;
|
||||
db.beginTransaction();
|
||||
try {
|
||||
// First check if we have any messages already downloading
|
||||
downloadingCnt = (int) db.queryNumEntries(DatabaseHelper.MESSAGES_TABLE,
|
||||
DatabaseHelper.MessageColumns.STATUS + " IN (?, ?) AND "
|
||||
+ DatabaseHelper.MessageColumns.SELF_PARTICIPANT_ID + " =?",
|
||||
new String[] {
|
||||
Integer.toString(MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING),
|
||||
Integer.toString(MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING),
|
||||
selfId
|
||||
});
|
||||
|
||||
// TODO: This query is not actually needed if downloadingCnt == 0.
|
||||
cursor = db.query(DatabaseHelper.MESSAGES_TABLE,
|
||||
MessageData.getProjection(),
|
||||
DatabaseHelper.MessageColumns.STATUS + " IN (?, ?) AND "
|
||||
+ DatabaseHelper.MessageColumns.SELF_PARTICIPANT_ID + " =? ",
|
||||
new String[]{
|
||||
Integer.toString(MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD),
|
||||
Integer.toString(
|
||||
MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD),
|
||||
selfId
|
||||
},
|
||||
null,
|
||||
null,
|
||||
DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC");
|
||||
|
||||
pendingCnt = cursor.getCount();
|
||||
|
||||
// If no messages are currently downloading and there is a download pending,
|
||||
// queue the download of the oldest pending message.
|
||||
if (downloadingCnt == 0 && cursor.moveToNext()) {
|
||||
// Always start the next pending message. We will check if a download has
|
||||
// expired in DownloadMmsAction and mark message failed there.
|
||||
final MessageData message = new MessageData();
|
||||
message.bind(cursor);
|
||||
toDownloadMessageId = message.getMessageId();
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "ProcessPendingMessagesAction: "
|
||||
+ downloadingCnt + " messages already downloading, "
|
||||
+ pendingCnt + " messages to download");
|
||||
}
|
||||
|
||||
return toDownloadMessageId;
|
||||
}
|
||||
|
||||
private ProcessPendingMessagesAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ProcessPendingMessagesAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public ProcessPendingMessagesAction createFromParcel(final Parcel in) {
|
||||
return new ProcessPendingMessagesAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProcessPendingMessagesAction[] newArray(final int size) {
|
||||
return new ProcessPendingMessagesAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 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.action;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.v7.mms.pdu.SendConf;
|
||||
import android.telephony.PhoneNumberUtils;
|
||||
import android.telephony.SmsManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MmsFileProvider;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.MessagePartData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsConfig;
|
||||
import com.android.messaging.sms.MmsSender;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Update message status to reflect success or failure
|
||||
* Can also update the message itself if a "final" message is now available from telephony db
|
||||
*/
|
||||
public class ProcessSentMessageAction extends Action {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
// These are always set
|
||||
private static final String KEY_SMS = "is_sms";
|
||||
private static final String KEY_SENT_BY_PLATFORM = "sent_by_platform";
|
||||
|
||||
// These are set when we're processing a message sent by the user. They are null for messages
|
||||
// sent automatically (e.g. a NotifyRespInd/AcknowledgeInd sent in response to a download).
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_MESSAGE_URI = "message_uri";
|
||||
private static final String KEY_UPDATED_MESSAGE_URI = "updated_message_uri";
|
||||
private static final String KEY_SUB_ID = "sub_id";
|
||||
|
||||
// These are set for messages sent by the platform (L+)
|
||||
public static final String KEY_RESULT_CODE = "result_code";
|
||||
public static final String KEY_HTTP_STATUS_CODE = "http_status_code";
|
||||
private static final String KEY_CONTENT_URI = "content_uri";
|
||||
private static final String KEY_RESPONSE = "response";
|
||||
private static final String KEY_RESPONSE_IMPORTANT = "response_important";
|
||||
|
||||
// These are set for messages we sent ourself (legacy), or which we fast-failed before sending.
|
||||
private static final String KEY_STATUS = "status";
|
||||
private static final String KEY_RAW_STATUS = "raw_status";
|
||||
|
||||
// This is called when MMS lib API returns via PendingIntent
|
||||
public static void processMmsSent(final int resultCode, final Uri messageUri,
|
||||
final Bundle extras) {
|
||||
final ProcessSentMessageAction action = new ProcessSentMessageAction();
|
||||
final Bundle params = action.actionParameters;
|
||||
params.putBoolean(KEY_SMS, false);
|
||||
params.putBoolean(KEY_SENT_BY_PLATFORM, true);
|
||||
params.putString(KEY_MESSAGE_ID, extras.getString(SendMessageAction.EXTRA_MESSAGE_ID));
|
||||
params.putParcelable(KEY_MESSAGE_URI, messageUri);
|
||||
params.putParcelable(KEY_UPDATED_MESSAGE_URI,
|
||||
extras.getParcelable(SendMessageAction.EXTRA_UPDATED_MESSAGE_URI, Uri.class));
|
||||
params.putInt(KEY_SUB_ID,
|
||||
extras.getInt(SendMessageAction.KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID));
|
||||
params.putInt(KEY_RESULT_CODE, resultCode);
|
||||
params.putInt(KEY_HTTP_STATUS_CODE, extras.getInt(SmsManager.EXTRA_MMS_HTTP_STATUS, 0));
|
||||
params.putParcelable(KEY_CONTENT_URI,
|
||||
extras.getParcelable(SendMessageAction.EXTRA_CONTENT_URI, Uri.class));
|
||||
params.putByteArray(KEY_RESPONSE, extras.getByteArray(SmsManager.EXTRA_MMS_DATA));
|
||||
params.putBoolean(KEY_RESPONSE_IMPORTANT,
|
||||
extras.getBoolean(SendMessageAction.EXTRA_RESPONSE_IMPORTANT));
|
||||
action.start();
|
||||
}
|
||||
|
||||
public static void processMessageSentFastFailed(final String messageId,
|
||||
final Uri messageUri, final Uri updatedMessageUri, final int subId, final boolean isSms,
|
||||
final int status, final int rawStatus, final int resultCode) {
|
||||
final ProcessSentMessageAction action = new ProcessSentMessageAction();
|
||||
final Bundle params = action.actionParameters;
|
||||
params.putBoolean(KEY_SMS, isSms);
|
||||
params.putBoolean(KEY_SENT_BY_PLATFORM, false);
|
||||
params.putString(KEY_MESSAGE_ID, messageId);
|
||||
params.putParcelable(KEY_MESSAGE_URI, messageUri);
|
||||
params.putParcelable(KEY_UPDATED_MESSAGE_URI, updatedMessageUri);
|
||||
params.putInt(KEY_SUB_ID, subId);
|
||||
params.putInt(KEY_STATUS, status);
|
||||
params.putInt(KEY_RAW_STATUS, rawStatus);
|
||||
params.putInt(KEY_RESULT_CODE, resultCode);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private ProcessSentMessageAction() {
|
||||
// Callers must use one of the static methods above
|
||||
}
|
||||
|
||||
/**
|
||||
* Update message status to reflect success or failure
|
||||
* Can also update the message itself if a "final" message is now available from telephony db
|
||||
*/
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
final Uri messageUri = actionParameters.getParcelable(KEY_MESSAGE_URI, Uri.class);
|
||||
final Uri updatedMessageUri = actionParameters.getParcelable(KEY_UPDATED_MESSAGE_URI,
|
||||
Uri.class);
|
||||
final boolean isSms = actionParameters.getBoolean(KEY_SMS);
|
||||
final boolean sentByPlatform = actionParameters.getBoolean(KEY_SENT_BY_PLATFORM);
|
||||
|
||||
int status = actionParameters.getInt(KEY_STATUS, MmsUtils.MMS_REQUEST_MANUAL_RETRY);
|
||||
int rawStatus = actionParameters.getInt(KEY_RAW_STATUS,
|
||||
MmsUtils.PDU_HEADER_VALUE_UNDEFINED);
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
|
||||
if (sentByPlatform) {
|
||||
// Delete temporary file backing the contentUri passed to MMS service
|
||||
final Uri contentUri = actionParameters.getParcelable(KEY_CONTENT_URI, Uri.class);
|
||||
Assert.isTrue(contentUri != null);
|
||||
final File tempFile = MmsFileProvider.getFile(contentUri);
|
||||
long messageSize = 0;
|
||||
if (tempFile.exists()) {
|
||||
messageSize = tempFile.length();
|
||||
tempFile.delete();
|
||||
LogUtil.v(TAG, "ProcessSentMessageAction: Deleted temp file with outgoing "
|
||||
+ "MMS pdu: " + contentUri);
|
||||
}
|
||||
|
||||
final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
|
||||
final boolean responseImportant = actionParameters.getBoolean(KEY_RESPONSE_IMPORTANT);
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
if (responseImportant) {
|
||||
// Get the status from the response PDU and update telephony
|
||||
final byte[] response = actionParameters.getByteArray(KEY_RESPONSE);
|
||||
final SendConf sendConf = MmsSender.parseSendConf(response, subId);
|
||||
if (sendConf != null) {
|
||||
final MmsUtils.StatusPlusUri result =
|
||||
MmsUtils.updateSentMmsMessageStatus(context, messageUri, sendConf);
|
||||
status = result.status;
|
||||
rawStatus = result.rawStatus;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String errorMsg = "ProcessSentMessageAction: Platform returned error resultCode: "
|
||||
+ resultCode;
|
||||
final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
|
||||
if (httpStatusCode != 0) {
|
||||
errorMsg += (", HTTP status code: " + httpStatusCode);
|
||||
}
|
||||
LogUtil.w(TAG, errorMsg);
|
||||
status = MmsSender.getErrorResultStatus(resultCode, httpStatusCode);
|
||||
|
||||
// Check for MMS messages that failed because they exceeded the maximum size,
|
||||
// indicated by an I/O error from the platform.
|
||||
if (resultCode == SmsManager.MMS_ERROR_IO_ERROR) {
|
||||
if (messageSize > MmsConfig.get(subId).getMaxMessageSize()) {
|
||||
rawStatus = MessageData.RAW_TELEPHONY_STATUS_MESSAGE_TOO_BIG;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (messageId != null) {
|
||||
final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
|
||||
final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
|
||||
processResult(
|
||||
messageId, updatedMessageUri, status, rawStatus, isSms, this, subId,
|
||||
resultCode, httpStatusCode);
|
||||
} else {
|
||||
LogUtil.v(TAG, "ProcessSentMessageAction: No sent message to process (it was "
|
||||
+ "probably a notify response for an MMS download)");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static void processResult(final String messageId, Uri updatedMessageUri, int status,
|
||||
final int rawStatus, final boolean isSms, final Action processingAction,
|
||||
final int subId, final int resultCode, final int httpStatusCode) {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
|
||||
final MessageData originalMessage = message;
|
||||
if (message == null) {
|
||||
LogUtil.w(TAG, "ProcessSentMessageAction: Sent message " + messageId
|
||||
+ " missing from local database");
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(
|
||||
true /* failed */, processingAction);
|
||||
return;
|
||||
}
|
||||
final String conversationId = message.getConversationId();
|
||||
if (updatedMessageUri != null) {
|
||||
// Update message if we have newly written final message in the telephony db
|
||||
final MessageData update = MmsUtils.readSendingMmsMessage(updatedMessageUri,
|
||||
conversationId, message.getParticipantId(), message.getSelfId());
|
||||
if (update != null) {
|
||||
// Set message Id of final message to that of the existing place holder.
|
||||
update.updateMessageId(message.getMessageId());
|
||||
// Update image sizes.
|
||||
update.updateSizesForImageParts();
|
||||
// Temp attachments are no longer needed
|
||||
for (final MessagePartData part : message.getParts()) {
|
||||
part.destroySync();
|
||||
}
|
||||
message = update;
|
||||
// processResult will rewrite the complete message as part of update
|
||||
} else {
|
||||
updatedMessageUri = null;
|
||||
status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
|
||||
LogUtil.e(TAG, "ProcessSentMessageAction: Unable to read sending message");
|
||||
}
|
||||
}
|
||||
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
boolean failed;
|
||||
if (status == MmsUtils.MMS_REQUEST_SUCCEEDED) {
|
||||
message.markMessageSent(timestamp);
|
||||
failed = false;
|
||||
} else if (status == MmsUtils.MMS_REQUEST_AUTO_RETRY
|
||||
&& message.getInResendWindow(timestamp)) {
|
||||
message.markMessageNotSent(timestamp);
|
||||
message.setRawTelephonyStatus(rawStatus);
|
||||
failed = false;
|
||||
} else {
|
||||
message.markMessageFailed(timestamp);
|
||||
message.setRawTelephonyStatus(rawStatus);
|
||||
message.setMessageSeen(false);
|
||||
failed = true;
|
||||
}
|
||||
|
||||
// We have special handling for when a message to an emergency number fails. In this case,
|
||||
// we notify immediately of any failure (even if we auto-retry), and instruct the user to
|
||||
// try calling the emergency number instead.
|
||||
if (status != MmsUtils.MMS_REQUEST_SUCCEEDED) {
|
||||
final ArrayList<String> recipients =
|
||||
BugleDatabaseOperations.getRecipientsForConversation(db, conversationId);
|
||||
for (final String recipient : recipients) {
|
||||
if (PhoneNumberUtils.isEmergencyNumber(recipient)) {
|
||||
BugleNotifications.notifyEmergencySmsFailed(recipient, conversationId);
|
||||
message.markMessageFailedEmergencyNumber(timestamp);
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the message status and optionally refresh the message with final parts/values.
|
||||
if (SendMessageAction.updateMessageAndStatus(isSms, message, updatedMessageUri, failed)) {
|
||||
// We shouldn't show any notifications if we're not allowed to modify Telephony for
|
||||
// this message.
|
||||
if (failed) {
|
||||
BugleNotifications.update(false, BugleNotifications.UPDATE_ERRORS);
|
||||
}
|
||||
BugleActionToasts.onSendMessageOrManualDownloadActionCompleted(
|
||||
conversationId, !failed, status, isSms, subId, true/*isSend*/);
|
||||
}
|
||||
|
||||
LogUtil.i(TAG, "ProcessSentMessageAction: Done sending " + (isSms ? "SMS" : "MMS")
|
||||
+ " message " + message.getMessageId()
|
||||
+ " in conversation " + conversationId
|
||||
+ "; status is " + MmsUtils.getRequestStatusDescription(status));
|
||||
|
||||
// Whether we succeeded or failed we will check and maybe schedule some more work
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(
|
||||
status != MmsUtils.MMS_REQUEST_SUCCEEDED, processingAction);
|
||||
}
|
||||
|
||||
private ProcessSentMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ProcessSentMessageAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public ProcessSentMessageAction createFromParcel(final Parcel in) {
|
||||
return new ProcessSentMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProcessSentMessageAction[] newArray(final int size) {
|
||||
return new ProcessSentMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.action.ActionMonitor.ActionCompletedListener;
|
||||
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.RunsOnMainThread;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
public class ReadDraftDataAction extends Action implements Parcelable {
|
||||
|
||||
/**
|
||||
* Interface for ReadDraftDataAction listeners
|
||||
*/
|
||||
public interface ReadDraftDataActionListener {
|
||||
@RunsOnMainThread
|
||||
void onReadDraftDataSucceeded(final ReadDraftDataAction action,
|
||||
final Object data, final MessageData message,
|
||||
final ConversationListItemData conversation);
|
||||
@RunsOnMainThread
|
||||
void onReadDraftDataFailed(final ReadDraftDataAction action, final Object data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read draft message and associated data (with listener)
|
||||
*/
|
||||
public static ReadDraftDataActionMonitor readDraftData(final String conversationId,
|
||||
final MessageData incomingDraft, final Object data,
|
||||
final ReadDraftDataActionListener listener) {
|
||||
final ReadDraftDataActionMonitor monitor = new ReadDraftDataActionMonitor(data,
|
||||
listener);
|
||||
final ReadDraftDataAction action = new ReadDraftDataAction(conversationId,
|
||||
incomingDraft, monitor.getActionKey());
|
||||
action.start(monitor);
|
||||
return monitor;
|
||||
}
|
||||
|
||||
private static final String KEY_CONVERSATION_ID = "conversationId";
|
||||
private static final String KEY_INCOMING_DRAFT = "draftMessage";
|
||||
|
||||
private ReadDraftDataAction(final String conversationId, final MessageData incomingDraft,
|
||||
final String actionKey) {
|
||||
super(actionKey);
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
actionParameters.putParcelable(KEY_INCOMING_DRAFT, incomingDraft);
|
||||
}
|
||||
|
||||
class DraftData {
|
||||
public final MessageData message;
|
||||
public final ConversationListItemData conversation;
|
||||
|
||||
DraftData(final MessageData message, final ConversationListItemData conversation) {
|
||||
this.message = message;
|
||||
this.conversation = conversation;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final MessageData incomingDraft = actionParameters.getParcelable(KEY_INCOMING_DRAFT,
|
||||
MessageData.class);
|
||||
final ConversationListItemData conversation =
|
||||
ConversationListItemData.getExistingConversation(db, conversationId);
|
||||
MessageData message = null;
|
||||
if (conversation != null) {
|
||||
if (incomingDraft == null) {
|
||||
message = BugleDatabaseOperations.readDraftMessageData(db, conversationId,
|
||||
conversation.getSelfId());
|
||||
}
|
||||
if (message == null) {
|
||||
message = MessageData.createDraftMessage(conversationId, conversation.getSelfId(),
|
||||
incomingDraft);
|
||||
LogUtil.d(LogUtil.BUGLE_TAG, "ReadDraftMessage: created draft. "
|
||||
+ "conversationId=" + conversationId
|
||||
+ " selfId=" + conversation.getSelfId());
|
||||
} else {
|
||||
LogUtil.d(LogUtil.BUGLE_TAG, "ReadDraftMessage: read draft. "
|
||||
+ "conversationId=" + conversationId
|
||||
+ " selfId=" + conversation.getSelfId());
|
||||
}
|
||||
return new DraftData(message, conversation);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* An operation that notifies a listener upon completion
|
||||
*/
|
||||
public static class ReadDraftDataActionMonitor extends ActionMonitor
|
||||
implements ActionCompletedListener {
|
||||
|
||||
private final ReadDraftDataActionListener mListener;
|
||||
|
||||
ReadDraftDataActionMonitor(final Object data,
|
||||
final ReadDraftDataActionListener completed) {
|
||||
super(STATE_CREATED, generateUniqueActionKey("ReadDraftDataAction"), data);
|
||||
setCompletedListener(this);
|
||||
mListener = completed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionSucceeded(final ActionMonitor monitor,
|
||||
final Action action, final Object data, final Object result) {
|
||||
final DraftData draft = (DraftData) result;
|
||||
if (draft == null) {
|
||||
mListener.onReadDraftDataFailed((ReadDraftDataAction) action, data);
|
||||
} else {
|
||||
mListener.onReadDraftDataSucceeded((ReadDraftDataAction) action, data,
|
||||
draft.message, draft.conversation);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionFailed(final ActionMonitor monitor,
|
||||
final Action action, final Object data, final Object result) {
|
||||
Assert.fail("Reading draft should not fail");
|
||||
}
|
||||
}
|
||||
|
||||
private ReadDraftDataAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ReadDraftDataAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public ReadDraftDataAction createFromParcel(final Parcel in) {
|
||||
return new ReadDraftDataAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadDraftDataAction[] newArray(final int size) {
|
||||
return new ReadDraftDataAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.v7.mms.pdu.PduHeaders;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.DatabaseMessages;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Action used to "receive" an incoming message
|
||||
*/
|
||||
public class ReceiveMmsMessageAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
private static final String KEY_SUB_ID = "sub_id";
|
||||
private static final String KEY_PUSH_DATA = "push_data";
|
||||
private static final String KEY_TRANSACTION_ID = "transaction_id";
|
||||
private static final String KEY_CONTENT_LOCATION = "content_location";
|
||||
|
||||
/**
|
||||
* Create a message received from a particular number in a particular conversation
|
||||
*/
|
||||
public ReceiveMmsMessageAction(final int subId, final byte[] pushData) {
|
||||
actionParameters.putInt(KEY_SUB_ID, subId);
|
||||
actionParameters.putByteArray(KEY_PUSH_DATA, pushData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
final byte[] pushData = actionParameters.getByteArray(KEY_PUSH_DATA);
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// Write received message to telephony DB
|
||||
MessageData message = null;
|
||||
final ParticipantData self = BugleDatabaseOperations.getOrCreateSelf(db, subId);
|
||||
|
||||
final long received = System.currentTimeMillis();
|
||||
// Inform sync that message has been added at local received timestamp
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(received);
|
||||
|
||||
// TODO: Should use local time to set received time in MMS message
|
||||
final DatabaseMessages.MmsMessage mms = MmsUtils.processReceivedPdu(
|
||||
context, pushData, self.getSubId(), self.getNormalizedDestination());
|
||||
|
||||
if (mms != null) {
|
||||
final List<String> recipients = MmsUtils.getRecipientsByThread(mms.mThreadId);
|
||||
String from = MmsUtils.getMmsSender(recipients, mms.getUri());
|
||||
if (from == null) {
|
||||
LogUtil.w(TAG, "Received an MMS without sender address; using unknown sender.");
|
||||
from = ParticipantData.getUnknownSenderDestination();
|
||||
}
|
||||
final ParticipantData rawSender = ParticipantData.getFromRawPhoneBySimLocale(
|
||||
from, subId);
|
||||
final boolean blocked = BugleDatabaseOperations.isBlockedDestination(
|
||||
db, rawSender.getNormalizedDestination());
|
||||
final boolean autoDownload = (!blocked && MmsUtils.allowMmsAutoRetrieve(subId));
|
||||
final String conversationId =
|
||||
BugleDatabaseOperations.getOrCreateConversationFromThreadId(db, mms.mThreadId,
|
||||
blocked, subId);
|
||||
|
||||
final boolean messageInFocusedConversation =
|
||||
DataModel.get().isFocusedConversation(conversationId);
|
||||
final boolean messageInObservableConversation =
|
||||
DataModel.get().isNewMessageObservable(conversationId);
|
||||
|
||||
// TODO: Also write these values to the telephony provider
|
||||
mms.mRead = messageInFocusedConversation;
|
||||
mms.mSeen = messageInObservableConversation || blocked;
|
||||
|
||||
// Write received placeholder message to our DB
|
||||
db.beginTransaction();
|
||||
try {
|
||||
final String participantId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, rawSender);
|
||||
final String selfId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
|
||||
|
||||
message = MmsUtils.createMmsMessage(mms, conversationId, participantId, selfId,
|
||||
(autoDownload ? MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD :
|
||||
MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD));
|
||||
// Write the message
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
|
||||
if (!autoDownload) {
|
||||
BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
|
||||
conversationId, message.getMessageId(), message.getReceivedTimeStamp(),
|
||||
blocked, true /* shouldAutoSwitchSelfId */);
|
||||
final ParticipantData sender = ParticipantData .getFromId(
|
||||
db, participantId);
|
||||
BugleActionToasts.onMessageReceived(conversationId, sender, message);
|
||||
}
|
||||
// else update the conversation once we have downloaded final message (or failed)
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
// Update conversation if not immediately initiating a download
|
||||
if (!autoDownload) {
|
||||
MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
|
||||
// Show a notification to let the user know a new message has arrived
|
||||
BugleNotifications.update(false/*silent*/, conversationId,
|
||||
BugleNotifications.UPDATE_ALL);
|
||||
|
||||
// Send the NotifyRespInd with DEFERRED status since no auto download
|
||||
actionParameters.putString(KEY_TRANSACTION_ID, mms.mTransactionId);
|
||||
actionParameters.putString(KEY_CONTENT_LOCATION, mms.mContentLocation);
|
||||
requestBackgroundWork();
|
||||
}
|
||||
|
||||
LogUtil.i(TAG, "ReceiveMmsMessageAction: Received MMS message " + message.getMessageId()
|
||||
+ " in conversation " + message.getConversationId()
|
||||
+ ", uri = " + message.getSmsMessageUri());
|
||||
} else {
|
||||
LogUtil.e(TAG, "ReceiveMmsMessageAction: Skipping processing of incoming PDU");
|
||||
}
|
||||
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bundle doBackgroundWork() {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
|
||||
final String contentLocation = actionParameters.getString(KEY_CONTENT_LOCATION);
|
||||
MmsUtils.sendNotifyResponseForMmsDownload(
|
||||
context,
|
||||
subId,
|
||||
MmsUtils.stringToBytes(transactionId, "UTF-8"),
|
||||
contentLocation,
|
||||
PduHeaders.STATUS_DEFERRED);
|
||||
// We don't need to return anything.
|
||||
return null;
|
||||
}
|
||||
|
||||
private ReceiveMmsMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ReceiveMmsMessageAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public ReceiveMmsMessageAction createFromParcel(final Parcel in) {
|
||||
return new ReceiveMmsMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReceiveMmsMessageAction[] newArray(final int size) {
|
||||
return new ReceiveMmsMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.Telephony.Sms;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsSmsUtils;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
|
||||
/**
|
||||
* Action used to "receive" an incoming message
|
||||
*/
|
||||
public class ReceiveSmsMessageAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
private static final String KEY_MESSAGE_VALUES = "message_values";
|
||||
private static final String KEY_SUB_ID = "sub_id";
|
||||
|
||||
/**
|
||||
* Create a message received from a particular number in a particular conversation
|
||||
*/
|
||||
public ReceiveSmsMessageAction(final ContentValues messageValues) {
|
||||
actionParameters.putParcelable(KEY_MESSAGE_VALUES, messageValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final ContentValues messageValues = actionParameters.getParcelable(KEY_MESSAGE_VALUES,
|
||||
ContentValues.class);
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// Get the SIM subscription ID
|
||||
Integer subId = messageValues.getAsInteger(Sms.SUBSCRIPTION_ID);
|
||||
if (subId == null) {
|
||||
subId = ParticipantData.DEFAULT_SELF_SUB_ID;
|
||||
}
|
||||
// Make sure we have a sender address
|
||||
String address = messageValues.getAsString(Sms.ADDRESS);
|
||||
if (TextUtils.isEmpty(address)) {
|
||||
LogUtil.w(TAG, "Received an SMS without an address; using unknown sender.");
|
||||
address = ParticipantData.getUnknownSenderDestination();
|
||||
messageValues.put(Sms.ADDRESS, address);
|
||||
}
|
||||
final ParticipantData rawSender = ParticipantData.getFromRawPhoneBySimLocale(
|
||||
address, subId);
|
||||
|
||||
// TODO: Should use local timestamp for this?
|
||||
final long received = messageValues.getAsLong(Sms.DATE);
|
||||
// Inform sync that message has been added at local received timestamp
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(received);
|
||||
|
||||
// Make sure we've got a thread id
|
||||
final long threadId = MmsSmsUtils.Threads.getOrCreateThreadId(context, address);
|
||||
messageValues.put(Sms.THREAD_ID, threadId);
|
||||
final boolean blocked = BugleDatabaseOperations.isBlockedDestination(
|
||||
db, rawSender.getNormalizedDestination());
|
||||
final String conversationId = BugleDatabaseOperations.
|
||||
getOrCreateConversationFromRecipient(db, threadId, blocked, rawSender);
|
||||
|
||||
final boolean messageInFocusedConversation =
|
||||
DataModel.get().isFocusedConversation(conversationId);
|
||||
final boolean messageInObservableConversation =
|
||||
DataModel.get().isNewMessageObservable(conversationId);
|
||||
|
||||
MessageData message = null;
|
||||
// Only the primary user gets to insert the message into the telephony db and into bugle's
|
||||
// db. The secondary user goes through this path, but skips doing the actual insert. It
|
||||
// goes through this path because it needs to compute messageInFocusedConversation in order
|
||||
// to calculate whether to skip the notification and play a soft sound if the user is
|
||||
// already in the conversation.
|
||||
if (!OsUtil.isSecondaryUser()) {
|
||||
final boolean read = messageValues.getAsBoolean(Sms.Inbox.READ)
|
||||
|| messageInFocusedConversation;
|
||||
// If you have read it you have seen it
|
||||
final boolean seen = read || messageInObservableConversation || blocked;
|
||||
messageValues.put(Sms.Inbox.READ, read ? Integer.valueOf(1) : Integer.valueOf(0));
|
||||
|
||||
// incoming messages are marked as seen in the telephony db
|
||||
messageValues.put(Sms.Inbox.SEEN, 1);
|
||||
|
||||
// Insert into telephony
|
||||
final Uri messageUri = context.getContentResolver().insert(Sms.Inbox.CONTENT_URI,
|
||||
messageValues);
|
||||
|
||||
if (messageUri != null) {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "ReceiveSmsMessageAction: Inserted SMS message into telephony, "
|
||||
+ "uri = " + messageUri);
|
||||
}
|
||||
} else {
|
||||
LogUtil.e(TAG, "ReceiveSmsMessageAction: Failed to insert SMS into telephony!");
|
||||
}
|
||||
|
||||
final String text = messageValues.getAsString(Sms.BODY);
|
||||
final String subject = messageValues.getAsString(Sms.SUBJECT);
|
||||
final long sent = messageValues.getAsLong(Sms.DATE_SENT);
|
||||
final ParticipantData self = ParticipantData.getSelfParticipant(subId);
|
||||
final Integer pathPresent = messageValues.getAsInteger(Sms.REPLY_PATH_PRESENT);
|
||||
final String smsServiceCenter = messageValues.getAsString(Sms.SERVICE_CENTER);
|
||||
String conversationServiceCenter = null;
|
||||
// Only set service center if message REPLY_PATH_PRESENT = 1
|
||||
if (pathPresent != null && pathPresent == 1 && !TextUtils.isEmpty(smsServiceCenter)) {
|
||||
conversationServiceCenter = smsServiceCenter;
|
||||
}
|
||||
db.beginTransaction();
|
||||
try {
|
||||
final String participantId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, rawSender);
|
||||
final String selfId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
|
||||
|
||||
message = MessageData.createReceivedSmsMessage(messageUri, conversationId,
|
||||
participantId, selfId, text, subject, sent, received, seen, read);
|
||||
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
|
||||
BugleDatabaseOperations.updateConversationMetadataInTransaction(db, conversationId,
|
||||
message.getMessageId(), message.getReceivedTimeStamp(), blocked,
|
||||
conversationServiceCenter, true /* shouldAutoSwitchSelfId */);
|
||||
|
||||
final ParticipantData sender = ParticipantData.getFromId(db, participantId);
|
||||
BugleActionToasts.onMessageReceived(conversationId, sender, message);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
LogUtil.i(TAG, "ReceiveSmsMessageAction: Received SMS message " + message.getMessageId()
|
||||
+ " in conversation " + message.getConversationId()
|
||||
+ ", uri = " + messageUri);
|
||||
|
||||
actionParameters.putInt(KEY_SUB_ID, subId);
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
|
||||
} else {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "ReceiveSmsMessageAction: Not inserting received SMS message for "
|
||||
+ "secondary user.");
|
||||
}
|
||||
}
|
||||
// Show a notification to let the user know a new message has arrived
|
||||
BugleNotifications.update(false/*silent*/, conversationId, BugleNotifications.UPDATE_ALL);
|
||||
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private ReceiveSmsMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ReceiveSmsMessageAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public ReceiveSmsMessageAction createFromParcel(final Parcel in) {
|
||||
return new ReceiveSmsMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReceiveSmsMessageAction[] newArray(final int size) {
|
||||
return new ReceiveSmsMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Action to manually start an MMS download (after failed or manual mms download)
|
||||
*/
|
||||
public class RedownloadMmsAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
private static final int REQUEST_CODE_PENDING_INTENT = 101;
|
||||
|
||||
private static final String KEY_SUB_ID = "sub_id";
|
||||
|
||||
/**
|
||||
* Download an MMS message
|
||||
*/
|
||||
public static void redownloadMessage(final String messageId) {
|
||||
final RedownloadMmsAction action = new RedownloadMmsAction(messageId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a pending intent of for downloading an MMS
|
||||
*/
|
||||
public static PendingIntent getPendingIntentForRedownloadMms(
|
||||
final Context context, final String messageId) {
|
||||
final Action action = new RedownloadMmsAction(messageId);
|
||||
return ActionService.makeStartActionPendingIntent(context,
|
||||
action, REQUEST_CODE_PENDING_INTENT, false /*launchesAnActivity*/);
|
||||
}
|
||||
|
||||
// Core parameters needed for all types of message
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
|
||||
/**
|
||||
* Constructor used for retrying sending in the background (only message id available)
|
||||
*/
|
||||
RedownloadMmsAction(final String messageId) {
|
||||
super();
|
||||
actionParameters.putString(KEY_MESSAGE_ID, messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read message from database and change status to allow downloading
|
||||
*/
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
|
||||
// Check message can be redownloaded
|
||||
if (message != null && message.canRedownloadMessage()) {
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
|
||||
final ContentValues values = new ContentValues(2);
|
||||
values.put(DatabaseHelper.MessageColumns.STATUS,
|
||||
MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD);
|
||||
values.put(DatabaseHelper.MessageColumns.RETRY_START_TIMESTAMP, timestamp);
|
||||
|
||||
// Row must exist as was just loaded above (on ActionService thread)
|
||||
BugleDatabaseOperations.updateMessageRow(db, message.getMessageId(), values);
|
||||
|
||||
MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
|
||||
actionParameters.putInt(KEY_SUB_ID,
|
||||
BugleDatabaseOperations.getSelfSubscriptionId(db, message.getSelfId()));
|
||||
// Whether we succeeded or failed we will check and maybe schedule some more work
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
|
||||
} else {
|
||||
message = null;
|
||||
LogUtil.e(LogUtil.BUGLE_TAG,
|
||||
"Attempt to download a missing or un-redownloadable message");
|
||||
}
|
||||
// Immediately update the notifications in case we came from the download action from a
|
||||
// heads-up notification. This will dismiss the heads-up notification.
|
||||
BugleNotifications.update(false/*silent*/, BugleNotifications.UPDATE_ALL);
|
||||
return message;
|
||||
}
|
||||
|
||||
private RedownloadMmsAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<RedownloadMmsAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public RedownloadMmsAction createFromParcel(final Parcel in) {
|
||||
return new RedownloadMmsAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedownloadMmsAction[] newArray(final int size) {
|
||||
return new RedownloadMmsAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Action used to manually resend an outgoing message
|
||||
*/
|
||||
public class ResendMessageAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
private static final String KEY_SUB_ID = "sub_id";
|
||||
|
||||
/**
|
||||
* Manual send of existing message (no listener)
|
||||
*/
|
||||
public static void resendMessage(final String messageId) {
|
||||
final ResendMessageAction action = new ResendMessageAction(messageId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
// Core parameters needed for all types of message
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
|
||||
/**
|
||||
* Constructor used for retrying sending in the background (only message id available)
|
||||
*/
|
||||
ResendMessageAction(final String messageId) {
|
||||
super();
|
||||
actionParameters.putString(KEY_MESSAGE_ID, messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read message from database and change status to allow sending
|
||||
*/
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
final MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
|
||||
// Check message can be resent
|
||||
if (message != null && message.canResendMessage()) {
|
||||
final boolean isMms = message.getIsMms();
|
||||
long timestamp = System.currentTimeMillis();
|
||||
if (isMms) {
|
||||
// MMS expects timestamp rounded to nearest second
|
||||
timestamp = 1000 * ((timestamp + 500) / 1000);
|
||||
}
|
||||
|
||||
LogUtil.i(TAG, "ResendMessageAction: Resending message " + messageId
|
||||
+ "; changed timestamp from " + message.getReceivedTimeStamp() + " to "
|
||||
+ timestamp);
|
||||
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(MessageColumns.STATUS, MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND);
|
||||
values.put(MessageColumns.RETRY_START_TIMESTAMP, timestamp);
|
||||
|
||||
// Row must exist as was just loaded above (on ActionService thread)
|
||||
BugleDatabaseOperations.updateMessageRow(db, message.getMessageId(), values);
|
||||
|
||||
MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
|
||||
|
||||
actionParameters.putInt(KEY_SUB_ID,
|
||||
BugleDatabaseOperations.getSelfSubscriptionId(db, message.getSelfId()));
|
||||
|
||||
// Whether we succeeded or failed we will check and maybe schedule some more work
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
|
||||
|
||||
return message;
|
||||
} else {
|
||||
String error = "ResendMessageAction: Cannot resend message " + messageId + "; ";
|
||||
if (message != null) {
|
||||
error += ("status = " + MessageData.getStatusDescription(message.getStatus()));
|
||||
} else {
|
||||
error += "not found in database";
|
||||
}
|
||||
LogUtil.e(TAG, error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private ResendMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ResendMessageAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public ResendMessageAction createFromParcel(final Parcel in) {
|
||||
return new ResendMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResendMessageAction[] newArray(final int size) {
|
||||
return new ResendMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 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.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.Telephony.Mms;
|
||||
import android.provider.Telephony.Sms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Action used to send an outgoing message. It writes MMS messages to the telephony db
|
||||
* ({@link InsertNewMessageAction}) writes SMS messages to the telephony db). It also
|
||||
* initiates the actual sending. It will all be used for re-sending a failed message.
|
||||
* <p>
|
||||
* This class is public (not package-private) because the SMS/MMS (e.g. MmsUtils) classes need to
|
||||
* access the EXTRA_* fields for setting up the 'sent' pending intent.
|
||||
*/
|
||||
public class SendMessageAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
/**
|
||||
* Queue sending of existing message (can only be called during execute of action)
|
||||
*/
|
||||
static boolean queueForSendInBackground(final String messageId,
|
||||
final Action processingAction) {
|
||||
final SendMessageAction action = new SendMessageAction();
|
||||
return action.queueAction(messageId, processingAction);
|
||||
}
|
||||
|
||||
public static final boolean DEFAULT_DELIVERY_REPORT_MODE = false;
|
||||
public static final int MAX_SMS_RETRY = 3;
|
||||
|
||||
// Core parameters needed for all types of message
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_MESSAGE = "message";
|
||||
private static final String KEY_MESSAGE_URI = "message_uri";
|
||||
private static final String KEY_SUB_PHONE_NUMBER = "sub_phone_number";
|
||||
|
||||
// For sms messages a few extra values are included in the bundle
|
||||
private static final String KEY_RECIPIENT = "recipient";
|
||||
private static final String KEY_RECIPIENTS = "recipients";
|
||||
private static final String KEY_SMS_SERVICE_CENTER = "sms_service_center";
|
||||
|
||||
// Values we attach to the pending intent that's fired when the message is sent.
|
||||
// Only applicable when sending via the platform APIs on L+.
|
||||
public static final String KEY_SUB_ID = "sub_id";
|
||||
public static final String EXTRA_MESSAGE_ID = "message_id";
|
||||
public static final String EXTRA_UPDATED_MESSAGE_URI = "updated_message_uri";
|
||||
public static final String EXTRA_CONTENT_URI = "content_uri";
|
||||
public static final String EXTRA_RESPONSE_IMPORTANT = "response_important";
|
||||
|
||||
/**
|
||||
* Constructor used for retrying sending in the background (only message id available)
|
||||
*/
|
||||
private SendMessageAction() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read message from database and queue actual sending
|
||||
*/
|
||||
private boolean queueAction(final String messageId, final Action processingAction) {
|
||||
actionParameters.putString(KEY_MESSAGE_ID, messageId);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
final MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
|
||||
// Check message can be resent
|
||||
if (message != null && message.canSendMessage()) {
|
||||
final boolean isSms = message.getIsSms();
|
||||
long timestamp = System.currentTimeMillis();
|
||||
if (!isSms) {
|
||||
// MMS expects timestamp rounded to nearest second
|
||||
timestamp = 1000 * ((timestamp + 500) / 1000);
|
||||
}
|
||||
|
||||
final ParticipantData self = BugleDatabaseOperations.getExistingParticipant(
|
||||
db, message.getSelfId());
|
||||
final Uri messageUri = message.getSmsMessageUri();
|
||||
final String conversationId = message.getConversationId();
|
||||
|
||||
// Update message status
|
||||
if (message.getYetToSend()) {
|
||||
if (message.getReceivedTimeStamp() == message.getRetryStartTimestamp()) {
|
||||
// Initial sending of message
|
||||
message.markMessageSending(timestamp);
|
||||
} else {
|
||||
// Manual resend of message
|
||||
message.markMessageManualResend(timestamp);
|
||||
}
|
||||
} else {
|
||||
// Automatic resend of message
|
||||
message.markMessageResending(timestamp);
|
||||
}
|
||||
if (!updateMessageAndStatus(isSms, message, null /* messageUri */, false /*notify*/)) {
|
||||
// If message is missing in the telephony database we don't need to send it
|
||||
return false;
|
||||
}
|
||||
|
||||
final ArrayList<String> recipients =
|
||||
BugleDatabaseOperations.getRecipientsForConversation(db, conversationId);
|
||||
|
||||
// Update action state with parameters needed for background sending
|
||||
actionParameters.putParcelable(KEY_MESSAGE_URI, messageUri);
|
||||
actionParameters.putParcelable(KEY_MESSAGE, message);
|
||||
actionParameters.putStringArrayList(KEY_RECIPIENTS, recipients);
|
||||
actionParameters.putInt(KEY_SUB_ID, self.getSubId());
|
||||
actionParameters.putString(KEY_SUB_PHONE_NUMBER, self.getNormalizedDestination());
|
||||
|
||||
if (isSms) {
|
||||
final String smsc = BugleDatabaseOperations.getSmsServiceCenterForConversation(
|
||||
db, conversationId);
|
||||
actionParameters.putString(KEY_SMS_SERVICE_CENTER, smsc);
|
||||
|
||||
if (recipients.size() == 1) {
|
||||
final String recipient = recipients.get(0);
|
||||
|
||||
actionParameters.putString(KEY_RECIPIENT, recipient);
|
||||
// Queue actual sending for SMS
|
||||
processingAction.requestBackgroundWork(this);
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SendMessageAction: Queued SMS message " + messageId
|
||||
+ " for sending");
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
LogUtil.wtf(TAG, "Trying to resend a broadcast SMS - not allowed");
|
||||
}
|
||||
} else {
|
||||
// Queue actual sending for MMS
|
||||
processingAction.requestBackgroundWork(this);
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SendMessageAction: Queued MMS message " + messageId
|
||||
+ " for sending");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Never called
|
||||
*/
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
Assert.fail("SendMessageAction must be queued rather than started");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message on background worker thread
|
||||
*/
|
||||
@Override
|
||||
protected Bundle doBackgroundWork() {
|
||||
final MessageData message = actionParameters.getParcelable(KEY_MESSAGE, MessageData.class);
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
Uri messageUri = actionParameters.getParcelable(KEY_MESSAGE_URI, Uri.class);
|
||||
Uri updatedMessageUri = null;
|
||||
final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS;
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
final String subPhoneNumber = actionParameters.getString(KEY_SUB_PHONE_NUMBER);
|
||||
|
||||
LogUtil.i(TAG, "SendMessageAction: Sending " + (isSms ? "SMS" : "MMS") + " message "
|
||||
+ messageId + " in conversation " + message.getConversationId());
|
||||
|
||||
int status;
|
||||
int rawStatus = MessageData.RAW_TELEPHONY_STATUS_UNDEFINED;
|
||||
int resultCode = MessageData.UNKNOWN_RESULT_CODE;
|
||||
if (isSms) {
|
||||
Assert.notNull(messageUri);
|
||||
final String recipient = actionParameters.getString(KEY_RECIPIENT);
|
||||
final String messageText = message.getMessageText();
|
||||
final String smsServiceCenter = actionParameters.getString(KEY_SMS_SERVICE_CENTER);
|
||||
final boolean deliveryReportRequired = MmsUtils.isDeliveryReportRequired(subId);
|
||||
|
||||
status = MmsUtils.sendSmsMessage(recipient, messageText, messageUri, subId,
|
||||
smsServiceCenter, deliveryReportRequired);
|
||||
} else {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final ArrayList<String> recipients =
|
||||
actionParameters.getStringArrayList(KEY_RECIPIENTS);
|
||||
if (messageUri == null) {
|
||||
final long timestamp = message.getReceivedTimeStamp();
|
||||
|
||||
// Inform sync that message has been added at local received timestamp
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
syncManager.onNewMessageInserted(timestamp);
|
||||
|
||||
// For MMS messages first need to write to telephony (resizing images if needed)
|
||||
updatedMessageUri = MmsUtils.insertSendingMmsMessage(context, recipients,
|
||||
message, subId, subPhoneNumber, timestamp);
|
||||
if (updatedMessageUri != null) {
|
||||
messageUri = updatedMessageUri;
|
||||
// To prevent Sync seeing inconsistent state must write to DB on this thread
|
||||
updateMessageUri(messageId, updatedMessageUri);
|
||||
|
||||
LogUtil.v(TAG, "SendMessageAction: Updated message " + messageId
|
||||
+ " with new uri " + messageUri);
|
||||
}
|
||||
}
|
||||
if (messageUri != null) {
|
||||
// Actually send the MMS
|
||||
final Bundle extras = new Bundle();
|
||||
extras.putString(EXTRA_MESSAGE_ID, messageId);
|
||||
extras.putParcelable(EXTRA_UPDATED_MESSAGE_URI, updatedMessageUri);
|
||||
final MmsUtils.StatusPlusUri result = MmsUtils.sendMmsMessage(context, subId,
|
||||
messageUri, extras);
|
||||
if (result == MmsUtils.STATUS_PENDING) {
|
||||
// Async send, so no status yet
|
||||
LogUtil.d(TAG, "SendMessageAction: Sending MMS message " + messageId
|
||||
+ " asynchronously; waiting for callback to finish processing");
|
||||
return null;
|
||||
}
|
||||
status = result.status;
|
||||
rawStatus = result.rawStatus;
|
||||
resultCode = result.resultCode;
|
||||
} else {
|
||||
status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
|
||||
}
|
||||
}
|
||||
|
||||
// When we fast-fail before calling the MMS lib APIs (e.g. airplane mode,
|
||||
// sending message is deleted).
|
||||
ProcessSentMessageAction.processMessageSentFastFailed(messageId, messageUri,
|
||||
updatedMessageUri, subId, isSms, status, rawStatus, resultCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
private void updateMessageUri(final String messageId, final Uri updatedMessageUri) {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(MessageColumns.SMS_MESSAGE_URI, updatedMessageUri.toString());
|
||||
BugleDatabaseOperations.updateMessageRow(db, messageId, values);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object processBackgroundResponse(final Bundle response) {
|
||||
// Nothing to do here, post-send tasks handled by ProcessSentMessageAction
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update message status to reflect success or failure
|
||||
*/
|
||||
@Override
|
||||
protected Object processBackgroundFailure() {
|
||||
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
|
||||
final MessageData message = actionParameters.getParcelable(KEY_MESSAGE, MessageData.class);
|
||||
final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS;
|
||||
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
final int resultCode = actionParameters.getInt(ProcessSentMessageAction.KEY_RESULT_CODE);
|
||||
final int httpStatusCode =
|
||||
actionParameters.getInt(ProcessSentMessageAction.KEY_HTTP_STATUS_CODE);
|
||||
|
||||
ProcessSentMessageAction.processResult(messageId, null /* updatedMessageUri */,
|
||||
MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED,
|
||||
isSms, this, subId, resultCode, httpStatusCode);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the message status (and message itself if necessary)
|
||||
* @param isSms whether this is an SMS or MMS
|
||||
* @param message message to update
|
||||
* @param updatedMessageUri message uri for newly-inserted messages; null otherwise
|
||||
* @param clearSeen whether the message 'seen' status should be reset if error occurs
|
||||
*/
|
||||
public static boolean updateMessageAndStatus(final boolean isSms, final MessageData message,
|
||||
final Uri updatedMessageUri, final boolean clearSeen) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
// TODO: We're optimistically setting the type/box of outgoing messages to
|
||||
// 'SENT' even before they actually are. We should technically be using QUEUED or OUTBOX
|
||||
// instead, but if we do that, it's possible that the Messaging app will try to send them
|
||||
// as part of its clean-up logic that runs when it starts (http://b/18155366).
|
||||
//
|
||||
// We also use the wrong status when inserting queued SMS messages in
|
||||
// InsertNewMessageAction.insertBroadcastSmsMessage and insertSendingSmsMessage (should be
|
||||
// QUEUED or OUTBOX), and in MmsUtils.insertSendReq (should be OUTBOX).
|
||||
|
||||
boolean updatedTelephony = true;
|
||||
int messageBox;
|
||||
int type;
|
||||
switch(message.getStatus()) {
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE:
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED:
|
||||
type = Sms.MESSAGE_TYPE_SENT;
|
||||
messageBox = Mms.MESSAGE_BOX_SENT;
|
||||
break;
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND:
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
|
||||
type = Sms.MESSAGE_TYPE_SENT;
|
||||
messageBox = Mms.MESSAGE_BOX_SENT;
|
||||
break;
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_SENDING:
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_RESENDING:
|
||||
type = Sms.MESSAGE_TYPE_SENT;
|
||||
messageBox = Mms.MESSAGE_BOX_SENT;
|
||||
break;
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_FAILED:
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
|
||||
type = Sms.MESSAGE_TYPE_FAILED;
|
||||
messageBox = Mms.MESSAGE_BOX_FAILED;
|
||||
break;
|
||||
default:
|
||||
type = Sms.MESSAGE_TYPE_ALL;
|
||||
messageBox = Mms.MESSAGE_BOX_ALL;
|
||||
break;
|
||||
}
|
||||
// First in the telephony DB
|
||||
if (isSms) {
|
||||
// Ignore update message Uri
|
||||
if (type != Sms.MESSAGE_TYPE_ALL) {
|
||||
if (!MmsUtils.updateSmsMessageSendingStatus(context, message.getSmsMessageUri(),
|
||||
type, message.getReceivedTimeStamp())) {
|
||||
message.markMessageFailed(message.getSentTimeStamp());
|
||||
updatedTelephony = false;
|
||||
}
|
||||
}
|
||||
} else if (message.getSmsMessageUri() != null) {
|
||||
if (messageBox != Mms.MESSAGE_BOX_ALL) {
|
||||
if (!MmsUtils.updateMmsMessageSendingStatus(context, message.getSmsMessageUri(),
|
||||
messageBox, message.getReceivedTimeStamp())) {
|
||||
message.markMessageFailed(message.getSentTimeStamp());
|
||||
updatedTelephony = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updatedTelephony) {
|
||||
LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS")
|
||||
+ " message " + message.getMessageId()
|
||||
+ " in telephony (" + message.getSmsMessageUri() + ")");
|
||||
} else {
|
||||
LogUtil.w(TAG, "SendMessageAction: Failed to update " + (isSms ? "SMS" : "MMS")
|
||||
+ " message " + message.getMessageId()
|
||||
+ " in telephony (" + message.getSmsMessageUri() + "); marking message failed");
|
||||
}
|
||||
|
||||
// Update the local DB
|
||||
db.beginTransaction();
|
||||
try {
|
||||
if (updatedMessageUri != null) {
|
||||
// Update all message and part fields
|
||||
BugleDatabaseOperations.updateMessageInTransaction(db, message);
|
||||
BugleDatabaseOperations.refreshConversationMetadataInTransaction(
|
||||
db, message.getConversationId(), false/* shouldAutoSwitchSelfId */,
|
||||
false/*archived*/);
|
||||
} else {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(MessageColumns.STATUS, message.getStatus());
|
||||
|
||||
if (clearSeen) {
|
||||
// When a message fails to send, the message needs to
|
||||
// be unseen to be selected as an error notification.
|
||||
values.put(MessageColumns.SEEN, 0);
|
||||
}
|
||||
values.put(MessageColumns.RECEIVED_TIMESTAMP, message.getReceivedTimeStamp());
|
||||
values.put(MessageColumns.RAW_TELEPHONY_STATUS, message.getRawTelephonyStatus());
|
||||
|
||||
BugleDatabaseOperations.updateMessageRowIfExists(db, message.getMessageId(),
|
||||
values);
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS")
|
||||
+ " message " + message.getMessageId() + " in local db. Timestamp = "
|
||||
+ message.getReceivedTimeStamp());
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
|
||||
if (updatedMessageUri != null) {
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
}
|
||||
|
||||
return updatedTelephony;
|
||||
}
|
||||
|
||||
private SendMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<SendMessageAction> CREATOR = new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public SendMessageAction createFromParcel(final Parcel in) {
|
||||
return new SendMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageAction[] newArray(final int size) {
|
||||
return new SendMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,707 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 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.action;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.provider.Telephony.Mms;
|
||||
import android.provider.Telephony.Sms;
|
||||
import androidx.collection.LongSparseArray;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.SyncManager.ThreadInfoCache;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.mmslib.SqliteWrapper;
|
||||
import com.android.messaging.sms.DatabaseMessages;
|
||||
import com.android.messaging.sms.DatabaseMessages.DatabaseMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.LocalDatabaseMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.MmsMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.SmsMessage;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Class holding a pair of cursors - one for local db and one for telephony provider - allowing
|
||||
* synchronous stepping through messages as part of sync.
|
||||
*/
|
||||
class SyncCursorPair {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
static final long SYNC_COMPLETE = -1L;
|
||||
static final long SYNC_STARTING = Long.MAX_VALUE;
|
||||
|
||||
private CursorIterator mLocalCursorIterator;
|
||||
private CursorIterator mRemoteCursorsIterator;
|
||||
|
||||
private final String mLocalSelection;
|
||||
private final String mRemoteSmsSelection;
|
||||
private final String mRemoteMmsSelection;
|
||||
|
||||
/**
|
||||
* Check if SMS has been synchronized. We compare the counts of messages on both
|
||||
* sides and return true if they are equal.
|
||||
*
|
||||
* Note that this may not be the most reliable way to tell if messages are in sync.
|
||||
* For example, the local misses one message and has one obsolete message.
|
||||
* However, we have background sms sync once a while, also some other events might
|
||||
* trigger a full sync. So we will eventually catch up. And this should be rare to
|
||||
* happen.
|
||||
*
|
||||
* @return If sms is in sync with telephony sms/mms providers
|
||||
*/
|
||||
static boolean allSynchronized(final DatabaseWrapper db) {
|
||||
return isSynchronized(db, LOCAL_MESSAGES_SELECTION, null,
|
||||
getSmsTypeSelectionSql(), null, getMmsTypeSelectionSql(), null);
|
||||
}
|
||||
|
||||
SyncCursorPair(final long lowerBound, final long upperBound) {
|
||||
mLocalSelection = getTimeConstrainedQuery(
|
||||
LOCAL_MESSAGES_SELECTION,
|
||||
MessageColumns.RECEIVED_TIMESTAMP,
|
||||
lowerBound,
|
||||
upperBound,
|
||||
null /* threadColumn */, null /* threadId */);
|
||||
mRemoteSmsSelection = getTimeConstrainedQuery(
|
||||
getSmsTypeSelectionSql(),
|
||||
"date",
|
||||
lowerBound,
|
||||
upperBound,
|
||||
null /* threadColumn */, null /* threadId */);
|
||||
mRemoteMmsSelection = getTimeConstrainedQuery(
|
||||
getMmsTypeSelectionSql(),
|
||||
"date",
|
||||
((lowerBound < 0) ? lowerBound : (lowerBound + 999) / 1000), /*seconds*/
|
||||
((upperBound < 0) ? upperBound : (upperBound + 999) / 1000), /*seconds*/
|
||||
null /* threadColumn */, null /* threadId */);
|
||||
}
|
||||
|
||||
SyncCursorPair(final long threadId, final String conversationId) {
|
||||
mLocalSelection = getTimeConstrainedQuery(
|
||||
LOCAL_MESSAGES_SELECTION,
|
||||
MessageColumns.RECEIVED_TIMESTAMP,
|
||||
-1L,
|
||||
-1L,
|
||||
MessageColumns.CONVERSATION_ID, conversationId);
|
||||
// Find all SMS messages (excluding drafts) within the sync window
|
||||
mRemoteSmsSelection = getTimeConstrainedQuery(
|
||||
getSmsTypeSelectionSql(),
|
||||
"date",
|
||||
-1L,
|
||||
-1L,
|
||||
Sms.THREAD_ID, Long.toString(threadId));
|
||||
mRemoteMmsSelection = getTimeConstrainedQuery(
|
||||
getMmsTypeSelectionSql(),
|
||||
"date",
|
||||
-1L, /*seconds*/
|
||||
-1L, /*seconds*/
|
||||
Mms.THREAD_ID, Long.toString(threadId));
|
||||
}
|
||||
|
||||
void query(final DatabaseWrapper db) {
|
||||
// Load local messages in the sync window
|
||||
mLocalCursorIterator = new LocalCursorIterator(db, mLocalSelection);
|
||||
// Load remote messages in the sync window
|
||||
mRemoteCursorsIterator = new RemoteCursorsIterator(mRemoteSmsSelection,
|
||||
mRemoteMmsSelection);
|
||||
}
|
||||
|
||||
boolean isSynchronized(final DatabaseWrapper db) {
|
||||
return isSynchronized(db, mLocalSelection, null, mRemoteSmsSelection,
|
||||
null, mRemoteMmsSelection, null);
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (mLocalCursorIterator != null) {
|
||||
mLocalCursorIterator.close();
|
||||
}
|
||||
if (mRemoteCursorsIterator != null) {
|
||||
mRemoteCursorsIterator.close();
|
||||
}
|
||||
}
|
||||
|
||||
long scan(final int maxMessagesToScan,
|
||||
final int maxMessagesToUpdate, final ArrayList<SmsMessage> smsToAdd,
|
||||
final LongSparseArray<MmsMessage> mmsToAdd,
|
||||
final ArrayList<LocalDatabaseMessage> messagesToDelete,
|
||||
final SyncManager.ThreadInfoCache threadInfoCache) {
|
||||
// Set of local messages matched with the timestamp of a remote message
|
||||
final Set<DatabaseMessage> matchedLocalMessages = Sets.newHashSet();
|
||||
// Set of remote messages matched with the timestamp of a local message
|
||||
final Set<DatabaseMessage> matchedRemoteMessages = Sets.newHashSet();
|
||||
long lastTimestampMillis = SYNC_STARTING;
|
||||
// Number of messages scanned local and remote
|
||||
int localCount = 0;
|
||||
int remoteCount = 0;
|
||||
// Seed the initial values of remote and local messages for comparison
|
||||
DatabaseMessage remoteMessage = mRemoteCursorsIterator.next();
|
||||
DatabaseMessage localMessage = mLocalCursorIterator.next();
|
||||
// Iterate through messages on both sides in reverse time order
|
||||
// Import messages in remote not in local, delete messages in local not in remote
|
||||
while (localCount + remoteCount < maxMessagesToScan && smsToAdd.size()
|
||||
+ mmsToAdd.size() + messagesToDelete.size() < maxMessagesToUpdate) {
|
||||
if (remoteMessage == null && localMessage == null) {
|
||||
// No more message on both sides - scan complete
|
||||
lastTimestampMillis = SYNC_COMPLETE;
|
||||
break;
|
||||
} else if ((remoteMessage == null && localMessage != null) ||
|
||||
(localMessage != null && remoteMessage != null &&
|
||||
localMessage.getTimestampInMillis()
|
||||
> remoteMessage.getTimestampInMillis())) {
|
||||
// Found a local message that is not in remote db
|
||||
// Delete the local message
|
||||
messagesToDelete.add((LocalDatabaseMessage) localMessage);
|
||||
lastTimestampMillis = Math.min(lastTimestampMillis,
|
||||
localMessage.getTimestampInMillis());
|
||||
// Advance to next local message
|
||||
localMessage = mLocalCursorIterator.next();
|
||||
localCount += 1;
|
||||
} else if ((localMessage == null && remoteMessage != null) ||
|
||||
(localMessage != null && remoteMessage != null &&
|
||||
localMessage.getTimestampInMillis()
|
||||
< remoteMessage.getTimestampInMillis())) {
|
||||
// Found a remote message that is not in local db
|
||||
// Add the remote message
|
||||
saveMessageToAdd(smsToAdd, mmsToAdd, remoteMessage, threadInfoCache);
|
||||
lastTimestampMillis = Math.min(lastTimestampMillis,
|
||||
remoteMessage.getTimestampInMillis());
|
||||
// Advance to next remote message
|
||||
remoteMessage = mRemoteCursorsIterator.next();
|
||||
remoteCount += 1;
|
||||
} else {
|
||||
// Found remote and local messages at the same timestamp
|
||||
final long matchedTimestamp = localMessage.getTimestampInMillis();
|
||||
lastTimestampMillis = Math.min(lastTimestampMillis, matchedTimestamp);
|
||||
// Get the next local and remote messages
|
||||
final DatabaseMessage remoteMessagePeek = mRemoteCursorsIterator.next();
|
||||
final DatabaseMessage localMessagePeek = mLocalCursorIterator.next();
|
||||
// Check if only one message on each side matches the current timestamp
|
||||
// by looking at the next messages on both sides. If they are either null
|
||||
// (meaning no more messages) or having a different timestamp. We want
|
||||
// to optimize for this since this is the most common case when majority
|
||||
// of the messages are in sync (so they one-to-one pair up at each timestamp),
|
||||
// by not allocating the data structures required to compare a set of
|
||||
// messages from both sides.
|
||||
if ((remoteMessagePeek == null ||
|
||||
remoteMessagePeek.getTimestampInMillis() != matchedTimestamp) &&
|
||||
(localMessagePeek == null ||
|
||||
localMessagePeek.getTimestampInMillis() != matchedTimestamp)) {
|
||||
// Optimize the common case where only one message on each side
|
||||
// that matches the same timestamp
|
||||
if (!remoteMessage.equals(localMessage)) {
|
||||
// local != remote
|
||||
// Delete local message
|
||||
messagesToDelete.add((LocalDatabaseMessage) localMessage);
|
||||
// Add remote message
|
||||
saveMessageToAdd(smsToAdd, mmsToAdd, remoteMessage, threadInfoCache);
|
||||
}
|
||||
// Get next local and remote messages
|
||||
localMessage = localMessagePeek;
|
||||
remoteMessage = remoteMessagePeek;
|
||||
localCount += 1;
|
||||
remoteCount += 1;
|
||||
} else {
|
||||
// Rare case in which multiple messages are in the same timestamp
|
||||
// on either or both sides
|
||||
// Gather all the matched remote messages
|
||||
matchedRemoteMessages.clear();
|
||||
matchedRemoteMessages.add(remoteMessage);
|
||||
remoteCount += 1;
|
||||
remoteMessage = remoteMessagePeek;
|
||||
while (remoteMessage != null &&
|
||||
remoteMessage.getTimestampInMillis() == matchedTimestamp) {
|
||||
Assert.isTrue(!matchedRemoteMessages.contains(remoteMessage));
|
||||
matchedRemoteMessages.add(remoteMessage);
|
||||
remoteCount += 1;
|
||||
remoteMessage = mRemoteCursorsIterator.next();
|
||||
}
|
||||
// Gather all the matched local messages
|
||||
matchedLocalMessages.clear();
|
||||
matchedLocalMessages.add(localMessage);
|
||||
localCount += 1;
|
||||
localMessage = localMessagePeek;
|
||||
while (localMessage != null &&
|
||||
localMessage.getTimestampInMillis() == matchedTimestamp) {
|
||||
if (matchedLocalMessages.contains(localMessage)) {
|
||||
// Duplicate message is local database is deleted
|
||||
messagesToDelete.add((LocalDatabaseMessage) localMessage);
|
||||
} else {
|
||||
matchedLocalMessages.add(localMessage);
|
||||
}
|
||||
localCount += 1;
|
||||
localMessage = mLocalCursorIterator.next();
|
||||
}
|
||||
// Delete messages local only
|
||||
for (final DatabaseMessage msg : Sets.difference(
|
||||
matchedLocalMessages, matchedRemoteMessages)) {
|
||||
messagesToDelete.add((LocalDatabaseMessage) msg);
|
||||
}
|
||||
// Add messages remote only
|
||||
for (final DatabaseMessage msg : Sets.difference(
|
||||
matchedRemoteMessages, matchedLocalMessages)) {
|
||||
saveMessageToAdd(smsToAdd, mmsToAdd, msg, threadInfoCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return lastTimestampMillis;
|
||||
}
|
||||
|
||||
DatabaseMessage getLocalMessage() {
|
||||
return mLocalCursorIterator.next();
|
||||
}
|
||||
|
||||
DatabaseMessage getRemoteMessage() {
|
||||
return mRemoteCursorsIterator.next();
|
||||
}
|
||||
|
||||
int getLocalPosition() {
|
||||
return mLocalCursorIterator.getPosition();
|
||||
}
|
||||
|
||||
int getRemotePosition() {
|
||||
return mRemoteCursorsIterator.getPosition();
|
||||
}
|
||||
|
||||
int getLocalCount() {
|
||||
return mLocalCursorIterator.getCount();
|
||||
}
|
||||
|
||||
int getRemoteCount() {
|
||||
return mRemoteCursorsIterator.getCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* An iterator for a database cursor
|
||||
*/
|
||||
interface CursorIterator {
|
||||
/**
|
||||
* Move to next element in the cursor
|
||||
*
|
||||
* @return The next element (which becomes the current)
|
||||
*/
|
||||
DatabaseMessage next();
|
||||
/**
|
||||
* Close the cursor
|
||||
*/
|
||||
void close();
|
||||
/**
|
||||
* Get the position
|
||||
*/
|
||||
int getPosition();
|
||||
/**
|
||||
* Get the count
|
||||
*/
|
||||
int getCount();
|
||||
}
|
||||
|
||||
private static final String ORDER_BY_DATE_DESC = "date DESC";
|
||||
|
||||
// A subquery that selects SMS/MMS messages in Bugle which are also in telephony
|
||||
private static final String LOCAL_MESSAGES_SELECTION = String.format(
|
||||
Locale.US,
|
||||
"(%s NOTNULL)",
|
||||
MessageColumns.SMS_MESSAGE_URI);
|
||||
|
||||
private static final String ORDER_BY_TIMESTAMP_DESC =
|
||||
MessageColumns.RECEIVED_TIMESTAMP + " DESC";
|
||||
|
||||
// TODO : This should move into the provider
|
||||
private static class LocalMessageQuery {
|
||||
private static final String[] PROJECTION = new String[] {
|
||||
MessageColumns._ID,
|
||||
MessageColumns.RECEIVED_TIMESTAMP,
|
||||
MessageColumns.SMS_MESSAGE_URI,
|
||||
MessageColumns.PROTOCOL,
|
||||
MessageColumns.CONVERSATION_ID,
|
||||
};
|
||||
private static final int INDEX_MESSAGE_ID = 0;
|
||||
private static final int INDEX_MESSAGE_TIMESTAMP = 1;
|
||||
private static final int INDEX_SMS_MESSAGE_URI = 2;
|
||||
private static final int INDEX_MESSAGE_SMS_TYPE = 3;
|
||||
private static final int INDEX_CONVERSATION_ID = 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class provides the same DatabaseMessage interface over a local SMS db message
|
||||
*/
|
||||
private static LocalDatabaseMessage getLocalDatabaseMessage(final Cursor cursor) {
|
||||
if (cursor == null) {
|
||||
return null;
|
||||
}
|
||||
return new LocalDatabaseMessage(
|
||||
cursor.getLong(LocalMessageQuery.INDEX_MESSAGE_ID),
|
||||
cursor.getInt(LocalMessageQuery.INDEX_MESSAGE_SMS_TYPE),
|
||||
cursor.getString(LocalMessageQuery.INDEX_SMS_MESSAGE_URI),
|
||||
cursor.getLong(LocalMessageQuery.INDEX_MESSAGE_TIMESTAMP),
|
||||
cursor.getString(LocalMessageQuery.INDEX_CONVERSATION_ID));
|
||||
}
|
||||
|
||||
/**
|
||||
* The buffered cursor iterator for local SMS
|
||||
*/
|
||||
private static class LocalCursorIterator implements CursorIterator {
|
||||
private Cursor mCursor;
|
||||
private final DatabaseWrapper mDatabase;
|
||||
|
||||
LocalCursorIterator(final DatabaseWrapper database, final String selection)
|
||||
throws SQLiteException {
|
||||
mDatabase = database;
|
||||
try {
|
||||
LogUtil.v(TAG, "SyncCursorPair: Querying for local messages; selection = "
|
||||
+ selection);
|
||||
mCursor = mDatabase.query(
|
||||
DatabaseHelper.MESSAGES_TABLE,
|
||||
LocalMessageQuery.PROJECTION,
|
||||
selection,
|
||||
null /*selectionArgs*/,
|
||||
null/*groupBy*/,
|
||||
null/*having*/,
|
||||
ORDER_BY_TIMESTAMP_DESC);
|
||||
} catch (final SQLiteException e) {
|
||||
LogUtil.e(TAG, "SyncCursorPair: failed to query local sms/mms", e);
|
||||
// Can't query local database. So let's throw up the exception and abort sync
|
||||
// because we may end up import duplicate messages.
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseMessage next() {
|
||||
if (mCursor != null && mCursor.moveToNext()) {
|
||||
return getLocalDatabaseMessage(mCursor);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return (mCursor == null ? 0 : mCursor.getCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPosition() {
|
||||
return (mCursor == null ? 0 : mCursor.getPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (mCursor != null) {
|
||||
mCursor.close();
|
||||
mCursor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The cursor iterator for remote sms.
|
||||
* Since SMS and MMS are stored in different tables in telephony provider,
|
||||
* this class merges the two cursors and provides a unified view of messages
|
||||
* from both cursors. Note that the order is DESC.
|
||||
*/
|
||||
private static class RemoteCursorsIterator implements CursorIterator {
|
||||
private Cursor mSmsCursor;
|
||||
private Cursor mMmsCursor;
|
||||
private DatabaseMessage mNextSms;
|
||||
private DatabaseMessage mNextMms;
|
||||
|
||||
RemoteCursorsIterator(final String smsSelection, final String mmsSelection)
|
||||
throws SQLiteException {
|
||||
mSmsCursor = null;
|
||||
mMmsCursor = null;
|
||||
try {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
LogUtil.v(TAG, "SyncCursorPair: Querying for remote SMS; selection = "
|
||||
+ smsSelection);
|
||||
mSmsCursor = SqliteWrapper.query(
|
||||
context,
|
||||
context.getContentResolver(),
|
||||
Sms.CONTENT_URI,
|
||||
SmsMessage.getProjection(),
|
||||
smsSelection,
|
||||
null /* selectionArgs */,
|
||||
ORDER_BY_DATE_DESC);
|
||||
if (mSmsCursor == null) {
|
||||
LogUtil.w(TAG, "SyncCursorPair: Remote SMS query returned null cursor; "
|
||||
+ "need to cancel sync");
|
||||
throw new RuntimeException("Null cursor from remote SMS query");
|
||||
}
|
||||
LogUtil.v(TAG, "SyncCursorPair: Querying for remote MMS; selection = "
|
||||
+ mmsSelection);
|
||||
mMmsCursor = SqliteWrapper.query(
|
||||
context,
|
||||
context.getContentResolver(),
|
||||
Mms.CONTENT_URI,
|
||||
DatabaseMessages.MmsMessage.getProjection(),
|
||||
mmsSelection,
|
||||
null /* selectionArgs */,
|
||||
ORDER_BY_DATE_DESC);
|
||||
if (mMmsCursor == null) {
|
||||
LogUtil.w(TAG, "SyncCursorPair: Remote MMS query returned null cursor; "
|
||||
+ "need to cancel sync");
|
||||
throw new RuntimeException("Null cursor from remote MMS query");
|
||||
}
|
||||
// Move to the first element in the combined stream from both cursors
|
||||
mNextSms = getSmsCursorNext();
|
||||
mNextMms = getMmsCursorNext();
|
||||
} catch (final SQLiteException e) {
|
||||
LogUtil.e(TAG, "SyncCursorPair: failed to query remote messages", e);
|
||||
// If we ignore this, the following code would think there is no remote message
|
||||
// and will delete all the local sms. We should be cautious here. So instead,
|
||||
// let's throw the exception to the caller and abort sms sync. We do the same
|
||||
// thing if either of the remote cursors is null.
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseMessage next() {
|
||||
DatabaseMessage result;
|
||||
if (mNextSms != null && mNextMms != null) {
|
||||
if (mNextSms.getTimestampInMillis() >= mNextMms.getTimestampInMillis()) {
|
||||
result = mNextSms;
|
||||
mNextSms = getSmsCursorNext();
|
||||
} else {
|
||||
result = mNextMms;
|
||||
mNextMms = getMmsCursorNext();
|
||||
}
|
||||
} else {
|
||||
if (mNextSms != null) {
|
||||
result = mNextSms;
|
||||
mNextSms = getSmsCursorNext();
|
||||
} else {
|
||||
result = mNextMms;
|
||||
mNextMms = getMmsCursorNext();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private DatabaseMessage getSmsCursorNext() {
|
||||
if (mSmsCursor != null && mSmsCursor.moveToNext()) {
|
||||
return SmsMessage.get(mSmsCursor);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private DatabaseMessage getMmsCursorNext() {
|
||||
if (mMmsCursor != null && mMmsCursor.moveToNext()) {
|
||||
return MmsMessage.get(mMmsCursor);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
// Return approximate cursor position allowing for read ahead on two cursors (hence -1)
|
||||
public int getPosition() {
|
||||
return (mSmsCursor == null ? 0 : mSmsCursor.getPosition()) +
|
||||
(mMmsCursor == null ? 0 : mMmsCursor.getPosition()) - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return (mSmsCursor == null ? 0 : mSmsCursor.getCount()) +
|
||||
(mMmsCursor == null ? 0 : mMmsCursor.getCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (mSmsCursor != null) {
|
||||
mSmsCursor.close();
|
||||
mSmsCursor = null;
|
||||
}
|
||||
if (mMmsCursor != null) {
|
||||
mMmsCursor.close();
|
||||
mMmsCursor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type selection for importing sms messages. Only SENT and INBOX messages are imported.
|
||||
*
|
||||
* @return The SQL selection for importing sms messages
|
||||
*/
|
||||
public static String getSmsTypeSelectionSql() {
|
||||
return MmsUtils.getSmsTypeSelectionSql();
|
||||
}
|
||||
|
||||
/**
|
||||
* Type selection for importing mms messages.
|
||||
*
|
||||
* Criteria:
|
||||
* MESSAGE_BOX is INBOX, SENT or OUTBOX
|
||||
* MESSAGE_TYPE is SEND_REQ (sent), RETRIEVE_CONF (received) or NOTIFICATION_IND (download)
|
||||
*
|
||||
* @return The SQL selection for importing mms messages. This selects the message type,
|
||||
* not including the selection on timestamp.
|
||||
*/
|
||||
public static String getMmsTypeSelectionSql() {
|
||||
return MmsUtils.getMmsTypeSelectionSql();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a SQL selection string using an existing selection and time window limits
|
||||
* The limits are not applied if the value is < 0
|
||||
*
|
||||
* @param typeSelection The existing selection
|
||||
* @param from The inclusive lower bound
|
||||
* @param to The exclusive upper bound
|
||||
* @return The created SQL selection
|
||||
*/
|
||||
private static String getTimeConstrainedQuery(final String typeSelection,
|
||||
final String timeColumn, final long from, final long to,
|
||||
final String threadColumn, final String threadId) {
|
||||
final StringBuilder queryBuilder = new StringBuilder();
|
||||
queryBuilder.append(typeSelection);
|
||||
if (from > 0) {
|
||||
queryBuilder.append(" AND ").append(timeColumn).append(">=").append(from);
|
||||
}
|
||||
if (to > 0) {
|
||||
queryBuilder.append(" AND ").append(timeColumn).append("<").append(to);
|
||||
}
|
||||
if (!TextUtils.isEmpty(threadColumn) && !TextUtils.isEmpty(threadId)) {
|
||||
queryBuilder.append(" AND ").append(threadColumn).append("=").append(threadId);
|
||||
}
|
||||
return queryBuilder.toString();
|
||||
}
|
||||
|
||||
private static final String[] COUNT_PROJECTION = new String[] { "count()" };
|
||||
|
||||
private static int getCountFromCursor(final Cursor cursor) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
// We should only return a number if we were able to read it from the cursor.
|
||||
// Otherwise, we throw an exception to cancel the sync.
|
||||
String cursorDesc = "";
|
||||
if (cursor == null) {
|
||||
cursorDesc = "null";
|
||||
} else if (cursor.getCount() == 0) {
|
||||
cursorDesc = "empty";
|
||||
}
|
||||
throw new IllegalArgumentException("Cannot get count from " + cursorDesc + " cursor");
|
||||
}
|
||||
|
||||
private void saveMessageToAdd(final List<SmsMessage> smsToAdd,
|
||||
final LongSparseArray<MmsMessage> mmsToAdd, final DatabaseMessage message,
|
||||
final ThreadInfoCache threadInfoCache) {
|
||||
long threadId;
|
||||
if (message.getProtocol() == MessageData.PROTOCOL_MMS) {
|
||||
final MmsMessage mms = (MmsMessage) message;
|
||||
mmsToAdd.append(mms.getId(), mms);
|
||||
threadId = mms.mThreadId;
|
||||
} else {
|
||||
final SmsMessage sms = (SmsMessage) message;
|
||||
smsToAdd.add(sms);
|
||||
threadId = sms.mThreadId;
|
||||
}
|
||||
// Cache the lookup and canonicalization of the phone number outside of the transaction...
|
||||
threadInfoCache.getThreadRecipients(threadId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SMS has been synchronized. We compare the counts of messages on both
|
||||
* sides and return true if they are equal.
|
||||
*
|
||||
* Note that this may not be the most reliable way to tell if messages are in sync.
|
||||
* For example, the local misses one message and has one obsolete message.
|
||||
* However, we have background sms sync once a while, also some other events might
|
||||
* trigger a full sync. So we will eventually catch up. And this should be rare to
|
||||
* happen.
|
||||
*
|
||||
* @return If sms is in sync with telephony sms/mms providers
|
||||
*/
|
||||
private static boolean isSynchronized(final DatabaseWrapper db, final String localSelection,
|
||||
final String[] localSelectionArgs, final String smsSelection,
|
||||
final String[] smsSelectionArgs, final String mmsSelection,
|
||||
final String[] mmsSelectionArgs) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
Cursor localCursor = null;
|
||||
Cursor remoteSmsCursor = null;
|
||||
Cursor remoteMmsCursor = null;
|
||||
try {
|
||||
localCursor = db.query(
|
||||
DatabaseHelper.MESSAGES_TABLE,
|
||||
COUNT_PROJECTION,
|
||||
localSelection,
|
||||
localSelectionArgs,
|
||||
null/*groupBy*/,
|
||||
null/*having*/,
|
||||
null/*orderBy*/);
|
||||
final int localCount = getCountFromCursor(localCursor);
|
||||
remoteSmsCursor = SqliteWrapper.query(
|
||||
context,
|
||||
context.getContentResolver(),
|
||||
Sms.CONTENT_URI,
|
||||
COUNT_PROJECTION,
|
||||
smsSelection,
|
||||
smsSelectionArgs,
|
||||
null/*orderBy*/);
|
||||
final int smsCount = getCountFromCursor(remoteSmsCursor);
|
||||
remoteMmsCursor = SqliteWrapper.query(
|
||||
context,
|
||||
context.getContentResolver(),
|
||||
Mms.CONTENT_URI,
|
||||
COUNT_PROJECTION,
|
||||
mmsSelection,
|
||||
mmsSelectionArgs,
|
||||
null/*orderBy*/);
|
||||
final int mmsCount = getCountFromCursor(remoteMmsCursor);
|
||||
final int remoteCount = smsCount + mmsCount;
|
||||
final boolean isInSync = (localCount == remoteCount);
|
||||
if (isInSync) {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncCursorPair: Same # of local and remote messages = "
|
||||
+ localCount);
|
||||
}
|
||||
} else {
|
||||
LogUtil.i(TAG, "SyncCursorPair: Not in sync; # local messages = " + localCount
|
||||
+ ", # remote message = " + remoteCount);
|
||||
}
|
||||
return isInSync;
|
||||
} catch (final Exception e) {
|
||||
LogUtil.e(TAG, "SyncCursorPair: failed to query local or remote message counts", e);
|
||||
// If something is wrong in querying database, assume we are synced so
|
||||
// we don't retry indefinitely
|
||||
} finally {
|
||||
if (localCursor != null) {
|
||||
localCursor.close();
|
||||
}
|
||||
if (remoteSmsCursor != null) {
|
||||
remoteSmsCursor.close();
|
||||
}
|
||||
if (remoteMmsCursor != null) {
|
||||
remoteMmsCursor.close();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 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.action;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.provider.Telephony;
|
||||
import android.provider.Telephony.Mms;
|
||||
import android.provider.Telephony.Sms;
|
||||
import android.support.v7.mms.pdu.PduHeaders;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.SyncManager.ThreadInfoCache;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.DatabaseMessages.LocalDatabaseMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.MmsMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.SmsMessage;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Update local database with a batch of messages to add/delete in one transaction
|
||||
*/
|
||||
class SyncMessageBatch {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
// Variables used during executeAction
|
||||
private final HashSet<String> mConversationsToUpdate;
|
||||
// Cache of thread->conversationId map
|
||||
private final ThreadInfoCache mCache;
|
||||
|
||||
// Set of SMS messages to add
|
||||
private final ArrayList<SmsMessage> mSmsToAdd;
|
||||
// Set of MMS messages to add
|
||||
private final ArrayList<MmsMessage> mMmsToAdd;
|
||||
// Set of local messages to delete
|
||||
private final ArrayList<LocalDatabaseMessage> mMessagesToDelete;
|
||||
|
||||
SyncMessageBatch(final ArrayList<SmsMessage> smsToAdd,
|
||||
final ArrayList<MmsMessage> mmsToAdd,
|
||||
final ArrayList<LocalDatabaseMessage> messagesToDelete,
|
||||
final ThreadInfoCache cache) {
|
||||
mSmsToAdd = smsToAdd;
|
||||
mMmsToAdd = mmsToAdd;
|
||||
mMessagesToDelete = messagesToDelete;
|
||||
mCache = cache;
|
||||
mConversationsToUpdate = new HashSet<>();
|
||||
}
|
||||
|
||||
void updateLocalDatabase() {
|
||||
// Perform local database changes in one transaction
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
// Store all the SMS messages
|
||||
for (final SmsMessage sms : mSmsToAdd) {
|
||||
storeSms(db, sms);
|
||||
}
|
||||
// Store all the MMS messages
|
||||
for (final MmsMessage mms : mMmsToAdd) {
|
||||
storeMms(db, mms);
|
||||
}
|
||||
// Keep track of conversations with messages deleted
|
||||
for (final LocalDatabaseMessage message : mMessagesToDelete) {
|
||||
mConversationsToUpdate.add(message.getConversationId());
|
||||
}
|
||||
// Batch delete local messages
|
||||
batchDelete(db, DatabaseHelper.MESSAGES_TABLE, MessageColumns._ID,
|
||||
messageListToIds(mMessagesToDelete));
|
||||
|
||||
for (final LocalDatabaseMessage message : mMessagesToDelete) {
|
||||
LogUtil.v(TAG, "SyncMessageBatch: Deleted message " + message.getLocalId()
|
||||
+ " for SMS/MMS " + message.getUri() + " with timestamp "
|
||||
+ message.getTimestampInMillis());
|
||||
}
|
||||
|
||||
// Update conversation state for imported messages, like snippet,
|
||||
updateConversations(db);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private static String[] messageListToIds(final List<LocalDatabaseMessage> messagesToDelete) {
|
||||
final String[] ids = new String[messagesToDelete.size()];
|
||||
for (int i = 0; i < ids.length; i++) {
|
||||
ids[i] = Long.toString(messagesToDelete.get(i).getLocalId());
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the SMS message into local database.
|
||||
*/
|
||||
private void storeSms(final DatabaseWrapper db, final SmsMessage sms) {
|
||||
if (sms.mBody == null) {
|
||||
LogUtil.w(TAG, "SyncMessageBatch: SMS " + sms.mUri + " has no body; adding empty one");
|
||||
// try to fix it
|
||||
sms.mBody = "";
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(sms.mAddress)) {
|
||||
LogUtil.e(TAG, "SyncMessageBatch: SMS has no address; using unknown sender");
|
||||
// try to fix it
|
||||
sms.mAddress = ParticipantData.getUnknownSenderDestination();
|
||||
}
|
||||
|
||||
// TODO : We need to also deal with messages in a failed/retry state
|
||||
final boolean isOutgoing = sms.mType != Sms.MESSAGE_TYPE_INBOX;
|
||||
|
||||
final String otherPhoneNumber = sms.mAddress;
|
||||
|
||||
// A forced resync of all messages should still keep the archived states.
|
||||
// The database upgrade code notifies sync manager of this. We need to
|
||||
// honor the original customization to this conversation if created.
|
||||
final String conversationId = mCache.getOrCreateConversation(db, sms.mThreadId, sms.mSubId,
|
||||
DataModel.get().getSyncManager().getCustomizationForThread(sms.mThreadId));
|
||||
if (conversationId == null) {
|
||||
// Cannot create conversation for this message? This should not happen.
|
||||
LogUtil.e(TAG, "SyncMessageBatch: Failed to create conversation for SMS thread "
|
||||
+ sms.mThreadId);
|
||||
return;
|
||||
}
|
||||
final ParticipantData self = ParticipantData.getSelfParticipant(sms.getSubId());
|
||||
final String selfId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
|
||||
final ParticipantData sender = isOutgoing ?
|
||||
self :
|
||||
ParticipantData.getFromRawPhoneBySimLocale(otherPhoneNumber, sms.getSubId());
|
||||
final String participantId = (isOutgoing ? selfId :
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, sender));
|
||||
|
||||
final int bugleStatus = bugleStatusForSms(isOutgoing, sms.mType, sms.mStatus);
|
||||
|
||||
final MessageData message = MessageData.createSmsMessage(
|
||||
sms.mUri,
|
||||
participantId,
|
||||
selfId,
|
||||
conversationId,
|
||||
bugleStatus,
|
||||
sms.mSeen,
|
||||
sms.mRead,
|
||||
sms.mTimestampSentInMillis,
|
||||
sms.mTimestampInMillis,
|
||||
sms.mBody);
|
||||
|
||||
// Inserting sms content into messages table
|
||||
try {
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
rethrowSQLiteConstraintExceptionWithDetails(e, db, sms.mUri, sms.mThreadId,
|
||||
conversationId, selfId, participantId);
|
||||
}
|
||||
|
||||
LogUtil.v(TAG, "SyncMessageBatch: Inserted new message " + message.getMessageId()
|
||||
+ " for SMS " + message.getSmsMessageUri() + " received at "
|
||||
+ message.getReceivedTimeStamp());
|
||||
|
||||
// Keep track of updated conversation for later updating the conversation snippet, etc.
|
||||
mConversationsToUpdate.add(conversationId);
|
||||
}
|
||||
|
||||
public static int bugleStatusForSms(final boolean isOutgoing, final int type,
|
||||
final int status) {
|
||||
int bugleStatus;
|
||||
// For a message we sync either
|
||||
if (isOutgoing) {
|
||||
// Outgoing message not yet been sent
|
||||
if (type == Telephony.Sms.MESSAGE_TYPE_FAILED
|
||||
|| type == Telephony.Sms.MESSAGE_TYPE_OUTBOX
|
||||
|| type == Telephony.Sms.MESSAGE_TYPE_QUEUED
|
||||
|| (type == Telephony.Sms.MESSAGE_TYPE_SENT
|
||||
&& status >= Telephony.Sms.STATUS_FAILED)) {
|
||||
// Not sent counts as failed and available for manual resend
|
||||
bugleStatus = MessageData.BUGLE_STATUS_OUTGOING_FAILED;
|
||||
} else if (status == Sms.STATUS_COMPLETE) {
|
||||
bugleStatus = MessageData.BUGLE_STATUS_OUTGOING_DELIVERED;
|
||||
} else {
|
||||
// Otherwise outgoing message is complete
|
||||
bugleStatus = MessageData.BUGLE_STATUS_OUTGOING_COMPLETE;
|
||||
}
|
||||
} else {
|
||||
// All incoming SMS messages are complete
|
||||
bugleStatus = MessageData.BUGLE_STATUS_INCOMING_COMPLETE;
|
||||
}
|
||||
return bugleStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the MMS message into local database
|
||||
*/
|
||||
private void storeMms(final DatabaseWrapper db, final MmsMessage mms) {
|
||||
if (mms.mParts.size() < 1) {
|
||||
LogUtil.w(TAG, "SyncMessageBatch: MMS " + mms.mUri + " has no parts");
|
||||
}
|
||||
|
||||
// TODO : We need to also deal with messages in a failed/retry state
|
||||
final boolean isOutgoing = mms.mType != Mms.MESSAGE_BOX_INBOX;
|
||||
final boolean isNotification = (mms.mMmsMessageType ==
|
||||
PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND);
|
||||
|
||||
final String senderId = mms.mSender;
|
||||
|
||||
// A forced resync of all messages should still keep the archived states.
|
||||
// The database upgrade code notifies sync manager of this. We need to
|
||||
// honor the original customization to this conversation if created.
|
||||
final String conversationId = mCache.getOrCreateConversation(db, mms.mThreadId, mms.mSubId,
|
||||
DataModel.get().getSyncManager().getCustomizationForThread(mms.mThreadId));
|
||||
if (conversationId == null) {
|
||||
LogUtil.e(TAG, "SyncMessageBatch: Failed to create conversation for MMS thread "
|
||||
+ mms.mThreadId);
|
||||
return;
|
||||
}
|
||||
final ParticipantData self = ParticipantData.getSelfParticipant(mms.getSubId());
|
||||
final String selfId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
|
||||
final ParticipantData sender = isOutgoing ?
|
||||
self : ParticipantData.getFromRawPhoneBySimLocale(senderId, mms.getSubId());
|
||||
final String participantId = (isOutgoing ? selfId :
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, sender));
|
||||
|
||||
final int bugleStatus = MmsUtils.bugleStatusForMms(isOutgoing, isNotification, mms.mType);
|
||||
|
||||
// Import message and all of the parts.
|
||||
// TODO : For now we are importing these in the order we found them in the MMS
|
||||
// database. Ideally we would load and parse the SMIL which describes how the parts relate
|
||||
// to one another.
|
||||
|
||||
// TODO: Need to set correct status on message
|
||||
final MessageData message = MmsUtils.createMmsMessage(mms, conversationId, participantId,
|
||||
selfId, bugleStatus);
|
||||
|
||||
// Inserting mms content into messages table
|
||||
try {
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
rethrowSQLiteConstraintExceptionWithDetails(e, db, mms.mUri, mms.mThreadId,
|
||||
conversationId, selfId, participantId);
|
||||
}
|
||||
|
||||
LogUtil.v(TAG, "SyncMessageBatch: Inserted new message " + message.getMessageId()
|
||||
+ " for MMS " + message.getSmsMessageUri() + " received at "
|
||||
+ message.getReceivedTimeStamp());
|
||||
|
||||
// Keep track of updated conversation for later updating the conversation snippet, etc.
|
||||
mConversationsToUpdate.add(conversationId);
|
||||
}
|
||||
|
||||
// TODO: Remove this after we no longer see this crash (b/18375758)
|
||||
private static void rethrowSQLiteConstraintExceptionWithDetails(SQLiteConstraintException e,
|
||||
DatabaseWrapper db, String messageUri, long threadId, String conversationId,
|
||||
String selfId, String senderId) {
|
||||
// Add some extra debug information to the exception for tracking down b/18375758.
|
||||
// The default detail message for SQLiteConstraintException tells us that a foreign
|
||||
// key constraint failed, but not which one! Messages have foreign keys to 3 tables:
|
||||
// conversations, participants (self), participants (sender). We'll query each one
|
||||
// to determine which one(s) violated the constraint, and then throw a new exception
|
||||
// with those details.
|
||||
|
||||
String foundConversationId = null;
|
||||
try (Cursor cursor = db.rawQuery("SELECT " + ConversationColumns._ID
|
||||
+ " FROM " + DatabaseHelper.CONVERSATIONS_TABLE
|
||||
+ " WHERE " + ConversationColumns._ID + "=" + conversationId,
|
||||
null)) {
|
||||
// Look for an existing conversation in the db with the conversation id
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
Assert.isTrue(cursor.getCount() == 1);
|
||||
foundConversationId = cursor.getString(0);
|
||||
}
|
||||
}
|
||||
|
||||
ParticipantData foundSelfParticipant =
|
||||
BugleDatabaseOperations.getExistingParticipant(db, selfId);
|
||||
ParticipantData foundSenderParticipant =
|
||||
BugleDatabaseOperations.getExistingParticipant(db, senderId);
|
||||
|
||||
String errorMsg = "SQLiteConstraintException while inserting message for " + messageUri
|
||||
+ "; conversation id from getOrCreateConversation = " + conversationId
|
||||
+ " (lookup thread = " + threadId + "), found conversation id = "
|
||||
+ foundConversationId + ", found self participant = "
|
||||
+ LogUtil.sanitizePII(foundSelfParticipant.getNormalizedDestination())
|
||||
+ " (lookup id = " + selfId + "), found sender participant = "
|
||||
+ LogUtil.sanitizePII(foundSenderParticipant.getNormalizedDestination())
|
||||
+ " (lookup id = " + senderId + ")";
|
||||
throw new RuntimeException(errorMsg, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the tracked latest message info to update conversations, including
|
||||
* latest chat message and sort timestamp.
|
||||
*/
|
||||
private void updateConversations(final DatabaseWrapper db) {
|
||||
for (final String conversationId : mConversationsToUpdate) {
|
||||
if (BugleDatabaseOperations.deleteConversationIfEmptyInTransaction(db,
|
||||
conversationId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final boolean archived = mCache.isArchived(conversationId);
|
||||
// Always attempt to auto-switch conversation self id for sync/import case.
|
||||
BugleDatabaseOperations.maybeRefreshConversationMetadataInTransaction(db,
|
||||
conversationId, true /*shouldAutoSwitchSelfId*/, archived /*keepArchived*/);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Batch delete database rows by matching a column with a list of values, usually some
|
||||
* kind of IDs.
|
||||
*
|
||||
* @return Total number of deleted messages
|
||||
*/
|
||||
private static int batchDelete(final DatabaseWrapper db, final String table,
|
||||
final String column, final String[] ids) {
|
||||
int totalDeleted = 0;
|
||||
final int totalIds = ids.length;
|
||||
for (int start = 0; start < totalIds; start += MmsUtils.MAX_IDS_PER_QUERY) {
|
||||
final int end = Math.min(start + MmsUtils.MAX_IDS_PER_QUERY, totalIds); //excluding
|
||||
final int count = end - start;
|
||||
final String batchSelection = String.format(
|
||||
Locale.US,
|
||||
"%s IN %s",
|
||||
column,
|
||||
MmsUtils.getSqlInOperand(count));
|
||||
final String[] batchSelectionArgs = Arrays.copyOfRange(ids, start, end);
|
||||
final int deleted = db.delete(
|
||||
table,
|
||||
batchSelection,
|
||||
batchSelectionArgs);
|
||||
totalDeleted += deleted;
|
||||
}
|
||||
return totalDeleted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.SystemClock;
|
||||
import android.provider.Telephony.Mms;
|
||||
import android.support.v7.mms.pdu.ContentType;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.collection.LongSparseArray;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.SyncManager.ThreadInfoCache;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.mmslib.SqliteWrapper;
|
||||
import com.android.messaging.sms.DatabaseMessages;
|
||||
import com.android.messaging.sms.DatabaseMessages.LocalDatabaseMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.MmsMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.SmsMessage;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Action used to sync messages from smsmms db to local database
|
||||
*/
|
||||
public class SyncMessagesAction extends Action implements Parcelable {
|
||||
static final long SYNC_FAILED = Long.MIN_VALUE;
|
||||
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
private static final String KEY_START_TIMESTAMP = "start_timestamp";
|
||||
private static final String KEY_MAX_UPDATE = "max_update";
|
||||
private static final String KEY_LOWER_BOUND = "lower_bound";
|
||||
private static final String KEY_UPPER_BOUND = "upper_bound";
|
||||
private static final String BUNDLE_KEY_LAST_TIMESTAMP = "last_timestamp";
|
||||
private static final String BUNDLE_KEY_SMS_MESSAGES = "sms_to_add";
|
||||
private static final String BUNDLE_KEY_MMS_MESSAGES = "mms_to_add";
|
||||
private static final String BUNDLE_KEY_MESSAGES_TO_DELETE = "messages_to_delete";
|
||||
|
||||
/**
|
||||
* Start a full sync (backed off a few seconds to avoid pulling sending/receiving messages).
|
||||
*/
|
||||
public static void fullSync() {
|
||||
final long smsSyncBackoffTimeMillis =
|
||||
BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS_DEFAULT;
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
// TODO: Could base this off most recent message in db but now should be okay...
|
||||
final long startTimestamp = now - smsSyncBackoffTimeMillis;
|
||||
|
||||
final SyncMessagesAction action = new SyncMessagesAction(-1L, startTimestamp,
|
||||
0, startTimestamp);
|
||||
action.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an incremental sync to pull messages since last sync (backed off a few seconds)..
|
||||
*/
|
||||
public static void sync() {
|
||||
final long smsSyncBackoffTimeMillis =
|
||||
BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS_DEFAULT;
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
// TODO: Could base this off most recent message in db but now should be okay...
|
||||
final long startTimestamp = now - smsSyncBackoffTimeMillis;
|
||||
|
||||
sync(startTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an incremental sync when the application starts up (no back off as not yet
|
||||
* sending/receiving).
|
||||
*/
|
||||
public static void immediateSync() {
|
||||
sync(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private static void sync(final long startTimestamp) {
|
||||
if (!OsUtil.hasSmsPermission()) {
|
||||
// Sync requires READ_SMS permission
|
||||
return;
|
||||
}
|
||||
|
||||
final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
|
||||
// Lower bound is end of previous sync
|
||||
final long syncLowerBoundTimeMillis = prefs.getLong(BuglePrefsKeys.LAST_SYNC_TIME,
|
||||
BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT);
|
||||
|
||||
final SyncMessagesAction action = new SyncMessagesAction(syncLowerBoundTimeMillis,
|
||||
startTimestamp, 0, startTimestamp);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private SyncMessagesAction(final long lowerBound, final long upperBound,
|
||||
final int maxMessagesToUpdate, final long startTimestamp) {
|
||||
actionParameters.putLong(KEY_LOWER_BOUND, lowerBound);
|
||||
actionParameters.putLong(KEY_UPPER_BOUND, upperBound);
|
||||
actionParameters.putInt(KEY_MAX_UPDATE, maxMessagesToUpdate);
|
||||
actionParameters.putLong(KEY_START_TIMESTAMP, startTimestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
long lowerBoundTimeMillis = actionParameters.getLong(KEY_LOWER_BOUND);
|
||||
final long upperBoundTimeMillis = actionParameters.getLong(KEY_UPPER_BOUND);
|
||||
final int initialMaxMessagesToUpdate = actionParameters.getInt(KEY_MAX_UPDATE);
|
||||
final long startTimestamp = actionParameters.getLong(KEY_START_TIMESTAMP);
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: Request to sync messages from "
|
||||
+ lowerBoundTimeMillis + " to " + upperBoundTimeMillis + " (start timestamp = "
|
||||
+ startTimestamp + ", message update limit = " + initialMaxMessagesToUpdate
|
||||
+ ")");
|
||||
}
|
||||
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
if (lowerBoundTimeMillis >= 0) {
|
||||
// Cursors
|
||||
final SyncCursorPair cursors = new SyncCursorPair(-1L, lowerBoundTimeMillis);
|
||||
final boolean inSync = cursors.isSynchronized(db);
|
||||
if (!inSync) {
|
||||
if (syncManager.delayUntilFullSync(startTimestamp) == 0) {
|
||||
lowerBoundTimeMillis = -1;
|
||||
actionParameters.putLong(KEY_LOWER_BOUND, lowerBoundTimeMillis);
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: Messages before "
|
||||
+ lowerBoundTimeMillis + " not in sync; promoting to full sync");
|
||||
}
|
||||
} else if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: Messages before "
|
||||
+ lowerBoundTimeMillis + " not in sync; will do incremental sync");
|
||||
}
|
||||
} else {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: Messages before " + lowerBoundTimeMillis
|
||||
+ " are in sync");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if sync allowed (can be too soon after last or one is already running)
|
||||
if (syncManager.shouldSync(lowerBoundTimeMillis < 0, startTimestamp)) {
|
||||
syncManager.startSyncBatch(upperBoundTimeMillis);
|
||||
requestBackgroundWork();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bundle doBackgroundWork() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
|
||||
final int maxMessagesToScan =
|
||||
BugleGservicesKeys.SMS_SYNC_BATCH_MAX_MESSAGES_TO_SCAN_DEFAULT;
|
||||
|
||||
final int initialMaxMessagesToUpdate = actionParameters.getInt(KEY_MAX_UPDATE);
|
||||
final int smsSyncSubsequentBatchSizeMin =
|
||||
BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MIN_DEFAULT;
|
||||
final int smsSyncSubsequentBatchSizeMax =
|
||||
BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MAX_DEFAULT;
|
||||
|
||||
// Cap sync size to GServices limits
|
||||
final int maxMessagesToUpdate = Math.max(smsSyncSubsequentBatchSizeMin,
|
||||
Math.min(initialMaxMessagesToUpdate, smsSyncSubsequentBatchSizeMax));
|
||||
|
||||
final long lowerBoundTimeMillis = actionParameters.getLong(KEY_LOWER_BOUND);
|
||||
final long upperBoundTimeMillis = actionParameters.getLong(KEY_UPPER_BOUND);
|
||||
|
||||
LogUtil.i(TAG, "SyncMessagesAction: Starting batch for messages from "
|
||||
+ lowerBoundTimeMillis + " to " + upperBoundTimeMillis
|
||||
+ " (message update limit = " + maxMessagesToUpdate + ", message scan limit = "
|
||||
+ maxMessagesToScan + ")");
|
||||
|
||||
// Clear last change time so that we can work out if this batch is dirty when it completes
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
|
||||
// Clear the singleton cache that maps threads to recipients and to conversations.
|
||||
final SyncManager.ThreadInfoCache cache = syncManager.getThreadInfoCache();
|
||||
cache.clear();
|
||||
|
||||
// Sms messages to store
|
||||
final ArrayList<SmsMessage> smsToAdd = new ArrayList<>();
|
||||
// Mms messages to store
|
||||
final LongSparseArray<MmsMessage> mmsToAdd = new LongSparseArray<>();
|
||||
// List of local SMS/MMS to remove
|
||||
final ArrayList<LocalDatabaseMessage> messagesToDelete = new ArrayList<>();
|
||||
|
||||
long lastTimestampMillis = SYNC_FAILED;
|
||||
if (syncManager.isSyncing(upperBoundTimeMillis)) {
|
||||
// Cursors
|
||||
final SyncCursorPair cursors = new SyncCursorPair(lowerBoundTimeMillis,
|
||||
upperBoundTimeMillis);
|
||||
|
||||
// Actually compare the messages using cursor pair
|
||||
lastTimestampMillis = syncCursorPair(db, cursors, smsToAdd, mmsToAdd,
|
||||
messagesToDelete, maxMessagesToScan, maxMessagesToUpdate, cache);
|
||||
}
|
||||
final Bundle response = new Bundle();
|
||||
|
||||
// If comparison succeeds bundle up the changes for processing in ActionService
|
||||
if (lastTimestampMillis > SYNC_FAILED) {
|
||||
final ArrayList<MmsMessage> mmsToAddList = new ArrayList<>();
|
||||
for (int i = 0; i < mmsToAdd.size(); i++) {
|
||||
final MmsMessage mms = mmsToAdd.valueAt(i);
|
||||
mmsToAddList.add(mms);
|
||||
}
|
||||
|
||||
response.putParcelableArrayList(BUNDLE_KEY_SMS_MESSAGES, smsToAdd);
|
||||
response.putParcelableArrayList(BUNDLE_KEY_MMS_MESSAGES, mmsToAddList);
|
||||
response.putParcelableArrayList(BUNDLE_KEY_MESSAGES_TO_DELETE, messagesToDelete);
|
||||
}
|
||||
response.putLong(BUNDLE_KEY_LAST_TIMESTAMP, lastTimestampMillis);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare messages based on timestamp and uri
|
||||
* @param db local database wrapper
|
||||
* @param cursors cursor pair holding references to local and remote messages
|
||||
* @param smsToAdd newly found sms messages to add
|
||||
* @param mmsToAdd newly found mms messages to add
|
||||
* @param messagesToDelete messages not found needing deletion
|
||||
* @param maxMessagesToScan max messages to scan for changes
|
||||
* @param maxMessagesToUpdate max messages to return for updates
|
||||
* @param cache cache for conversation id / thread id / recipient set mapping
|
||||
* @return timestamp of the oldest message seen during the sync scan
|
||||
*/
|
||||
private long syncCursorPair(final DatabaseWrapper db, final SyncCursorPair cursors,
|
||||
final ArrayList<SmsMessage> smsToAdd, final LongSparseArray<MmsMessage> mmsToAdd,
|
||||
final ArrayList<LocalDatabaseMessage> messagesToDelete, final int maxMessagesToScan,
|
||||
final int maxMessagesToUpdate, final ThreadInfoCache cache) {
|
||||
long lastTimestampMillis;
|
||||
final long startTimeMillis = SystemClock.elapsedRealtime();
|
||||
|
||||
// Number of messages scanned local and remote
|
||||
int localPos;
|
||||
int remotePos;
|
||||
int localTotal;
|
||||
int remoteTotal;
|
||||
// Scan through the messages on both sides and prepare messages for local message table
|
||||
// changes (including adding and deleting)
|
||||
try {
|
||||
cursors.query(db);
|
||||
|
||||
localTotal = cursors.getLocalCount();
|
||||
remoteTotal = cursors.getRemoteCount();
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: Scanning cursors (local count = " + localTotal
|
||||
+ ", remote count = " + remoteTotal + ", message update limit = "
|
||||
+ maxMessagesToUpdate + ", message scan limit = " + maxMessagesToScan
|
||||
+ ")");
|
||||
}
|
||||
|
||||
lastTimestampMillis = cursors.scan(maxMessagesToScan, maxMessagesToUpdate,
|
||||
smsToAdd, mmsToAdd, messagesToDelete, cache);
|
||||
|
||||
localPos = cursors.getLocalPosition();
|
||||
remotePos = cursors.getRemotePosition();
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: Scanned cursors (local position = " + localPos
|
||||
+ " of " + localTotal + ", remote position = " + remotePos + " of "
|
||||
+ remoteTotal + ")");
|
||||
}
|
||||
|
||||
// Batch loading the parts of the MMS messages in this batch
|
||||
loadMmsParts(mmsToAdd);
|
||||
// Lookup senders for incoming mms messages
|
||||
setMmsSenders(mmsToAdd, cache);
|
||||
} catch (final SQLiteException e) {
|
||||
LogUtil.e(TAG, "SyncMessagesAction: Database exception", e);
|
||||
// Let's abort
|
||||
lastTimestampMillis = SYNC_FAILED;
|
||||
} catch (final Exception e) {
|
||||
// We want to catch anything unexpected since this is running in a separate thread
|
||||
// and any unexpected exception will just fail this thread silently.
|
||||
// Let's crash for dogfooders!
|
||||
LogUtil.wtf(TAG, "SyncMessagesAction: unexpected failure in scan", e);
|
||||
lastTimestampMillis = SYNC_FAILED;
|
||||
} finally {
|
||||
if (cursors != null) {
|
||||
cursors.close();
|
||||
}
|
||||
}
|
||||
|
||||
final long endTimeMillis = SystemClock.elapsedRealtime();
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: Scan complete (took "
|
||||
+ (endTimeMillis - startTimeMillis) + " ms). " + smsToAdd.size()
|
||||
+ " remote SMS to add, " + mmsToAdd.size() + " MMS to add, "
|
||||
+ messagesToDelete.size() + " local messages to delete. "
|
||||
+ "Oldest timestamp seen = " + lastTimestampMillis);
|
||||
}
|
||||
|
||||
return lastTimestampMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform local database updates and schedule follow on sync actions
|
||||
*/
|
||||
@Override
|
||||
protected Object processBackgroundResponse(final Bundle response) {
|
||||
final long lastTimestampMillis = response.getLong(BUNDLE_KEY_LAST_TIMESTAMP);
|
||||
final long lowerBoundTimeMillis = actionParameters.getLong(KEY_LOWER_BOUND);
|
||||
final long upperBoundTimeMillis = actionParameters.getLong(KEY_UPPER_BOUND);
|
||||
final int maxMessagesToUpdate = actionParameters.getInt(KEY_MAX_UPDATE);
|
||||
final long startTimestamp = actionParameters.getLong(KEY_START_TIMESTAMP);
|
||||
|
||||
// Check with the sync manager if any conflicting updates have been made to databases
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
final boolean orphan = !syncManager.isSyncing(upperBoundTimeMillis);
|
||||
|
||||
// lastTimestampMillis used to indicate failure
|
||||
if (orphan) {
|
||||
// This batch does not match current in progress timestamp.
|
||||
LogUtil.w(TAG, "SyncMessagesAction: Ignoring orphan sync batch for messages from "
|
||||
+ lowerBoundTimeMillis + " to " + upperBoundTimeMillis);
|
||||
} else {
|
||||
final boolean dirty = syncManager.isBatchDirty(lastTimestampMillis);
|
||||
if (lastTimestampMillis == SYNC_FAILED) {
|
||||
LogUtil.e(TAG, "SyncMessagesAction: Sync failed - terminating");
|
||||
|
||||
// Failed - update last sync times to throttle our failure rate
|
||||
final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
|
||||
// Save sync completion time so next sync will start from here
|
||||
prefs.putLong(BuglePrefsKeys.LAST_SYNC_TIME, startTimestamp);
|
||||
// Remember last full sync so that don't start background full sync right away
|
||||
prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, startTimestamp);
|
||||
|
||||
syncManager.complete();
|
||||
} else if (dirty) {
|
||||
LogUtil.w(TAG, "SyncMessagesAction: Redoing dirty sync batch of messages from "
|
||||
+ lowerBoundTimeMillis + " to " + upperBoundTimeMillis);
|
||||
|
||||
// Redo this batch
|
||||
final SyncMessagesAction nextBatch =
|
||||
new SyncMessagesAction(lowerBoundTimeMillis, upperBoundTimeMillis,
|
||||
maxMessagesToUpdate, startTimestamp);
|
||||
|
||||
syncManager.startSyncBatch(upperBoundTimeMillis);
|
||||
requestBackgroundWork(nextBatch);
|
||||
} else {
|
||||
// Succeeded
|
||||
final ArrayList<SmsMessage> smsToAdd =
|
||||
response.getParcelableArrayList(BUNDLE_KEY_SMS_MESSAGES, SmsMessage.class);
|
||||
final ArrayList<MmsMessage> mmsToAdd =
|
||||
response.getParcelableArrayList(BUNDLE_KEY_MMS_MESSAGES, MmsMessage.class);
|
||||
final ArrayList<LocalDatabaseMessage> messagesToDelete =
|
||||
response.getParcelableArrayList(BUNDLE_KEY_MESSAGES_TO_DELETE,
|
||||
LocalDatabaseMessage.class);
|
||||
|
||||
final int messagesUpdated = smsToAdd.size() + mmsToAdd.size()
|
||||
+ messagesToDelete.size();
|
||||
|
||||
// Perform local database changes in one transaction
|
||||
long txnTimeMillis = 0;
|
||||
if (messagesUpdated > 0) {
|
||||
final long startTimeMillis = SystemClock.elapsedRealtime();
|
||||
final SyncMessageBatch batch = new SyncMessageBatch(smsToAdd, mmsToAdd,
|
||||
messagesToDelete, syncManager.getThreadInfoCache());
|
||||
batch.updateLocalDatabase();
|
||||
final long endTimeMillis = SystemClock.elapsedRealtime();
|
||||
txnTimeMillis = endTimeMillis - startTimeMillis;
|
||||
|
||||
LogUtil.i(TAG, "SyncMessagesAction: Updated local database "
|
||||
+ "(took " + txnTimeMillis + " ms). Added "
|
||||
+ smsToAdd.size() + " SMS, added " + mmsToAdd.size() + " MMS, deleted "
|
||||
+ messagesToDelete.size() + " messages.");
|
||||
|
||||
// TODO: Investigate whether we can make this more fine-grained.
|
||||
MessagingContentProvider.notifyEverythingChanged();
|
||||
} else {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: No local database updates to make");
|
||||
}
|
||||
|
||||
if (!syncManager.getHasFirstSyncCompleted()) {
|
||||
// If we have never completed a sync before (fresh install) and there are
|
||||
// no messages, still inform the UI of a change so it can update syncing
|
||||
// messages shown to the user
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
MessagingContentProvider.notifyPartsChanged();
|
||||
}
|
||||
}
|
||||
// Determine if there are more messages that need to be scanned
|
||||
if (lastTimestampMillis >= 0 && lastTimestampMillis >= lowerBoundTimeMillis) {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "SyncMessagesAction: More messages to sync; scheduling next "
|
||||
+ "sync batch now.");
|
||||
}
|
||||
|
||||
// Include final millisecond of last sync in next sync
|
||||
final long newUpperBoundTimeMillis = lastTimestampMillis + 1;
|
||||
final int newMaxMessagesToUpdate = nextBatchSize(messagesUpdated,
|
||||
txnTimeMillis);
|
||||
|
||||
final SyncMessagesAction nextBatch =
|
||||
new SyncMessagesAction(lowerBoundTimeMillis, newUpperBoundTimeMillis,
|
||||
newMaxMessagesToUpdate, startTimestamp);
|
||||
|
||||
// Proceed with next batch
|
||||
syncManager.startSyncBatch(newUpperBoundTimeMillis);
|
||||
requestBackgroundWork(nextBatch);
|
||||
} else {
|
||||
final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
|
||||
// Save sync completion time so next sync will start from here
|
||||
prefs.putLong(BuglePrefsKeys.LAST_SYNC_TIME, startTimestamp);
|
||||
if (lowerBoundTimeMillis < 0) {
|
||||
// Remember last full sync so that don't start another full sync right away
|
||||
prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, startTimestamp);
|
||||
}
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
|
||||
// After any sync check if new messages have arrived
|
||||
final SyncCursorPair recents = new SyncCursorPair(startTimestamp, now);
|
||||
final SyncCursorPair olders = new SyncCursorPair(-1L, startTimestamp);
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
if (!recents.isSynchronized(db)) {
|
||||
LogUtil.i(TAG, "SyncMessagesAction: Changed messages after sync; "
|
||||
+ "scheduling an incremental sync now.");
|
||||
|
||||
// Just add a new batch for recent messages
|
||||
final SyncMessagesAction nextBatch =
|
||||
new SyncMessagesAction(startTimestamp, now, 0, startTimestamp);
|
||||
syncManager.startSyncBatch(now);
|
||||
requestBackgroundWork(nextBatch);
|
||||
// After partial sync verify sync state
|
||||
} else if (lowerBoundTimeMillis >= 0 && !olders.isSynchronized(db)) {
|
||||
// Add a batch going back to start of time
|
||||
LogUtil.w(TAG, "SyncMessagesAction: Changed messages before sync batch; "
|
||||
+ "scheduling a full sync now.");
|
||||
|
||||
final SyncMessagesAction nextBatch =
|
||||
new SyncMessagesAction(-1L, startTimestamp, 0, startTimestamp);
|
||||
|
||||
syncManager.startSyncBatch(startTimestamp);
|
||||
requestBackgroundWork(nextBatch);
|
||||
} else {
|
||||
LogUtil.i(TAG, "SyncMessagesAction: All messages now in sync");
|
||||
|
||||
// All done, in sync
|
||||
syncManager.complete();
|
||||
}
|
||||
}
|
||||
// Either sync should be complete or we should have a follow up request
|
||||
Assert.isTrue(hasBackgroundActions() || !syncManager.isSyncing());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide the next batch size based on the stats we collected with past batch
|
||||
* @param messagesUpdated number of messages updated in this batch
|
||||
* @param txnTimeMillis time the transaction took in ms
|
||||
* @return Target number of messages to sync for next batch
|
||||
*/
|
||||
private static int nextBatchSize(final int messagesUpdated, final long txnTimeMillis) {
|
||||
final long smsSyncSubsequentBatchTimeLimitMillis =
|
||||
BugleGservicesKeys.SMS_SYNC_BATCH_TIME_LIMIT_MILLIS_DEFAULT;
|
||||
|
||||
if (txnTimeMillis <= 0) {
|
||||
return 0;
|
||||
}
|
||||
// Number of messages we can sync within the batch time limit using
|
||||
// the average sync time calculated based on the stats we collected
|
||||
// in previous batch
|
||||
return (int) ((double) (messagesUpdated) / (double) txnTimeMillis
|
||||
* smsSyncSubsequentBatchTimeLimitMillis);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch loading MMS parts for the messages in current batch
|
||||
*/
|
||||
private void loadMmsParts(final LongSparseArray<MmsMessage> mmses) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final int totalIds = mmses.size();
|
||||
for (int start = 0; start < totalIds; start += MmsUtils.MAX_IDS_PER_QUERY) {
|
||||
final int end = Math.min(start + MmsUtils.MAX_IDS_PER_QUERY, totalIds); //excluding
|
||||
final int count = end - start;
|
||||
final String batchSelection = String.format(
|
||||
Locale.US,
|
||||
"%s != '%s' AND %s IN %s",
|
||||
Mms.Part.CONTENT_TYPE,
|
||||
ContentType.APP_SMIL,
|
||||
Mms.Part.MSG_ID,
|
||||
MmsUtils.getSqlInOperand(count));
|
||||
final String[] batchSelectionArgs = new String[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
batchSelectionArgs[i] = Long.toString(mmses.valueAt(start + i).getId());
|
||||
}
|
||||
final Cursor cursor = SqliteWrapper.query(
|
||||
context,
|
||||
context.getContentResolver(),
|
||||
MmsUtils.MMS_PART_CONTENT_URI,
|
||||
DatabaseMessages.MmsPart.PROJECTION,
|
||||
batchSelection,
|
||||
batchSelectionArgs,
|
||||
null/*sortOrder*/);
|
||||
if (cursor != null) {
|
||||
try {
|
||||
while (cursor.moveToNext()) {
|
||||
// Delay loading the media content for parsing for efficiency
|
||||
// TODO: load the media and fill in the dimensions when
|
||||
// we actually display it
|
||||
final DatabaseMessages.MmsPart part =
|
||||
DatabaseMessages.MmsPart.get(cursor, false/*loadMedia*/);
|
||||
final DatabaseMessages.MmsMessage mms = mmses.get(part.mMessageId);
|
||||
if (mms != null) {
|
||||
mms.addPart(part);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch loading MMS sender for the messages in current batch
|
||||
*/
|
||||
private void setMmsSenders(final LongSparseArray<MmsMessage> mmses,
|
||||
final ThreadInfoCache cache) {
|
||||
// Store all the MMS messages
|
||||
for (int i = 0; i < mmses.size(); i++) {
|
||||
final MmsMessage mms = mmses.valueAt(i);
|
||||
|
||||
final boolean isOutgoing = mms.mType != Mms.MESSAGE_BOX_INBOX;
|
||||
String senderId = null;
|
||||
if (!isOutgoing) {
|
||||
// We only need to find out sender phone number for received message
|
||||
senderId = getMmsSender(mms, cache);
|
||||
if (senderId == null) {
|
||||
LogUtil.w(TAG, "SyncMessagesAction: Could not find sender of incoming MMS "
|
||||
+ "message " + mms.getUri() + "; using 'unknown sender' instead");
|
||||
senderId = ParticipantData.getUnknownSenderDestination();
|
||||
}
|
||||
}
|
||||
mms.setSender(senderId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out the sender of an MMS message
|
||||
*/
|
||||
private String getMmsSender(final MmsMessage mms, final ThreadInfoCache cache) {
|
||||
final List<String> recipients = cache.getThreadRecipients(mms.mThreadId);
|
||||
Assert.notNull(recipients);
|
||||
Assert.isTrue(recipients.size() > 0);
|
||||
|
||||
if (recipients.size() == 1
|
||||
&& recipients.get(0).equals(ParticipantData.getUnknownSenderDestination())) {
|
||||
LogUtil.w(TAG, "SyncMessagesAction: MMS message " + mms.mUri + " has unknown sender "
|
||||
+ "(thread id = " + mms.mThreadId + ")");
|
||||
}
|
||||
|
||||
return MmsUtils.getMmsSender(recipients, mms.mUri);
|
||||
}
|
||||
|
||||
private SyncMessagesAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<SyncMessagesAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public SyncMessagesAction createFromParcel(final Parcel in) {
|
||||
return new SyncMessagesAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SyncMessagesAction[] newArray(final int size) {
|
||||
return new SyncMessagesAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
public class UpdateConversationArchiveStatusAction extends Action {
|
||||
|
||||
public static void archiveConversation(final String conversationId) {
|
||||
final UpdateConversationArchiveStatusAction action =
|
||||
new UpdateConversationArchiveStatusAction(conversationId, true /* isArchive */);
|
||||
action.start();
|
||||
}
|
||||
|
||||
public static void unarchiveConversation(final String conversationId) {
|
||||
final UpdateConversationArchiveStatusAction action =
|
||||
new UpdateConversationArchiveStatusAction(conversationId, false /* isArchive */);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
private static final String KEY_IS_ARCHIVE = "is_archive";
|
||||
|
||||
protected UpdateConversationArchiveStatusAction(
|
||||
final String conversationId, final boolean isArchive) {
|
||||
Assert.isTrue(!TextUtils.isEmpty(conversationId));
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
actionParameters.putBoolean(KEY_IS_ARCHIVE, isArchive);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final boolean isArchived = actionParameters.getBoolean(KEY_IS_ARCHIVE);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
BugleDatabaseOperations.updateConversationArchiveStatusInTransaction(
|
||||
db, conversationId, isArchived);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
|
||||
return null;
|
||||
}
|
||||
|
||||
protected UpdateConversationArchiveStatusAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<UpdateConversationArchiveStatusAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public UpdateConversationArchiveStatusAction createFromParcel(final Parcel in) {
|
||||
return new UpdateConversationArchiveStatusAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateConversationArchiveStatusAction[] newArray(final int size) {
|
||||
return new UpdateConversationArchiveStatusAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
public class UpdateDestinationBlockedAction extends Action {
|
||||
public interface UpdateDestinationBlockedActionListener {
|
||||
@Assert.RunsOnMainThread
|
||||
void onUpdateDestinationBlockedAction(final UpdateDestinationBlockedAction action,
|
||||
final boolean success,
|
||||
final boolean block,
|
||||
final String destination);
|
||||
}
|
||||
|
||||
public static class UpdateDestinationBlockedActionMonitor extends ActionMonitor
|
||||
implements ActionMonitor.ActionCompletedListener {
|
||||
private final UpdateDestinationBlockedActionListener mListener;
|
||||
|
||||
public UpdateDestinationBlockedActionMonitor(
|
||||
Object data, UpdateDestinationBlockedActionListener mListener) {
|
||||
super(STATE_CREATED, generateUniqueActionKey("UpdateDestinationBlockedAction"), data);
|
||||
setCompletedListener(this);
|
||||
this.mListener = mListener;
|
||||
}
|
||||
|
||||
private void onActionDone(final boolean succeeded,
|
||||
final ActionMonitor monitor,
|
||||
final Action action,
|
||||
final Object data,
|
||||
final Object result) {
|
||||
mListener.onUpdateDestinationBlockedAction(
|
||||
(UpdateDestinationBlockedAction) action,
|
||||
succeeded,
|
||||
action.actionParameters.getBoolean(KEY_BLOCKED),
|
||||
action.actionParameters.getString(KEY_DESTINATION));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionSucceeded(final ActionMonitor monitor,
|
||||
final Action action,
|
||||
final Object data,
|
||||
final Object result) {
|
||||
onActionDone(true, monitor, action, data, result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionFailed(final ActionMonitor monitor,
|
||||
final Action action,
|
||||
final Object data,
|
||||
final Object result) {
|
||||
onActionDone(false, monitor, action, data, result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static UpdateDestinationBlockedActionMonitor updateDestinationBlocked(
|
||||
final String destination, final boolean blocked, final String conversationId,
|
||||
final UpdateDestinationBlockedActionListener listener) {
|
||||
Assert.notNull(listener);
|
||||
final UpdateDestinationBlockedActionMonitor monitor =
|
||||
new UpdateDestinationBlockedActionMonitor(null, listener);
|
||||
final UpdateDestinationBlockedAction action =
|
||||
new UpdateDestinationBlockedAction(destination, blocked, conversationId,
|
||||
monitor.getActionKey());
|
||||
action.start(monitor);
|
||||
return monitor;
|
||||
}
|
||||
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
private static final String KEY_DESTINATION = "destination";
|
||||
private static final String KEY_BLOCKED = "blocked";
|
||||
|
||||
protected UpdateDestinationBlockedAction(
|
||||
final String destination, final boolean blocked, final String conversationId,
|
||||
final String actionKey) {
|
||||
super(actionKey);
|
||||
Assert.isTrue(!TextUtils.isEmpty(destination));
|
||||
actionParameters.putString(KEY_DESTINATION, destination);
|
||||
actionParameters.putBoolean(KEY_BLOCKED, blocked);
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String destination = actionParameters.getString(KEY_DESTINATION);
|
||||
final boolean isBlocked = actionParameters.getBoolean(KEY_BLOCKED);
|
||||
String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
BugleDatabaseOperations.updateDestination(db, destination, isBlocked);
|
||||
if (conversationId == null) {
|
||||
conversationId = BugleDatabaseOperations
|
||||
.getConversationFromOtherParticipantDestination(db, destination);
|
||||
}
|
||||
if (conversationId != null) {
|
||||
if (isBlocked) {
|
||||
UpdateConversationArchiveStatusAction.archiveConversation(conversationId);
|
||||
} else {
|
||||
UpdateConversationArchiveStatusAction.unarchiveConversation(conversationId);
|
||||
}
|
||||
MessagingContentProvider.notifyParticipantsChanged(conversationId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected UpdateDestinationBlockedAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<UpdateDestinationBlockedAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public UpdateDestinationBlockedAction createFromParcel(final Parcel in) {
|
||||
return new UpdateDestinationBlockedAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateDestinationBlockedAction[] newArray(final int size) {
|
||||
return new UpdateDestinationBlockedAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
|
||||
/**
|
||||
* Updates the message notification (generally, to include voice replies we've
|
||||
* made since the notification was first posted).
|
||||
*/
|
||||
public class UpdateMessageNotificationAction extends Action {
|
||||
|
||||
public static void updateMessageNotification() {
|
||||
new UpdateMessageNotificationAction().start();
|
||||
}
|
||||
|
||||
private UpdateMessageNotificationAction() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
BugleNotifications.update(true /* silent */, BugleNotifications.UPDATE_MESSAGES);
|
||||
return null;
|
||||
}
|
||||
|
||||
private UpdateMessageNotificationAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<UpdateMessageNotificationAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public UpdateMessageNotificationAction createFromParcel(final Parcel in) {
|
||||
return new UpdateMessageNotificationAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateMessageNotificationAction[] newArray(final int size) {
|
||||
return new UpdateMessageNotificationAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
/**
|
||||
* Action used to update size fields of a single part
|
||||
*/
|
||||
public class UpdateMessagePartSizeAction extends Action implements Parcelable {
|
||||
/**
|
||||
* Update size of part
|
||||
*/
|
||||
public static void updateSize(final String partId, final int width, final int height) {
|
||||
Assert.notNull(partId);
|
||||
Assert.inRange(width, 0, Integer.MAX_VALUE);
|
||||
Assert.inRange(height, 0, Integer.MAX_VALUE);
|
||||
|
||||
final UpdateMessagePartSizeAction action = new UpdateMessagePartSizeAction(
|
||||
partId, width, height);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private static final String KEY_PART_ID = "part_id";
|
||||
private static final String KEY_WIDTH = "width";
|
||||
private static final String KEY_HEIGHT = "height";
|
||||
|
||||
private UpdateMessagePartSizeAction(final String partId, final int width, final int height) {
|
||||
actionParameters.putString(KEY_PART_ID, partId);
|
||||
actionParameters.putInt(KEY_WIDTH, width);
|
||||
actionParameters.putInt(KEY_HEIGHT, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String partId = actionParameters.getString(KEY_PART_ID);
|
||||
final int width = actionParameters.getInt(KEY_WIDTH);
|
||||
final int height = actionParameters.getInt(KEY_HEIGHT);
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
final ContentValues values = new ContentValues();
|
||||
|
||||
values.put(PartColumns.WIDTH, width);
|
||||
values.put(PartColumns.HEIGHT, height);
|
||||
|
||||
// Part may have been deleted so allow update to fail without asserting
|
||||
BugleDatabaseOperations.updateRowIfExists(db, DatabaseHelper.PARTS_TABLE,
|
||||
PartColumns._ID, partId, values);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private UpdateMessagePartSizeAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<UpdateMessagePartSizeAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public UpdateMessagePartSizeAction createFromParcel(final Parcel in) {
|
||||
return new UpdateMessagePartSizeAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateMessagePartSizeAction[] newArray(final int size) {
|
||||
return new UpdateMessagePartSizeAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
public class WriteDraftMessageAction extends Action implements Parcelable {
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
/**
|
||||
* Set draft message (no listener)
|
||||
*/
|
||||
public static void writeDraftMessage(final String conversationId, final MessageData message) {
|
||||
final WriteDraftMessageAction action = new WriteDraftMessageAction(conversationId, message);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private static final String KEY_CONVERSATION_ID = "conversationId";
|
||||
private static final String KEY_MESSAGE = "message";
|
||||
|
||||
private WriteDraftMessageAction(final String conversationId, final MessageData message) {
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
actionParameters.putParcelable(KEY_MESSAGE, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final MessageData message = actionParameters.getParcelable(KEY_MESSAGE, MessageData.class);
|
||||
if (message.getSelfId() == null || message.getParticipantId() == null) {
|
||||
// This could happen when this occurs before the draft message is loaded
|
||||
// In this case, we just use the conversation's current self id as draft's
|
||||
// self id and/or participant id
|
||||
final ConversationListItemData conversation =
|
||||
ConversationListItemData.getExistingConversation(db, conversationId);
|
||||
if (conversation != null) {
|
||||
final String senderAndSelf = conversation.getSelfId();
|
||||
if (message.getSelfId() == null) {
|
||||
message.bindSelfId(senderAndSelf);
|
||||
}
|
||||
if (message.getParticipantId() == null) {
|
||||
message.bindParticipantId(senderAndSelf);
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_DATAMODEL_TAG, "Conversation " + conversationId +
|
||||
"already deleted before saving draft message " +
|
||||
message.getMessageId() + ". Aborting WriteDraftMessageAction.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// Drafts are only kept in the local DB...
|
||||
final String messageId = BugleDatabaseOperations.updateDraftMessageData(
|
||||
db, conversationId, message, BugleDatabaseOperations.UPDATE_MODE_ADD_DRAFT);
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
|
||||
return messageId;
|
||||
}
|
||||
|
||||
private WriteDraftMessageAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<WriteDraftMessageAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public WriteDraftMessageAction createFromParcel(final Parcel in) {
|
||||
return new WriteDraftMessageAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriteDraftMessageAction[] newArray(final int size) {
|
||||
return new WriteDraftMessageAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.binding;
|
||||
|
||||
/**
|
||||
* Base class for data objects that will be bound to a piece of the UI
|
||||
*/
|
||||
public abstract class BindableData {
|
||||
/**
|
||||
* Called by Binding during unbind to allow data to proactively unregister callbacks
|
||||
* Data instance should release all listeners that may call back to the host UI
|
||||
*/
|
||||
protected abstract void unregisterListeners();
|
||||
|
||||
/**
|
||||
* Key used to identify the piece of UI that the data is currently bound to
|
||||
*/
|
||||
private String mBindingId;
|
||||
|
||||
/**
|
||||
* Bind this data to the ui host - checks data is currently unbound
|
||||
*/
|
||||
public void bind(final String bindingId) {
|
||||
if (isBound() || bindingId == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
mBindingId = bindingId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbind this data from the ui host - checks that the data is currently bound to specified id
|
||||
*/
|
||||
public void unbind(final String bindingId) {
|
||||
if (!isBound(bindingId)) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
unregisterListeners();
|
||||
mBindingId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the data is bound to anything
|
||||
*
|
||||
* TODO: This should be package private because it's supposed to only be used by Binding,
|
||||
* however, several classes call this directly. We want the classes to track what they are
|
||||
* bound to.
|
||||
*/
|
||||
protected boolean isBound() {
|
||||
return (mBindingId != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if data is still bound with specified bindingId before calling over to ui
|
||||
*/
|
||||
public boolean isBound(final String bindingId) {
|
||||
return (bindingId.equals(mBindingId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.binding;
|
||||
|
||||
/**
|
||||
* A BindableData that's only used to be bound once. If the client needs to rebind, it needs
|
||||
* to create a new instance of the BindableOnceData.
|
||||
*/
|
||||
public abstract class BindableOnceData extends BindableData {
|
||||
private boolean boundOnce = false;
|
||||
|
||||
@Override
|
||||
public void bind(final String bindingId) {
|
||||
// Ensures that we can't re-bind again after the first binding.
|
||||
if (boundOnce) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
super.bind(bindingId);
|
||||
boundOnce = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the instance is bound to anything.
|
||||
*/
|
||||
@Override
|
||||
public boolean isBound() {
|
||||
return super.isBound();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.binding;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class Binding<T extends BindableData> extends BindingBase<T> {
|
||||
private static final AtomicLong sBindingIdx = new AtomicLong(System.currentTimeMillis() * 1000);
|
||||
|
||||
private String mBindingId;
|
||||
private T mData;
|
||||
private final Object mOwner;
|
||||
private boolean mWasBound;
|
||||
|
||||
/**
|
||||
* Initialize a binding instance - the owner is typically the containing class
|
||||
*/
|
||||
Binding(final Object owner) {
|
||||
mOwner = owner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getData() {
|
||||
ensureBound();
|
||||
return mData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBound() {
|
||||
return (mData != null && mData.isBound(mBindingId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBound(final T data) {
|
||||
return (isBound() && data == mData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ensureBound() {
|
||||
if (!isBound()) {
|
||||
throw new IllegalStateException("not bound; wasBound = " + mWasBound);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ensureBound(final T data) {
|
||||
if (!isBound()) {
|
||||
throw new IllegalStateException("not bound; wasBound = " + mWasBound);
|
||||
} else if (data != mData) {
|
||||
throw new IllegalStateException("not bound to correct data " + data + " vs " + mData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBindingId() {
|
||||
return mBindingId;
|
||||
}
|
||||
|
||||
public void bind(final T data) {
|
||||
// Check both this binding and the data not already bound
|
||||
if (mData != null || data.isBound()) {
|
||||
throw new IllegalStateException("already bound when binding to " + data);
|
||||
}
|
||||
// Generate a unique identifier for this bind call
|
||||
mBindingId = Long.toHexString(sBindingIdx.getAndIncrement());
|
||||
data.bind(mBindingId);
|
||||
mData = data;
|
||||
mWasBound = true;
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
// Check this binding is bound and that data is bound to this binding
|
||||
if (mData == null || !mData.isBound(mBindingId)) {
|
||||
throw new IllegalStateException("not bound when unbind");
|
||||
}
|
||||
mData.unbind(mBindingId);
|
||||
mData = null;
|
||||
mBindingId = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.binding;
|
||||
|
||||
/*
|
||||
* The binding class keeps track of a binding between a ui component and an item of BindableData
|
||||
* It allows each side to ensure that when it communicates with the other they are still bound
|
||||
* together.
|
||||
* NOTE: Ensure that the UI component uses the same binding instance for it's whole lifetime
|
||||
* (DO NOT CREATE A NEW BINDING EACH TIME A NEW PIECE OF DATA IS BOUND)...
|
||||
*
|
||||
* The ui component owns the binding instance.
|
||||
* It can use it [isBound(data)] to see if the binding still binds to the right piece of data
|
||||
*
|
||||
* Upon binding the data is informed of a unique binding key generated in this class and can use
|
||||
* that to ensure that it is still issuing callbacks to the right piece of ui.
|
||||
*/
|
||||
public abstract class BindingBase<T extends BindableData> {
|
||||
/**
|
||||
* Creates a new exclusively owned binding for the owner object.
|
||||
*/
|
||||
public static <T extends BindableData> Binding<T> createBinding(final Object owner) {
|
||||
return new Binding<>(owner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new read-only binding referencing the source binding object.
|
||||
* TODO: We may want to refcount the Binding references, so that when the binding owner
|
||||
* calls unbind() when there's still outstanding references we can catch it.
|
||||
*/
|
||||
public static <T extends BindableData> ImmutableBindingRef<T> createBindingReference(
|
||||
final BindingBase<T> srcBinding) {
|
||||
return new ImmutableBindingRef<>(srcBinding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a detachable binding for the owner object. Use this if your owner object is a UI
|
||||
* component that may undergo a "detached from window" -> "re-attached to window" transition.
|
||||
*/
|
||||
public static <T extends BindableData> DetachableBinding<T> createDetachableBinding(
|
||||
final Object owner) {
|
||||
return new DetachableBinding<>(owner);
|
||||
}
|
||||
|
||||
public abstract T getData();
|
||||
|
||||
/**
|
||||
* Check if binding connects to the specified data instance
|
||||
*/
|
||||
public abstract boolean isBound();
|
||||
|
||||
/**
|
||||
* Check if binding connects to the specified data instance
|
||||
*/
|
||||
public abstract boolean isBound(final T data);
|
||||
|
||||
/**
|
||||
* Throw if binding connects to the specified data instance
|
||||
*/
|
||||
public abstract void ensureBound();
|
||||
|
||||
/**
|
||||
* Throw if binding connects to the specified data instance
|
||||
*/
|
||||
public abstract void ensureBound(final T data);
|
||||
|
||||
/**
|
||||
* Return the binding id for this binding (will be null if not bound)
|
||||
*/
|
||||
public abstract String getBindingId();
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.binding;
|
||||
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
/**
|
||||
* An extension on {@link Binding} that allows for temporary data detachment from the UI component.
|
||||
* This is used when, instead of destruction or data rebinding, the owning UI undergoes a
|
||||
* "detached from window" -> "re-attached to window" transition, in which case we want to
|
||||
* temporarily unbind the data and remember it so that it can be rebound when the UI is re-attached
|
||||
* to window later.
|
||||
*/
|
||||
public class DetachableBinding<T extends BindableData> extends Binding<T> {
|
||||
private T mDetachedData;
|
||||
|
||||
DetachableBinding(Object owner) {
|
||||
super(owner);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(T data) {
|
||||
super.bind(data);
|
||||
// Rebinding before re-attaching. Pre-emptively throw away the detached data because
|
||||
// it's now stale.
|
||||
mDetachedData = null;
|
||||
}
|
||||
|
||||
public void detach() {
|
||||
Assert.isNull(mDetachedData);
|
||||
Assert.isTrue(isBound());
|
||||
mDetachedData = getData();
|
||||
unbind();
|
||||
}
|
||||
|
||||
public void reAttachIfPossible() {
|
||||
if (mDetachedData != null) {
|
||||
Assert.isFalse(isBound());
|
||||
bind(mDetachedData);
|
||||
mDetachedData = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.binding;
|
||||
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
/**
|
||||
* A immutable wrapper around a Binding object. Callers can only access readonly methods like
|
||||
* getData(), isBound() and ensureBound() but not bind() and unbind(). This is used for MVC pattern
|
||||
* where both the View and the Controller needs access to a centrally bound Model object. The View
|
||||
* is the one that owns the bind/unbind logic of the Binding, whereas controller only serves as a
|
||||
* consumer.
|
||||
*/
|
||||
public class ImmutableBindingRef<T extends BindableData> extends BindingBase<T> {
|
||||
/**
|
||||
* The referenced, read-only binding object.
|
||||
*/
|
||||
private final BindingBase<T> mBinding;
|
||||
|
||||
/**
|
||||
* Hidden ctor.
|
||||
*/
|
||||
ImmutableBindingRef(final BindingBase<T> binding) {
|
||||
mBinding = resolveBinding(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getData() {
|
||||
return mBinding.getData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBound() {
|
||||
return mBinding.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBound(final T data) {
|
||||
return mBinding.isBound(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ensureBound() {
|
||||
mBinding.ensureBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ensureBound(final T data) {
|
||||
mBinding.ensureBound(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBindingId() {
|
||||
return mBinding.getBindingId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the source binding to the real BindingImpl it's referencing. This avoids the
|
||||
* redundancy of multiple wrapper calls when creating a binding reference from an existing
|
||||
* binding reference.
|
||||
*/
|
||||
private BindingBase<T> resolveBinding(final BindingBase<T> binding) {
|
||||
BindingBase<T> resolvedBinding = binding;
|
||||
while (resolvedBinding instanceof ImmutableBindingRef<?>) {
|
||||
resolvedBinding = ((ImmutableBindingRef<T>) resolvedBinding).mBinding;
|
||||
}
|
||||
Assert.isTrue(resolvedBinding instanceof Binding<?>);
|
||||
return resolvedBinding;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import com.android.messaging.datamodel.BoundCursorLoader;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.binding.BindableData;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
/**
|
||||
* Services data needs for BlockedParticipantsFragment
|
||||
*/
|
||||
public class BlockedParticipantsData extends BindableData implements
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
public interface BlockedParticipantsDataListener {
|
||||
void onBlockedParticipantsCursorUpdated(final Cursor cursor);
|
||||
}
|
||||
private static final String BINDING_ID = "bindingId";
|
||||
private static final int BLOCKED_PARTICIPANTS_LOADER = 1;
|
||||
private final Context mContext;
|
||||
private LoaderManager mLoaderManager;
|
||||
private BlockedParticipantsDataListener mListener;
|
||||
|
||||
public BlockedParticipantsData(final Context context,
|
||||
final BlockedParticipantsDataListener listener) {
|
||||
mContext = context;
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
|
||||
Assert.isTrue(id == BLOCKED_PARTICIPANTS_LOADER);
|
||||
final String bindingId = args.getString(BINDING_ID);
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(bindingId)) {
|
||||
final Uri uri = MessagingContentProvider.PARTICIPANTS_URI;
|
||||
return new BoundCursorLoader(bindingId, mContext, uri,
|
||||
ParticipantData.ParticipantsQuery.PROJECTION,
|
||||
ParticipantColumns.BLOCKED + "=1", null, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(final Loader<Cursor> loader, final Cursor cursor) {
|
||||
Assert.isTrue(loader.getId() == BLOCKED_PARTICIPANTS_LOADER);
|
||||
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
|
||||
Assert.isTrue(isBound(cursorLoader.getBindingId()));
|
||||
mListener.onBlockedParticipantsCursorUpdated(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(final Loader<Cursor> loader) {
|
||||
Assert.isTrue(loader.getId() == BLOCKED_PARTICIPANTS_LOADER);
|
||||
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
|
||||
Assert.isTrue(isBound(cursorLoader.getBindingId()));
|
||||
mListener.onBlockedParticipantsCursorUpdated(null);
|
||||
}
|
||||
|
||||
public void init(final LoaderManager loaderManager,
|
||||
final BindingBase<BlockedParticipantsData> binding) {
|
||||
final Bundle args = new Bundle();
|
||||
args.putString(BINDING_ID, binding.getBindingId());
|
||||
mLoaderManager = loaderManager;
|
||||
mLoaderManager.initLoader(BLOCKED_PARTICIPANTS_LOADER, args, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unregisterListeners() {
|
||||
mListener = null;
|
||||
if (mLoaderManager != null) {
|
||||
mLoaderManager.destroyLoader(BLOCKED_PARTICIPANTS_LOADER);
|
||||
mLoaderManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
public ParticipantListItemData createParticipantListItemData(Cursor cursor) {
|
||||
return new ParticipantListItemData(ParticipantData.getFromCursor(cursor));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract.DisplayNameSources;
|
||||
|
||||
import com.android.ex.chips.RecipientEntry;
|
||||
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.ContactRecipientEntryUtils;
|
||||
import com.android.messaging.util.ContactUtil;
|
||||
|
||||
/**
|
||||
* Data model object used to power ContactListItemViews, which may be displayed either in
|
||||
* our contact list, or in the chips UI search drop down presented by ContactDropdownLayouter.
|
||||
*/
|
||||
public class ContactListItemData {
|
||||
// Keeps the contact data in the form of RecipientEntry that RecipientEditTextView can
|
||||
// directly use.
|
||||
private RecipientEntry mRecipientEntry;
|
||||
|
||||
private CharSequence mStyledName;
|
||||
private CharSequence mStyledDestination;
|
||||
|
||||
// If this contact is the first in the list for its first letter, then this will be the
|
||||
// first letter, otherwise this is null.
|
||||
private String mAlphabetHeader;
|
||||
|
||||
// Is the contact the only item in the list (happens when the user clicks on an
|
||||
// existing chip for which we show full contact detail for the selected contact).
|
||||
private boolean mSingleRecipient;
|
||||
|
||||
// Is the contact in managed profile.
|
||||
private boolean mIsWorkContact;
|
||||
|
||||
/**
|
||||
* Bind to a contact cursor in the contact list.
|
||||
*/
|
||||
public void bind(final Cursor cursor, final String alphabetHeader) {
|
||||
final long dataId = cursor.getLong(ContactUtil.INDEX_DATA_ID);
|
||||
final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID);
|
||||
final String lookupKey = cursor.getString(ContactUtil.INDEX_LOOKUP_KEY);
|
||||
final String displayName = cursor.getString(ContactUtil.INDEX_DISPLAY_NAME);
|
||||
final String photoThumbnailUri = cursor.getString(ContactUtil.INDEX_PHOTO_URI);
|
||||
final String destination = cursor.getString(ContactUtil.INDEX_PHONE_EMAIL);
|
||||
final int destinationType = cursor.getInt(ContactUtil.INDEX_PHONE_EMAIL_TYPE);
|
||||
final String destinationLabel = cursor.getString(ContactUtil.INDEX_PHONE_EMAIL_LABEL);
|
||||
mStyledName = null;
|
||||
mStyledDestination = null;
|
||||
mAlphabetHeader = alphabetHeader;
|
||||
mSingleRecipient = false;
|
||||
|
||||
// Check whether this contact is first level (i.e. whether it's the first entry of this
|
||||
// contact in the contact list).
|
||||
boolean isFirstLevel = true;
|
||||
if (!cursor.isFirst() && cursor.moveToPrevious()) {
|
||||
final long contactIdPrevious = cursor.getLong(ContactUtil.INDEX_CONTACT_ID);
|
||||
if (contactId == contactIdPrevious) {
|
||||
isFirstLevel = false;
|
||||
}
|
||||
cursor.moveToNext();
|
||||
}
|
||||
|
||||
mRecipientEntry = ContactUtil.createRecipientEntry(displayName,
|
||||
DisplayNameSources.STRUCTURED_NAME, destination, destinationType, destinationLabel,
|
||||
contactId, lookupKey, dataId, photoThumbnailUri, isFirstLevel);
|
||||
|
||||
mIsWorkContact = ContactUtil.isEnterpriseContactId(contactId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to a RecipientEntry produced by the chips text view in the search drop down, plus
|
||||
* optional styled name & destination for showing bold search match.
|
||||
*/
|
||||
public void bind(final RecipientEntry entry, final CharSequence styledName,
|
||||
final CharSequence styledDestination, final boolean singleRecipient,
|
||||
final boolean isWorkContact) {
|
||||
Assert.isTrue(entry.isValid());
|
||||
mRecipientEntry = entry;
|
||||
mStyledName = styledName;
|
||||
mStyledDestination = styledDestination;
|
||||
mAlphabetHeader = null;
|
||||
mSingleRecipient = singleRecipient;
|
||||
mIsWorkContact = isWorkContact;
|
||||
}
|
||||
|
||||
public CharSequence getDisplayName() {
|
||||
final CharSequence displayName = mStyledName != null ? mStyledName :
|
||||
ContactRecipientEntryUtils.getDisplayNameForContactList(mRecipientEntry);
|
||||
return displayName == null ? "" : displayName;
|
||||
}
|
||||
|
||||
public Uri getPhotoThumbnailUri() {
|
||||
return mRecipientEntry.getPhotoThumbnailUri() == null ? null :
|
||||
mRecipientEntry.getPhotoThumbnailUri();
|
||||
}
|
||||
|
||||
public CharSequence getDestination() {
|
||||
final CharSequence destination = mStyledDestination != null ?
|
||||
mStyledDestination : ContactRecipientEntryUtils.formatDestination(mRecipientEntry);
|
||||
return destination == null ? "" : destination;
|
||||
}
|
||||
|
||||
public int getDestinationType() {
|
||||
return mRecipientEntry.getDestinationType();
|
||||
}
|
||||
|
||||
public String getDestinationLabel() {
|
||||
return mRecipientEntry.getDestinationLabel();
|
||||
}
|
||||
|
||||
public long getContactId() {
|
||||
return mRecipientEntry.getContactId();
|
||||
}
|
||||
|
||||
public String getLookupKey() {
|
||||
return mRecipientEntry.getLookupKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if this item is "first-level," i.e. whether it's the first entry of the contact
|
||||
* that it represents in the list. For example, if John Smith has 3 different phone numbers,
|
||||
* then the first number is considered first-level, while the other two are considered
|
||||
* second-level.
|
||||
*/
|
||||
public boolean getIsFirstLevel() {
|
||||
// Treat the item as first level if it's a top-level recipient entry, or if it's the only
|
||||
// item in the list.
|
||||
return mRecipientEntry.isFirstLevel() || mSingleRecipient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if this item is simple, i.e. it has only avatar and a display name with phone number
|
||||
* embedded so we can hide everything else.
|
||||
*/
|
||||
public boolean getIsSimpleContactItem() {
|
||||
return ContactRecipientEntryUtils.isAvatarAndNumberOnlyContact(mRecipientEntry) ||
|
||||
ContactRecipientEntryUtils.isSendToDestinationContact(mRecipientEntry);
|
||||
}
|
||||
|
||||
public String getAlphabetHeader() {
|
||||
return mAlphabetHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a RecipientEntry instance readily usable by the RecipientEditTextView.
|
||||
*/
|
||||
public RecipientEntry getRecipientEntry() {
|
||||
return mRecipientEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the contact is in managed profile.
|
||||
*/
|
||||
public boolean getIsWorkContact() {
|
||||
return mIsWorkContact;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import com.android.messaging.datamodel.BoundCursorLoader;
|
||||
import com.android.messaging.datamodel.FrequentContactsCursorBuilder;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.binding.BindableData;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.sms.MmsConfig;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.ContactUtil;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Class to access phone contacts.
|
||||
* The caller is responsible for ensuring that the app has READ_CONTACTS permission (see
|
||||
* {@link ContactUtil#hasReadContactsPermission()}) before instantiating this class.
|
||||
*/
|
||||
public class ContactPickerData extends BindableData implements
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
public interface ContactPickerDataListener {
|
||||
void onAllContactsCursorUpdated(Cursor data);
|
||||
void onFrequentContactsCursorUpdated(Cursor data);
|
||||
void onContactCustomColorLoaded(ContactPickerData data);
|
||||
}
|
||||
|
||||
private static final String BINDING_ID = "bindingId";
|
||||
private final Context mContext;
|
||||
private LoaderManager mLoaderManager;
|
||||
private ContactPickerDataListener mListener;
|
||||
private final FrequentContactsCursorBuilder mFrequentContactsCursorBuilder;
|
||||
|
||||
public ContactPickerData(final Context context, final ContactPickerDataListener listener) {
|
||||
mListener = listener;
|
||||
mContext = context;
|
||||
mFrequentContactsCursorBuilder = new FrequentContactsCursorBuilder();
|
||||
}
|
||||
|
||||
private static final int ALL_CONTACTS_LOADER = 1;
|
||||
private static final int FREQUENT_CONTACTS_LOADER = 2;
|
||||
private static final int PARTICIPANT_LOADER = 3;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
|
||||
final String bindingId = args.getString(BINDING_ID);
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(bindingId)) {
|
||||
switch (id) {
|
||||
case ALL_CONTACTS_LOADER:
|
||||
return ContactUtil.getPhones(mContext)
|
||||
.createBoundCursorLoader(bindingId);
|
||||
case FREQUENT_CONTACTS_LOADER:
|
||||
return ContactUtil.getFrequentContacts(mContext)
|
||||
.createBoundCursorLoader(bindingId);
|
||||
case PARTICIPANT_LOADER:
|
||||
return new BoundCursorLoader(bindingId, mContext,
|
||||
MessagingContentProvider.PARTICIPANTS_URI,
|
||||
ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
|
||||
default:
|
||||
Assert.fail("Unknown loader id for contact picker!");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Loader created after unbinding the contacts list");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull final Loader<Cursor> loader, final Cursor data) {
|
||||
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
|
||||
if (isBound(cursorLoader.getBindingId())) {
|
||||
switch (loader.getId()) {
|
||||
case ALL_CONTACTS_LOADER:
|
||||
mFrequentContactsCursorBuilder.setAllContacts(data);
|
||||
break;
|
||||
case FREQUENT_CONTACTS_LOADER:
|
||||
mFrequentContactsCursorBuilder.setFrequents(data);
|
||||
break;
|
||||
case PARTICIPANT_LOADER:
|
||||
mListener.onContactCustomColorLoaded(this);
|
||||
break;
|
||||
default:
|
||||
Assert.fail("Unknown loader id for contact picker!");
|
||||
break;
|
||||
}
|
||||
|
||||
if (loader.getId() != PARTICIPANT_LOADER) {
|
||||
// The frequent contacts cursor to be used in the UI depends on results from both
|
||||
// all contacts and frequent contacts loader, and we don't know which will finish
|
||||
// first. Therefore, try to build the cursor and notify the listener if it's
|
||||
// successfully built.
|
||||
final Cursor frequentContactsCursor = mFrequentContactsCursorBuilder.build(false);
|
||||
if (frequentContactsCursor != null) {
|
||||
mListener.onFrequentContactsCursorUpdated(frequentContactsCursor);
|
||||
|
||||
final Cursor allContactsCursor = mFrequentContactsCursorBuilder.build(true);
|
||||
mListener.onAllContactsCursorUpdated(allContactsCursor);
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Loader finished after unbinding the contacts list");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onLoaderReset(final Loader<Cursor> loader) {
|
||||
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
|
||||
if (isBound(cursorLoader.getBindingId())) {
|
||||
switch (loader.getId()) {
|
||||
case ALL_CONTACTS_LOADER:
|
||||
mListener.onAllContactsCursorUpdated(null);
|
||||
mFrequentContactsCursorBuilder.setAllContacts(null);
|
||||
break;
|
||||
case FREQUENT_CONTACTS_LOADER:
|
||||
mListener.onFrequentContactsCursorUpdated(null);
|
||||
mFrequentContactsCursorBuilder.setFrequents(null);
|
||||
break;
|
||||
case PARTICIPANT_LOADER:
|
||||
mListener.onContactCustomColorLoaded(this);
|
||||
break;
|
||||
default:
|
||||
Assert.fail("Unknown loader id for contact picker!");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Loader reset after unbinding the contacts list");
|
||||
}
|
||||
}
|
||||
|
||||
public void init(final LoaderManager loaderManager,
|
||||
final BindingBase<ContactPickerData> binding) {
|
||||
final Bundle args = new Bundle();
|
||||
args.putString(BINDING_ID, binding.getBindingId());
|
||||
mLoaderManager = loaderManager;
|
||||
mLoaderManager.initLoader(ALL_CONTACTS_LOADER, args, this);
|
||||
mLoaderManager.initLoader(FREQUENT_CONTACTS_LOADER, args, this);
|
||||
mLoaderManager.initLoader(PARTICIPANT_LOADER, args, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unregisterListeners() {
|
||||
mListener = null;
|
||||
|
||||
|
||||
// This could be null if we bind but the caller doesn't init the BindableData
|
||||
if (mLoaderManager != null) {
|
||||
mLoaderManager.destroyLoader(ALL_CONTACTS_LOADER);
|
||||
mLoaderManager.destroyLoader(FREQUENT_CONTACTS_LOADER);
|
||||
mLoaderManager.destroyLoader(PARTICIPANT_LOADER);
|
||||
mLoaderManager = null;
|
||||
}
|
||||
mFrequentContactsCursorBuilder.resetBuilder();
|
||||
}
|
||||
|
||||
public static boolean isTooManyParticipants(final int participantCount) {
|
||||
// When creating a conversation, the conversation will be created using the system's
|
||||
// default SIM, so use the default MmsConfig's recipient limit.
|
||||
return (participantCount > MmsConfig.get(ParticipantData.DEFAULT_SELF_SUB_ID)
|
||||
.getRecipientLimit());
|
||||
}
|
||||
|
||||
public static boolean getCanAddMoreParticipants(final int participantCount) {
|
||||
// When creating a conversation, the conversation will be created using the system's
|
||||
// default SIM, so use the default MmsConfig's recipient limit.
|
||||
return (participantCount < MmsConfig.get(ParticipantData.DEFAULT_SELF_SUB_ID)
|
||||
.getRecipientLimit());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,803 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.CursorWrapper;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.BoundCursorLoader;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.action.DeleteConversationAction;
|
||||
import com.android.messaging.datamodel.action.DeleteMessageAction;
|
||||
import com.android.messaging.datamodel.action.InsertNewMessageAction;
|
||||
import com.android.messaging.datamodel.action.RedownloadMmsAction;
|
||||
import com.android.messaging.datamodel.action.ResendMessageAction;
|
||||
import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
|
||||
import com.android.messaging.datamodel.binding.BindableData;
|
||||
import com.android.messaging.datamodel.binding.Binding;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
|
||||
import com.android.messaging.sms.MmsSmsUtils;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.RunsOnMainThread;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
import com.android.messaging.widget.WidgetConversationProvider;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ConversationData extends BindableData {
|
||||
|
||||
private static final String TAG = "bugle_datamodel";
|
||||
private static final String BINDING_ID = "bindingId";
|
||||
private static final long LAST_MESSAGE_TIMESTAMP_NaN = -1;
|
||||
private static final int MESSAGE_COUNT_NaN = -1;
|
||||
|
||||
public interface ConversationDataListener {
|
||||
void onConversationMessagesCursorUpdated(ConversationData data, Cursor cursor,
|
||||
@Nullable ConversationMessageData newestMessage, boolean isSync);
|
||||
void onConversationMetadataUpdated(ConversationData data);
|
||||
void closeConversation(String conversationId);
|
||||
void onConversationParticipantDataLoaded(ConversationData data);
|
||||
void onSubscriptionListDataLoaded(ConversationData data);
|
||||
}
|
||||
|
||||
private static class ReversedCursor extends CursorWrapper {
|
||||
final int mCount;
|
||||
|
||||
public ReversedCursor(final Cursor cursor) {
|
||||
super(cursor);
|
||||
mCount = cursor.getCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToPosition(final int position) {
|
||||
return super.moveToPosition(mCount - position - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPosition() {
|
||||
return mCount - super.getPosition() - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAfterLast() {
|
||||
return super.isBeforeFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBeforeFirst() {
|
||||
return super.isAfterLast();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFirst() {
|
||||
return super.isLast();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLast() {
|
||||
return super.isFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean move(final int offset) {
|
||||
return super.move(-offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToFirst() {
|
||||
return super.moveToLast();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToLast() {
|
||||
return super.moveToFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToNext() {
|
||||
return super.moveToPrevious();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToPrevious() {
|
||||
return super.moveToNext();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A trampoline class so that we can inherit from LoaderManager.LoaderCallbacks multiple times.
|
||||
*/
|
||||
private class MetadataLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
|
||||
Assert.equals(CONVERSATION_META_DATA_LOADER, id);
|
||||
Loader<Cursor> loader = null;
|
||||
|
||||
final String bindingId = args.getString(BINDING_ID);
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(bindingId)) {
|
||||
final Uri uri =
|
||||
MessagingContentProvider.buildConversationMetadataUri(mConversationId);
|
||||
loader = new BoundCursorLoader(bindingId, mContext, uri,
|
||||
ConversationListItemData.PROJECTION, null, null, null);
|
||||
} else {
|
||||
LogUtil.w(TAG, "Creating messages loader after unbinding mConversationId = " +
|
||||
mConversationId);
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull final Loader<Cursor> generic, final Cursor data) {
|
||||
final BoundCursorLoader loader = (BoundCursorLoader) generic;
|
||||
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(loader.getBindingId())) {
|
||||
if (data.moveToNext()) {
|
||||
Assert.isTrue(data.getCount() == 1);
|
||||
mConversationMetadata.bind(data);
|
||||
mListeners.onConversationMetadataUpdated(ConversationData.this);
|
||||
} else {
|
||||
// Close the conversation, no meta data means conversation was deleted
|
||||
LogUtil.w(TAG, "Meta data loader returned nothing for mConversationId = " +
|
||||
mConversationId);
|
||||
mListeners.closeConversation(mConversationId);
|
||||
// Notify the widget the conversation is deleted so it can go into its
|
||||
// configure state.
|
||||
WidgetConversationProvider.notifyConversationDeleted(
|
||||
Factory.get().getApplicationContext(),
|
||||
mConversationId);
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(TAG, "Meta data loader finished after unbinding mConversationId = " +
|
||||
mConversationId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull final Loader<Cursor> generic) {
|
||||
final BoundCursorLoader loader = (BoundCursorLoader) generic;
|
||||
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(loader.getBindingId())) {
|
||||
// Clear the conversation meta data
|
||||
mConversationMetadata = new ConversationListItemData();
|
||||
mListeners.onConversationMetadataUpdated(ConversationData.this);
|
||||
} else {
|
||||
LogUtil.w(TAG, "Meta data loader reset after unbinding mConversationId = " +
|
||||
mConversationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A trampoline class so that we can inherit from LoaderManager.LoaderCallbacks multiple times.
|
||||
*/
|
||||
private class MessagesLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
|
||||
Assert.equals(CONVERSATION_MESSAGES_LOADER, id);
|
||||
Loader<Cursor> loader = null;
|
||||
|
||||
final String bindingId = args.getString(BINDING_ID);
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(bindingId)) {
|
||||
final Uri uri =
|
||||
MessagingContentProvider.buildConversationMessagesUri(mConversationId);
|
||||
loader = new BoundCursorLoader(bindingId, mContext, uri,
|
||||
ConversationMessageData.getProjection(), null, null, null);
|
||||
mLastMessageTimestamp = LAST_MESSAGE_TIMESTAMP_NaN;
|
||||
mMessageCount = MESSAGE_COUNT_NaN;
|
||||
} else {
|
||||
LogUtil.w(TAG, "Creating messages loader after unbinding mConversationId = " +
|
||||
mConversationId);
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull final Loader<Cursor> generic, final Cursor rawData) {
|
||||
final BoundCursorLoader loader = (BoundCursorLoader) generic;
|
||||
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(loader.getBindingId())) {
|
||||
// Check if we have a new message, or if we had a message sync.
|
||||
ConversationMessageData newMessage = null;
|
||||
boolean isSync = false;
|
||||
Cursor data = null;
|
||||
if (rawData != null) {
|
||||
// Note that the cursor is sorted DESC so here we reverse it.
|
||||
// This is a performance issue (improvement) for large cursors.
|
||||
data = new ReversedCursor(rawData);
|
||||
|
||||
final int messageCountOld = mMessageCount;
|
||||
mMessageCount = data.getCount();
|
||||
final ConversationMessageData lastMessage = getLastMessage(data);
|
||||
if (lastMessage != null) {
|
||||
final long lastMessageTimestampOld = mLastMessageTimestamp;
|
||||
mLastMessageTimestamp = lastMessage.getReceivedTimeStamp();
|
||||
final String lastMessageIdOld = mLastMessageId;
|
||||
mLastMessageId = lastMessage.getMessageId();
|
||||
if (TextUtils.equals(lastMessageIdOld, mLastMessageId) &&
|
||||
messageCountOld < mMessageCount) {
|
||||
// Last message stays the same (no incoming message) but message
|
||||
// count increased, which means there has been a message sync.
|
||||
isSync = true;
|
||||
} else if (messageCountOld != MESSAGE_COUNT_NaN && // Ignore initial load
|
||||
mLastMessageTimestamp != LAST_MESSAGE_TIMESTAMP_NaN &&
|
||||
mLastMessageTimestamp > lastMessageTimestampOld) {
|
||||
newMessage = lastMessage;
|
||||
}
|
||||
} else {
|
||||
mLastMessageTimestamp = LAST_MESSAGE_TIMESTAMP_NaN;
|
||||
}
|
||||
} else {
|
||||
mMessageCount = MESSAGE_COUNT_NaN;
|
||||
}
|
||||
|
||||
mListeners.onConversationMessagesCursorUpdated(ConversationData.this, data,
|
||||
newMessage, isSync);
|
||||
} else {
|
||||
LogUtil.w(TAG, "Messages loader finished after unbinding mConversationId = " +
|
||||
mConversationId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull final Loader<Cursor> generic) {
|
||||
final BoundCursorLoader loader = (BoundCursorLoader) generic;
|
||||
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(loader.getBindingId())) {
|
||||
mListeners.onConversationMessagesCursorUpdated(ConversationData.this, null, null,
|
||||
false);
|
||||
mLastMessageTimestamp = LAST_MESSAGE_TIMESTAMP_NaN;
|
||||
mMessageCount = MESSAGE_COUNT_NaN;
|
||||
} else {
|
||||
LogUtil.w(TAG, "Messages loader reset after unbinding mConversationId = " +
|
||||
mConversationId);
|
||||
}
|
||||
}
|
||||
|
||||
private ConversationMessageData getLastMessage(final Cursor cursor) {
|
||||
if (cursor != null && cursor.getCount() > 0) {
|
||||
final int position = cursor.getPosition();
|
||||
if (cursor.moveToLast()) {
|
||||
final ConversationMessageData messageData = new ConversationMessageData();
|
||||
messageData.bind(cursor);
|
||||
cursor.move(position);
|
||||
return messageData;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A trampoline class so that we can inherit from LoaderManager.LoaderCallbacks multiple times.
|
||||
*/
|
||||
private class ParticipantLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
|
||||
Assert.equals(PARTICIPANT_LOADER, id);
|
||||
Loader<Cursor> loader = null;
|
||||
|
||||
final String bindingId = args.getString(BINDING_ID);
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(bindingId)) {
|
||||
final Uri uri =
|
||||
MessagingContentProvider.buildConversationParticipantsUri(mConversationId);
|
||||
loader = new BoundCursorLoader(bindingId, mContext, uri,
|
||||
ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
|
||||
} else {
|
||||
LogUtil.w(TAG, "Creating participant loader after unbinding mConversationId = " +
|
||||
mConversationId);
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull final Loader<Cursor> generic, final Cursor data) {
|
||||
final BoundCursorLoader loader = (BoundCursorLoader) generic;
|
||||
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(loader.getBindingId())) {
|
||||
mParticipantData.bind(data);
|
||||
mListeners.onConversationParticipantDataLoaded(ConversationData.this);
|
||||
} else {
|
||||
LogUtil.w(TAG, "Participant loader finished after unbinding mConversationId = " +
|
||||
mConversationId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull final Loader<Cursor> generic) {
|
||||
final BoundCursorLoader loader = (BoundCursorLoader) generic;
|
||||
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(loader.getBindingId())) {
|
||||
mParticipantData.bind(null);
|
||||
} else {
|
||||
LogUtil.w(TAG, "Participant loader reset after unbinding mConversationId = " +
|
||||
mConversationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A trampoline class so that we can inherit from LoaderManager.LoaderCallbacks multiple times.
|
||||
*/
|
||||
private class SelfParticipantLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
|
||||
Assert.equals(SELF_PARTICIPANT_LOADER, id);
|
||||
Loader<Cursor> loader = null;
|
||||
|
||||
final String bindingId = args.getString(BINDING_ID);
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(bindingId)) {
|
||||
loader = new BoundCursorLoader(bindingId, mContext,
|
||||
MessagingContentProvider.PARTICIPANTS_URI,
|
||||
ParticipantData.ParticipantsQuery.PROJECTION,
|
||||
ParticipantColumns.SUB_ID + " <> ?",
|
||||
new String[] { String.valueOf(ParticipantData.OTHER_THAN_SELF_SUB_ID) },
|
||||
null);
|
||||
} else {
|
||||
LogUtil.w(TAG, "Creating self loader after unbinding mConversationId = " +
|
||||
mConversationId);
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull final Loader<Cursor> generic, final Cursor data) {
|
||||
final BoundCursorLoader loader = (BoundCursorLoader) generic;
|
||||
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(loader.getBindingId())) {
|
||||
mSelfParticipantsData.bind(data);
|
||||
mSubscriptionListData.bind(mSelfParticipantsData.getSelfParticipants(true));
|
||||
mListeners.onSubscriptionListDataLoaded(ConversationData.this);
|
||||
} else {
|
||||
LogUtil.w(TAG, "Self loader finished after unbinding mConversationId = " +
|
||||
mConversationId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull final Loader<Cursor> generic) {
|
||||
final BoundCursorLoader loader = (BoundCursorLoader) generic;
|
||||
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(loader.getBindingId())) {
|
||||
mSelfParticipantsData.bind(null);
|
||||
} else {
|
||||
LogUtil.w(TAG, "Self loader reset after unbinding mConversationId = " +
|
||||
mConversationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final ConversationDataEventDispatcher mListeners;
|
||||
private final MetadataLoaderCallbacks mMetadataLoaderCallbacks;
|
||||
private final MessagesLoaderCallbacks mMessagesLoaderCallbacks;
|
||||
private final ParticipantLoaderCallbacks mParticipantsLoaderCallbacks;
|
||||
private final SelfParticipantLoaderCallbacks mSelfParticipantLoaderCallbacks;
|
||||
private final Context mContext;
|
||||
private final String mConversationId;
|
||||
private final ConversationParticipantsData mParticipantData;
|
||||
private final SelfParticipantsData mSelfParticipantsData;
|
||||
private ConversationListItemData mConversationMetadata;
|
||||
private final SubscriptionListData mSubscriptionListData;
|
||||
private LoaderManager mLoaderManager;
|
||||
private long mLastMessageTimestamp = LAST_MESSAGE_TIMESTAMP_NaN;
|
||||
private int mMessageCount = MESSAGE_COUNT_NaN;
|
||||
private String mLastMessageId;
|
||||
|
||||
public ConversationData(final Context context, final ConversationDataListener listener,
|
||||
final String conversationId) {
|
||||
Assert.isTrue(conversationId != null);
|
||||
mContext = context;
|
||||
mConversationId = conversationId;
|
||||
mMetadataLoaderCallbacks = new MetadataLoaderCallbacks();
|
||||
mMessagesLoaderCallbacks = new MessagesLoaderCallbacks();
|
||||
mParticipantsLoaderCallbacks = new ParticipantLoaderCallbacks();
|
||||
mSelfParticipantLoaderCallbacks = new SelfParticipantLoaderCallbacks();
|
||||
mParticipantData = new ConversationParticipantsData();
|
||||
mConversationMetadata = new ConversationListItemData();
|
||||
mSelfParticipantsData = new SelfParticipantsData();
|
||||
mSubscriptionListData = new SubscriptionListData(context);
|
||||
|
||||
mListeners = new ConversationDataEventDispatcher();
|
||||
mListeners.add(listener);
|
||||
}
|
||||
|
||||
@RunsOnMainThread
|
||||
public void addConversationDataListener(final ConversationDataListener listener) {
|
||||
Assert.isMainThread();
|
||||
mListeners.add(listener);
|
||||
}
|
||||
|
||||
public String getConversationName() {
|
||||
return mConversationMetadata.getName();
|
||||
}
|
||||
|
||||
public boolean getIsArchived() {
|
||||
return mConversationMetadata.getIsArchived();
|
||||
}
|
||||
|
||||
public String getIcon() {
|
||||
return mConversationMetadata.getIcon();
|
||||
}
|
||||
|
||||
public String getConversationId() {
|
||||
return mConversationId;
|
||||
}
|
||||
|
||||
public void setFocus() {
|
||||
DataModel.get().setFocusedConversation(mConversationId);
|
||||
// As we are loading the conversation assume the user has read the messages...
|
||||
// Do this late though so that it doesn't get in the way of other actions
|
||||
BugleNotifications.markMessagesAsRead(mConversationId);
|
||||
}
|
||||
|
||||
public void unsetFocus() {
|
||||
DataModel.get().setFocusedConversation(null);
|
||||
}
|
||||
|
||||
public boolean isFocused() {
|
||||
return isBound() && DataModel.get().isFocusedConversation(mConversationId);
|
||||
}
|
||||
|
||||
private static final int CONVERSATION_META_DATA_LOADER = 1;
|
||||
private static final int CONVERSATION_MESSAGES_LOADER = 2;
|
||||
private static final int PARTICIPANT_LOADER = 3;
|
||||
private static final int SELF_PARTICIPANT_LOADER = 4;
|
||||
|
||||
public void init(final LoaderManager loaderManager,
|
||||
final BindingBase<ConversationData> binding) {
|
||||
// Remember the binding id so that loader callbacks can check if data is still bound
|
||||
// to same ui component
|
||||
final Bundle args = new Bundle();
|
||||
args.putString(BINDING_ID, binding.getBindingId());
|
||||
mLoaderManager = loaderManager;
|
||||
mLoaderManager.initLoader(CONVERSATION_META_DATA_LOADER, args, mMetadataLoaderCallbacks);
|
||||
mLoaderManager.initLoader(CONVERSATION_MESSAGES_LOADER, args, mMessagesLoaderCallbacks);
|
||||
mLoaderManager.initLoader(PARTICIPANT_LOADER, args, mParticipantsLoaderCallbacks);
|
||||
mLoaderManager.initLoader(SELF_PARTICIPANT_LOADER, args, mSelfParticipantLoaderCallbacks);
|
||||
}
|
||||
|
||||
public void restart(final BindingBase<ConversationData> binding) {
|
||||
final Bundle args = new Bundle();
|
||||
args.putString(BINDING_ID, binding.getBindingId());
|
||||
mLoaderManager.restartLoader(CONVERSATION_META_DATA_LOADER, args, mMetadataLoaderCallbacks);
|
||||
mLoaderManager.restartLoader(CONVERSATION_MESSAGES_LOADER, args, mMessagesLoaderCallbacks);
|
||||
mLoaderManager.restartLoader(PARTICIPANT_LOADER, args, mParticipantsLoaderCallbacks);
|
||||
mLoaderManager.restartLoader(SELF_PARTICIPANT_LOADER, args,
|
||||
mSelfParticipantLoaderCallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unregisterListeners() {
|
||||
mListeners.clear();
|
||||
// Make sure focus has moved away from this conversation
|
||||
// TODO: May false trigger if destroy happens after "new" conversation is focused.
|
||||
// Assert.isTrue(!DataModel.get().isFocusedConversation(mConversationId));
|
||||
|
||||
// This could be null if we bind but the caller doesn't init the BindableData
|
||||
if (mLoaderManager != null) {
|
||||
mLoaderManager.destroyLoader(CONVERSATION_META_DATA_LOADER);
|
||||
mLoaderManager.destroyLoader(CONVERSATION_MESSAGES_LOADER);
|
||||
mLoaderManager.destroyLoader(PARTICIPANT_LOADER);
|
||||
mLoaderManager.destroyLoader(SELF_PARTICIPANT_LOADER);
|
||||
mLoaderManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default self participant in the participant table (NOT the conversation's self).
|
||||
* This is available as soon as self participant data is loaded.
|
||||
*/
|
||||
public ParticipantData getDefaultSelfParticipant() {
|
||||
return mSelfParticipantsData.getDefaultSelfParticipant();
|
||||
}
|
||||
|
||||
public List<ParticipantData> getSelfParticipants(final boolean activeOnly) {
|
||||
return mSelfParticipantsData.getSelfParticipants(activeOnly);
|
||||
}
|
||||
|
||||
public int getSelfParticipantsCountExcludingDefault(final boolean activeOnly) {
|
||||
return mSelfParticipantsData.getSelfParticipantsCountExcludingDefault(activeOnly);
|
||||
}
|
||||
|
||||
public ParticipantData getSelfParticipantById(final String selfId) {
|
||||
return mSelfParticipantsData.getSelfParticipantById(selfId);
|
||||
}
|
||||
|
||||
/**
|
||||
* For a 1:1 conversation return the other (not self) participant (else null)
|
||||
*/
|
||||
public ParticipantData getOtherParticipant() {
|
||||
return mParticipantData.getOtherParticipant();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true once the participants are loaded
|
||||
*/
|
||||
public boolean getParticipantsLoaded() {
|
||||
return mParticipantData.isLoaded();
|
||||
}
|
||||
|
||||
public void sendMessage(final BindingBase<ConversationData> binding,
|
||||
final MessageData message) {
|
||||
Assert.isTrue(TextUtils.equals(mConversationId, message.getConversationId()));
|
||||
Assert.isTrue(binding.getData() == this);
|
||||
|
||||
if (message.getSelfId() == null) {
|
||||
InsertNewMessageAction.insertNewMessage(message);
|
||||
} else {
|
||||
final int systemDefaultSubId = PhoneUtils.getDefault().getDefaultSmsSubscriptionId();
|
||||
if (systemDefaultSubId != ParticipantData.DEFAULT_SELF_SUB_ID &&
|
||||
mSelfParticipantsData.isDefaultSelf(message.getSelfId())) {
|
||||
// Lock the sub selection to the system default SIM as soon as the user clicks on
|
||||
// the send button to avoid races between this and when InsertNewMessageAction is
|
||||
// actually executed on the data model thread, during which the user can potentially
|
||||
// change the system default SIM in Settings.
|
||||
InsertNewMessageAction.insertNewMessage(message, systemDefaultSubId);
|
||||
} else {
|
||||
InsertNewMessageAction.insertNewMessage(message);
|
||||
}
|
||||
}
|
||||
// Update contacts so Frequents will reflect messaging activity.
|
||||
if (!getParticipantsLoaded()) {
|
||||
return; // oh well, not critical
|
||||
}
|
||||
final ArrayList<String> phones = new ArrayList<>();
|
||||
final ArrayList<String> emails = new ArrayList<>();
|
||||
for (final ParticipantData participant : mParticipantData) {
|
||||
if (!participant.isSelf()) {
|
||||
if (participant.isEmail()) {
|
||||
emails.add(participant.getSendDestination());
|
||||
} else {
|
||||
phones.add(participant.getSendDestination());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void downloadMessage(final BindingBase<ConversationData> binding,
|
||||
final String messageId) {
|
||||
Assert.isTrue(binding.getData() == this);
|
||||
Assert.notNull(messageId);
|
||||
RedownloadMmsAction.redownloadMessage(messageId);
|
||||
}
|
||||
|
||||
public void resendMessage(final BindingBase<ConversationData> binding, final String messageId) {
|
||||
Assert.isTrue(binding.getData() == this);
|
||||
Assert.notNull(messageId);
|
||||
ResendMessageAction.resendMessage(messageId);
|
||||
}
|
||||
|
||||
public void deleteMessage(final BindingBase<ConversationData> binding, final String messageId) {
|
||||
Assert.isTrue(binding.getData() == this);
|
||||
Assert.notNull(messageId);
|
||||
DeleteMessageAction.deleteMessage(messageId);
|
||||
}
|
||||
|
||||
public void deleteConversation(final Binding<ConversationData> binding) {
|
||||
Assert.isTrue(binding.getData() == this);
|
||||
// If possible use timestamp of last message shown to delete only messages user is aware of
|
||||
if (mConversationMetadata == null) {
|
||||
DeleteConversationAction.deleteConversation(mConversationId,
|
||||
System.currentTimeMillis());
|
||||
} else {
|
||||
mConversationMetadata.deleteConversation();
|
||||
}
|
||||
}
|
||||
|
||||
public void archiveConversation(final BindingBase<ConversationData> binding) {
|
||||
Assert.isTrue(binding.getData() == this);
|
||||
UpdateConversationArchiveStatusAction.archiveConversation(mConversationId);
|
||||
}
|
||||
|
||||
public void unarchiveConversation(final BindingBase<ConversationData> binding) {
|
||||
Assert.isTrue(binding.getData() == this);
|
||||
UpdateConversationArchiveStatusAction.unarchiveConversation(mConversationId);
|
||||
}
|
||||
|
||||
public ConversationParticipantsData getParticipants() {
|
||||
return mParticipantData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a dialable phone number for the participant if we are in a 1-1 conversation.
|
||||
* @return the participant phone number, or null if the phone number is not valid or if there
|
||||
* are more than one participant.
|
||||
*/
|
||||
public String getParticipantPhoneNumber() {
|
||||
final ParticipantData participant = this.getOtherParticipant();
|
||||
if (participant != null) {
|
||||
final String phoneNumber = participant.getSendDestination();
|
||||
if (!TextUtils.isEmpty(phoneNumber) && MmsSmsUtils.isPhoneNumber(phoneNumber)) {
|
||||
return phoneNumber;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a message to be forwarded from an existing message.
|
||||
*/
|
||||
public MessageData createForwardedMessage(final ConversationMessageData message) {
|
||||
final MessageData forwardedMessage = new MessageData();
|
||||
|
||||
final String originalSubject =
|
||||
MmsUtils.cleanseMmsSubject(mContext.getResources(), message.getMmsSubject());
|
||||
if (!TextUtils.isEmpty(originalSubject)) {
|
||||
forwardedMessage.setMmsSubject(
|
||||
mContext.getResources().getString(R.string.message_fwd, originalSubject));
|
||||
}
|
||||
|
||||
for (final MessagePartData part : message.getParts()) {
|
||||
MessagePartData forwardedPart;
|
||||
|
||||
// Depending on the part type, if it is text, we can directly create a text part;
|
||||
// if it is attachment, then we need to create a pending attachment data out of it, so
|
||||
// that we may persist the attachment locally in the scratch folder when the user picks
|
||||
// a conversation to forward to.
|
||||
if (part.isText()) {
|
||||
forwardedPart = MessagePartData.createTextMessagePart(part.getText());
|
||||
} else {
|
||||
final PendingAttachmentData pendingAttachmentData = PendingAttachmentData
|
||||
.createPendingAttachmentData(part.getContentType(), part.getContentUri());
|
||||
forwardedPart = pendingAttachmentData;
|
||||
}
|
||||
forwardedMessage.addPart(forwardedPart);
|
||||
}
|
||||
return forwardedMessage;
|
||||
}
|
||||
|
||||
public int getNumberOfParticipantsExcludingSelf() {
|
||||
return mParticipantData.getNumberOfParticipantsExcludingSelf();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link com.android.messaging.datamodel.data.SubscriptionListData
|
||||
* .SubscriptionListEntry} for a given self participant so UI can display SIM-related info
|
||||
* (icon, name etc.) for multi-SIM.
|
||||
*/
|
||||
public SubscriptionListEntry getSubscriptionEntryForSelfParticipant(
|
||||
final String selfParticipantId, final boolean excludeDefault) {
|
||||
return getSubscriptionEntryForSelfParticipant(selfParticipantId, excludeDefault,
|
||||
mSubscriptionListData, mSelfParticipantsData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link com.android.messaging.datamodel.data.SubscriptionListData
|
||||
* .SubscriptionListEntry} for a given self participant so UI can display SIM-related info
|
||||
* (icon, name etc.) for multi-SIM.
|
||||
*/
|
||||
public static SubscriptionListEntry getSubscriptionEntryForSelfParticipant(
|
||||
final String selfParticipantId, final boolean excludeDefault,
|
||||
final SubscriptionListData subscriptionListData,
|
||||
final SelfParticipantsData selfParticipantsData) {
|
||||
// SIM indicators are shown in the UI only if:
|
||||
// 1. Framework has MSIM support AND
|
||||
// 2. The device has had multiple *active* subscriptions. AND
|
||||
// 3. The message's subscription is active.
|
||||
if (selfParticipantsData.getSelfParticipantsCountExcludingDefault(true) > 1) {
|
||||
return subscriptionListData.getActiveSubscriptionEntryBySelfId(selfParticipantId,
|
||||
excludeDefault);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public SubscriptionListData getSubscriptionListData() {
|
||||
return mSubscriptionListData;
|
||||
}
|
||||
|
||||
/**
|
||||
* A placeholder implementation of {@link ConversationDataListener} so that subclasses may opt
|
||||
* to implement some, but not all, of the interface methods.
|
||||
*/
|
||||
public static class SimpleConversationDataListener implements ConversationDataListener {
|
||||
|
||||
@Override
|
||||
public void onConversationMessagesCursorUpdated(final ConversationData data, final Cursor cursor,
|
||||
@Nullable
|
||||
final
|
||||
ConversationMessageData newestMessage, final boolean isSync) {}
|
||||
|
||||
@Override
|
||||
public void onConversationMetadataUpdated(final ConversationData data) {}
|
||||
|
||||
@Override
|
||||
public void closeConversation(final String conversationId) {}
|
||||
|
||||
@Override
|
||||
public void onConversationParticipantDataLoaded(final ConversationData data) {}
|
||||
|
||||
@Override
|
||||
public void onSubscriptionListDataLoaded(final ConversationData data) {}
|
||||
|
||||
}
|
||||
|
||||
private class ConversationDataEventDispatcher
|
||||
extends ArrayList<ConversationDataListener>
|
||||
implements ConversationDataListener {
|
||||
|
||||
@Override
|
||||
public void onConversationMessagesCursorUpdated(final ConversationData data, final Cursor cursor,
|
||||
@Nullable
|
||||
final ConversationMessageData newestMessage, final boolean isSync) {
|
||||
for (final ConversationDataListener listener : this) {
|
||||
listener.onConversationMessagesCursorUpdated(data, cursor, newestMessage, isSync);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConversationMetadataUpdated(final ConversationData data) {
|
||||
for (final ConversationDataListener listener : this) {
|
||||
listener.onConversationMetadataUpdated(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeConversation(final String conversationId) {
|
||||
for (final ConversationDataListener listener : this) {
|
||||
listener.closeConversation(conversationId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConversationParticipantDataLoaded(final ConversationData data) {
|
||||
for (final ConversationDataListener listener : this) {
|
||||
listener.onConversationParticipantDataLoaded(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubscriptionListDataLoaded(final ConversationData data) {
|
||||
for (final ConversationDataListener listener : this) {
|
||||
listener.onSubscriptionListDataLoaded(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import com.android.messaging.datamodel.BoundCursorLoader;
|
||||
import com.android.messaging.datamodel.BugleNotifications;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.SyncManager;
|
||||
import com.android.messaging.datamodel.binding.BindableData;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.datamodel.data.ConversationListItemData.ConversationListViewColumns;
|
||||
import com.android.messaging.receiver.SmsReceiver;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
||||
public class ConversationListData extends BindableData
|
||||
implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
private static final String BINDING_ID = "bindingId";
|
||||
public static final String SORT_ORDER =
|
||||
ConversationListViewColumns.SORT_TIMESTAMP + " DESC";
|
||||
|
||||
private static final String WHERE_ARCHIVED =
|
||||
"(" + ConversationListViewColumns.ARCHIVE_STATUS + " = 1)";
|
||||
public static final String WHERE_NOT_ARCHIVED =
|
||||
"(" + ConversationListViewColumns.ARCHIVE_STATUS + " = 0)";
|
||||
|
||||
public interface ConversationListDataListener {
|
||||
void onConversationListCursorUpdated(ConversationListData data, Cursor cursor);
|
||||
void setBlockedParticipantsAvailable(boolean blockedAvailable);
|
||||
}
|
||||
|
||||
private ConversationListDataListener mListener;
|
||||
private final Context mContext;
|
||||
private final boolean mArchivedMode;
|
||||
private LoaderManager mLoaderManager;
|
||||
|
||||
public ConversationListData(final Context context, final ConversationListDataListener listener,
|
||||
final boolean archivedMode) {
|
||||
mListener = listener;
|
||||
mContext = context;
|
||||
mArchivedMode = archivedMode;
|
||||
}
|
||||
|
||||
private static final int CONVERSATION_LIST_LOADER = 1;
|
||||
private static final int BLOCKED_PARTICIPANTS_AVAILABLE_LOADER = 2;
|
||||
|
||||
private static final String[] BLOCKED_PARTICIPANTS_PROJECTION = new String[] {
|
||||
ParticipantColumns._ID,
|
||||
ParticipantColumns.NORMALIZED_DESTINATION,
|
||||
};
|
||||
private static final int INDEX_BLOCKED_PARTICIPANTS_ID = 0;
|
||||
private static final int INDEX_BLOCKED_PARTICIPANTS_NORMALIZED_DESTINATION = 1;
|
||||
|
||||
// all blocked participants
|
||||
private final HashSet<String> mBlockedParticipants = new HashSet<>();
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
|
||||
final String bindingId = args.getString(BINDING_ID);
|
||||
Loader<Cursor> loader = null;
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(bindingId)) {
|
||||
switch (id) {
|
||||
case BLOCKED_PARTICIPANTS_AVAILABLE_LOADER:
|
||||
loader = new BoundCursorLoader(bindingId, mContext,
|
||||
MessagingContentProvider.PARTICIPANTS_URI,
|
||||
BLOCKED_PARTICIPANTS_PROJECTION,
|
||||
ParticipantColumns.BLOCKED + "=1", null, null);
|
||||
break;
|
||||
case CONVERSATION_LIST_LOADER:
|
||||
loader = new BoundCursorLoader(bindingId, mContext,
|
||||
MessagingContentProvider.CONVERSATIONS_URI,
|
||||
ConversationListItemData.PROJECTION,
|
||||
mArchivedMode ? WHERE_ARCHIVED : WHERE_NOT_ARCHIVED,
|
||||
null, // selection args
|
||||
SORT_ORDER);
|
||||
break;
|
||||
default:
|
||||
Assert.fail("Unknown loader id");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(TAG, "Creating loader after unbinding list");
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull final Loader<Cursor> generic, final Cursor data) {
|
||||
final BoundCursorLoader loader = (BoundCursorLoader) generic;
|
||||
if (isBound(loader.getBindingId())) {
|
||||
switch (loader.getId()) {
|
||||
case BLOCKED_PARTICIPANTS_AVAILABLE_LOADER:
|
||||
mBlockedParticipants.clear();
|
||||
for (int i = 0; i < data.getCount(); i++) {
|
||||
data.moveToPosition(i);
|
||||
mBlockedParticipants.add(data.getString(
|
||||
INDEX_BLOCKED_PARTICIPANTS_NORMALIZED_DESTINATION));
|
||||
}
|
||||
mListener.setBlockedParticipantsAvailable(data != null && data.getCount() > 0);
|
||||
break;
|
||||
case CONVERSATION_LIST_LOADER:
|
||||
mListener.onConversationListCursorUpdated(this, data);
|
||||
break;
|
||||
default:
|
||||
Assert.fail("Unknown loader id");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(TAG, "Loader finished after unbinding list");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull final Loader<Cursor> generic) {
|
||||
final BoundCursorLoader loader = (BoundCursorLoader) generic;
|
||||
if (isBound(loader.getBindingId())) {
|
||||
switch (loader.getId()) {
|
||||
case BLOCKED_PARTICIPANTS_AVAILABLE_LOADER:
|
||||
mListener.setBlockedParticipantsAvailable(false);
|
||||
break;
|
||||
case CONVERSATION_LIST_LOADER:
|
||||
mListener.onConversationListCursorUpdated(this, null);
|
||||
break;
|
||||
default:
|
||||
Assert.fail("Unknown loader id");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(TAG, "Loader reset after unbinding list");
|
||||
}
|
||||
}
|
||||
|
||||
private Bundle mArgs;
|
||||
|
||||
public void init(final LoaderManager loaderManager,
|
||||
final BindingBase<ConversationListData> binding) {
|
||||
mArgs = new Bundle();
|
||||
mArgs.putString(BINDING_ID, binding.getBindingId());
|
||||
mLoaderManager = loaderManager;
|
||||
mLoaderManager.initLoader(CONVERSATION_LIST_LOADER, mArgs, this);
|
||||
mLoaderManager.initLoader(BLOCKED_PARTICIPANTS_AVAILABLE_LOADER, mArgs, this);
|
||||
}
|
||||
|
||||
public void handleMessagesSeen() {
|
||||
BugleNotifications.markAllMessagesAsSeen();
|
||||
|
||||
SmsReceiver.cancelSecondaryUserNotification();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unregisterListeners() {
|
||||
mListener = null;
|
||||
|
||||
// This could be null if we bind but the caller doesn't init the BindableData
|
||||
if (mLoaderManager != null) {
|
||||
mLoaderManager.destroyLoader(CONVERSATION_LIST_LOADER);
|
||||
mLoaderManager.destroyLoader(BLOCKED_PARTICIPANTS_AVAILABLE_LOADER);
|
||||
mLoaderManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getHasFirstSyncCompleted() {
|
||||
final SyncManager syncManager = DataModel.get().getSyncManager();
|
||||
return syncManager.getHasFirstSyncCompleted();
|
||||
}
|
||||
|
||||
public void setScrolledToNewestConversation(boolean scrolledToNewestConversation) {
|
||||
DataModel.get().setConversationListScrolledToNewestConversation(
|
||||
scrolledToNewestConversation);
|
||||
if (scrolledToNewestConversation) {
|
||||
handleMessagesSeen();
|
||||
}
|
||||
}
|
||||
|
||||
public HashSet<String> getBlockedParticipants() {
|
||||
return mBlockedParticipants;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.action.DeleteConversationAction;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.ContactUtil;
|
||||
import com.android.messaging.util.Dates;
|
||||
import com.google.common.base.Joiner;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class wrapping the conversation list view used to display each item in conversation list
|
||||
*/
|
||||
public class ConversationListItemData {
|
||||
private String mConversationId;
|
||||
private String mName;
|
||||
private String mIcon;
|
||||
private boolean mIsRead;
|
||||
private long mTimestamp;
|
||||
private String mSnippetText;
|
||||
private Uri mPreviewUri;
|
||||
private String mPreviewContentType;
|
||||
private long mParticipantContactId;
|
||||
private String mParticipantLookupKey;
|
||||
private String mOtherParticipantNormalizedDestination;
|
||||
private String mSelfId;
|
||||
private int mParticipantCount;
|
||||
private boolean mIncludeEmailAddress;
|
||||
private int mMessageStatus;
|
||||
private int mMessageRawTelephonyStatus;
|
||||
private boolean mShowDraft;
|
||||
private Uri mDraftPreviewUri;
|
||||
private String mDraftPreviewContentType;
|
||||
private String mDraftSnippetText;
|
||||
private boolean mIsArchived;
|
||||
private String mSubject;
|
||||
private String mDraftSubject;
|
||||
private String mSnippetSenderFirstName;
|
||||
private String mSnippetSenderDisplayDestination;
|
||||
private boolean mIsEnterprise;
|
||||
|
||||
public ConversationListItemData() {
|
||||
}
|
||||
|
||||
public void bind(final Cursor cursor) {
|
||||
bind(cursor, false);
|
||||
}
|
||||
|
||||
public void bind(final Cursor cursor, final boolean ignoreDraft) {
|
||||
mConversationId = cursor.getString(INDEX_ID);
|
||||
mName = cursor.getString(INDEX_CONVERSATION_NAME);
|
||||
mIcon = cursor.getString(INDEX_CONVERSATION_ICON);
|
||||
mSnippetText = cursor.getString(INDEX_SNIPPET_TEXT);
|
||||
mTimestamp = cursor.getLong(INDEX_SORT_TIMESTAMP);
|
||||
mIsRead = cursor.getInt(INDEX_READ) == 1;
|
||||
final String previewUriString = cursor.getString(INDEX_PREVIEW_URI);
|
||||
mPreviewUri = TextUtils.isEmpty(previewUriString) ? null : Uri.parse(previewUriString);
|
||||
mPreviewContentType = cursor.getString(INDEX_PREVIEW_CONTENT_TYPE);
|
||||
mParticipantContactId = cursor.getLong(INDEX_PARTICIPANT_CONTACT_ID);
|
||||
mParticipantLookupKey = cursor.getString(INDEX_PARTICIPANT_LOOKUP_KEY);
|
||||
mOtherParticipantNormalizedDestination = cursor.getString(
|
||||
INDEX_OTHER_PARTICIPANT_NORMALIZED_DESTINATION);
|
||||
mSelfId = cursor.getString(INDEX_SELF_ID);
|
||||
mParticipantCount = cursor.getInt(INDEX_PARTICIPANT_COUNT);
|
||||
mIncludeEmailAddress = cursor.getInt(INDEX_INCLUDE_EMAIL_ADDRESS) == 1;
|
||||
mMessageStatus = cursor.getInt(INDEX_MESSAGE_STATUS);
|
||||
mMessageRawTelephonyStatus = cursor.getInt(INDEX_MESSAGE_RAW_TELEPHONY_STATUS);
|
||||
if (!ignoreDraft) {
|
||||
mShowDraft = cursor.getInt(INDEX_SHOW_DRAFT) == 1;
|
||||
final String draftPreviewUriString = cursor.getString(INDEX_DRAFT_PREVIEW_URI);
|
||||
mDraftPreviewUri = TextUtils.isEmpty(draftPreviewUriString) ?
|
||||
null : Uri.parse(draftPreviewUriString);
|
||||
mDraftPreviewContentType = cursor.getString(INDEX_DRAFT_PREVIEW_CONTENT_TYPE);
|
||||
mDraftSnippetText = cursor.getString(INDEX_DRAFT_SNIPPET_TEXT);
|
||||
mDraftSubject = cursor.getString(INDEX_DRAFT_SUBJECT_TEXT);
|
||||
} else {
|
||||
mShowDraft = false;
|
||||
mDraftPreviewUri = null;
|
||||
mDraftPreviewContentType = null;
|
||||
mDraftSnippetText = null;
|
||||
mDraftSubject = null;
|
||||
}
|
||||
|
||||
mIsArchived = cursor.getInt(INDEX_ARCHIVE_STATUS) == 1;
|
||||
mSubject = cursor.getString(INDEX_SUBJECT_TEXT);
|
||||
mSnippetSenderFirstName = cursor.getString(INDEX_SNIPPET_SENDER_FIRST_NAME);
|
||||
mSnippetSenderDisplayDestination =
|
||||
cursor.getString(INDEX_SNIPPET_SENDER_DISPLAY_DESTINATION);
|
||||
mIsEnterprise = cursor.getInt(INDEX_IS_ENTERPRISE) == 1;
|
||||
}
|
||||
|
||||
public String getConversationId() {
|
||||
return mConversationId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
public String getIcon() {
|
||||
return mIcon;
|
||||
}
|
||||
|
||||
public boolean getIsRead() {
|
||||
return mIsRead;
|
||||
}
|
||||
|
||||
public String getFormattedTimestamp() {
|
||||
return Dates.getConversationTimeString(mTimestamp).toString();
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return mTimestamp;
|
||||
}
|
||||
|
||||
public String getSnippetText() {
|
||||
return mSnippetText;
|
||||
}
|
||||
|
||||
public Uri getPreviewUri() {
|
||||
return mPreviewUri;
|
||||
}
|
||||
|
||||
public String getPreviewContentType() {
|
||||
return mPreviewContentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ConversationColumns#PARTICIPANT_CONTACT_ID
|
||||
* @return the contact id of the participant if it is a 1:1 conversation, -1 for group.
|
||||
*/
|
||||
public long getParticipantContactId() {
|
||||
return mParticipantContactId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ConversationColumns#IS_ENTERPRISE
|
||||
* @return whether the conversation is enterprise.
|
||||
*/
|
||||
public boolean isEnterprise() {
|
||||
return mIsEnterprise;
|
||||
}
|
||||
|
||||
public String getParticipantLookupKey() {
|
||||
return mParticipantLookupKey;
|
||||
}
|
||||
|
||||
public String getOtherParticipantNormalizedDestination() {
|
||||
return mOtherParticipantNormalizedDestination;
|
||||
}
|
||||
|
||||
public String getSelfId() {
|
||||
return mSelfId;
|
||||
}
|
||||
|
||||
public int getParticipantCount() {
|
||||
return mParticipantCount;
|
||||
}
|
||||
|
||||
public boolean getIsGroup() {
|
||||
// Participant count excludes self
|
||||
return (mParticipantCount > 1);
|
||||
}
|
||||
|
||||
public boolean getIncludeEmailAddress() {
|
||||
return mIncludeEmailAddress;
|
||||
}
|
||||
|
||||
public final boolean getIsFailedStatus() {
|
||||
return (mMessageStatus == MessageData.BUGLE_STATUS_OUTGOING_FAILED ||
|
||||
mMessageStatus == MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER ||
|
||||
mMessageStatus == MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED ||
|
||||
mMessageStatus == MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE);
|
||||
}
|
||||
|
||||
public final boolean getIsSendRequested() {
|
||||
return (mMessageStatus == MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND ||
|
||||
mMessageStatus == MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY ||
|
||||
mMessageStatus == MessageData.BUGLE_STATUS_OUTGOING_SENDING ||
|
||||
mMessageStatus == MessageData.BUGLE_STATUS_OUTGOING_RESENDING);
|
||||
}
|
||||
|
||||
public boolean getIsMessageTypeOutgoing() {
|
||||
return !MessageData.getIsIncoming(mMessageStatus);
|
||||
}
|
||||
|
||||
public int getMessageRawTelephonyStatus() {
|
||||
return mMessageRawTelephonyStatus;
|
||||
}
|
||||
|
||||
public int getMessageStatus() {
|
||||
return mMessageStatus;
|
||||
}
|
||||
|
||||
public boolean getShowDraft() {
|
||||
return mShowDraft;
|
||||
}
|
||||
|
||||
public String getDraftSnippetText() {
|
||||
return mDraftSnippetText;
|
||||
}
|
||||
|
||||
public Uri getDraftPreviewUri() {
|
||||
return mDraftPreviewUri;
|
||||
}
|
||||
|
||||
public String getDraftPreviewContentType() {
|
||||
return mDraftPreviewContentType;
|
||||
}
|
||||
|
||||
public boolean getIsArchived() {
|
||||
return mIsArchived;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return mSubject;
|
||||
}
|
||||
|
||||
public String getDraftSubject() {
|
||||
return mDraftSubject;
|
||||
}
|
||||
|
||||
public String getSnippetSenderName() {
|
||||
if (!TextUtils.isEmpty(mSnippetSenderFirstName)) {
|
||||
return mSnippetSenderFirstName;
|
||||
}
|
||||
return mSnippetSenderDisplayDestination;
|
||||
}
|
||||
|
||||
public void deleteConversation() {
|
||||
DeleteConversationAction.deleteConversation(mConversationId, mTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the view for this data item
|
||||
*/
|
||||
public static String getConversationListView() {
|
||||
return CONVERSATION_LIST_VIEW;
|
||||
}
|
||||
|
||||
public static String getConversationListViewSql() {
|
||||
return CONVERSATION_LIST_VIEW_SQL;
|
||||
}
|
||||
|
||||
private static final String CONVERSATION_LIST_VIEW = "conversation_list_view";
|
||||
|
||||
private static final String CONVERSATION_LIST_VIEW_PROJECTION =
|
||||
DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns._ID
|
||||
+ " as " + ConversationListViewColumns._ID + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.NAME
|
||||
+ " as " + ConversationListViewColumns.NAME + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.CURRENT_SELF_ID
|
||||
+ " as " + ConversationListViewColumns.CURRENT_SELF_ID + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.ARCHIVE_STATUS
|
||||
+ " as " + ConversationListViewColumns.ARCHIVE_STATUS + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.READ
|
||||
+ " as " + ConversationListViewColumns.READ + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.ICON
|
||||
+ " as " + ConversationListViewColumns.ICON + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.PARTICIPANT_CONTACT_ID
|
||||
+ " as " + ConversationListViewColumns.PARTICIPANT_CONTACT_ID + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.PARTICIPANT_LOOKUP_KEY
|
||||
+ " as " + ConversationListViewColumns.PARTICIPANT_LOOKUP_KEY + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.'
|
||||
+ ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION
|
||||
+ " as " + ConversationListViewColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.SORT_TIMESTAMP
|
||||
+ " as " + ConversationListViewColumns.SORT_TIMESTAMP + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.SHOW_DRAFT
|
||||
+ " as " + ConversationListViewColumns.SHOW_DRAFT + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.DRAFT_SNIPPET_TEXT
|
||||
+ " as " + ConversationListViewColumns.DRAFT_SNIPPET_TEXT + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.DRAFT_PREVIEW_URI
|
||||
+ " as " + ConversationListViewColumns.DRAFT_PREVIEW_URI + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.DRAFT_SUBJECT_TEXT
|
||||
+ " as " + ConversationListViewColumns.DRAFT_SUBJECT_TEXT + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.'
|
||||
+ ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE
|
||||
+ " as " + ConversationListViewColumns.DRAFT_PREVIEW_CONTENT_TYPE + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.PREVIEW_URI
|
||||
+ " as " + ConversationListViewColumns.PREVIEW_URI + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.PREVIEW_CONTENT_TYPE
|
||||
+ " as " + ConversationListViewColumns.PREVIEW_CONTENT_TYPE + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.PARTICIPANT_COUNT
|
||||
+ " as " + ConversationListViewColumns.PARTICIPANT_COUNT + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' +
|
||||
ConversationColumns.INCLUDE_EMAIL_ADDRESS
|
||||
+ " as " + ConversationListViewColumns.INCLUDE_EMAIL_ADDRESS + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.STATUS
|
||||
+ " as " + ConversationListViewColumns.MESSAGE_STATUS + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RAW_TELEPHONY_STATUS
|
||||
+ " as " + ConversationListViewColumns.MESSAGE_RAW_TELEPHONY_STATUS + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID
|
||||
+ " as " + ConversationListViewColumns.MESSAGE_ID + ", "
|
||||
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FIRST_NAME
|
||||
+ " as " + ConversationListViewColumns.SNIPPET_SENDER_FIRST_NAME + ", "
|
||||
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.DISPLAY_DESTINATION
|
||||
+ " as " + ConversationListViewColumns.SNIPPET_SENDER_DISPLAY_DESTINATION + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.IS_ENTERPRISE
|
||||
+ " as " + ConversationListViewColumns.IS_ENTERPRISE;
|
||||
|
||||
private static final String JOIN_PARTICIPANTS =
|
||||
" LEFT JOIN " + DatabaseHelper.PARTICIPANTS_TABLE + " ON ("
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID
|
||||
+ '=' + DatabaseHelper.PARTICIPANTS_TABLE + '.' + DatabaseHelper.ParticipantColumns._ID
|
||||
+ ") ";
|
||||
|
||||
// View that makes latest message read flag available with rest of conversation data.
|
||||
private static final String CONVERSATION_LIST_VIEW_SQL = "CREATE VIEW " +
|
||||
CONVERSATION_LIST_VIEW + " AS SELECT "
|
||||
+ CONVERSATION_LIST_VIEW_PROJECTION + ", "
|
||||
// Snippet not part of the base projection shared with search view
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.SNIPPET_TEXT
|
||||
+ " as " + ConversationListViewColumns.SNIPPET_TEXT + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.SUBJECT_TEXT
|
||||
+ " as " + ConversationListViewColumns.SUBJECT_TEXT + " "
|
||||
+ " FROM " + DatabaseHelper.CONVERSATIONS_TABLE
|
||||
+ " LEFT JOIN " + DatabaseHelper.MESSAGES_TABLE + " ON ("
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.LATEST_MESSAGE_ID
|
||||
+ '=' + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID + ") "
|
||||
+ JOIN_PARTICIPANTS
|
||||
+ "ORDER BY " + DatabaseHelper.CONVERSATIONS_TABLE + '.'
|
||||
+ ConversationColumns.SORT_TIMESTAMP + " DESC";
|
||||
|
||||
public static class ConversationListViewColumns implements BaseColumns {
|
||||
public static final String _ID = ConversationColumns._ID;
|
||||
static final String NAME = ConversationColumns.NAME;
|
||||
static final String ARCHIVE_STATUS = ConversationColumns.ARCHIVE_STATUS;
|
||||
static final String READ = MessageColumns.READ;
|
||||
static final String SORT_TIMESTAMP = ConversationColumns.SORT_TIMESTAMP;
|
||||
static final String PREVIEW_URI = ConversationColumns.PREVIEW_URI;
|
||||
static final String PREVIEW_CONTENT_TYPE = ConversationColumns.PREVIEW_CONTENT_TYPE;
|
||||
static final String SNIPPET_TEXT = ConversationColumns.SNIPPET_TEXT;
|
||||
static final String SUBJECT_TEXT = ConversationColumns.SUBJECT_TEXT;
|
||||
static final String ICON = ConversationColumns.ICON;
|
||||
static final String SHOW_DRAFT = ConversationColumns.SHOW_DRAFT;
|
||||
static final String DRAFT_SUBJECT_TEXT = ConversationColumns.DRAFT_SUBJECT_TEXT;
|
||||
static final String DRAFT_PREVIEW_URI = ConversationColumns.DRAFT_PREVIEW_URI;
|
||||
static final String DRAFT_PREVIEW_CONTENT_TYPE =
|
||||
ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE;
|
||||
static final String DRAFT_SNIPPET_TEXT = ConversationColumns.DRAFT_SNIPPET_TEXT;
|
||||
static final String PARTICIPANT_CONTACT_ID = ConversationColumns.PARTICIPANT_CONTACT_ID;
|
||||
static final String PARTICIPANT_LOOKUP_KEY = ConversationColumns.PARTICIPANT_LOOKUP_KEY;
|
||||
static final String OTHER_PARTICIPANT_NORMALIZED_DESTINATION =
|
||||
ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION;
|
||||
static final String CURRENT_SELF_ID = ConversationColumns.CURRENT_SELF_ID;
|
||||
static final String PARTICIPANT_COUNT = ConversationColumns.PARTICIPANT_COUNT;
|
||||
static final String INCLUDE_EMAIL_ADDRESS =
|
||||
ConversationColumns.INCLUDE_EMAIL_ADDRESS;
|
||||
static final String MESSAGE_STATUS = MessageColumns.STATUS;
|
||||
static final String MESSAGE_RAW_TELEPHONY_STATUS = MessageColumns.RAW_TELEPHONY_STATUS;
|
||||
static final String MESSAGE_ID = "message_id";
|
||||
static final String SNIPPET_SENDER_FIRST_NAME = "snippet_sender_first_name";
|
||||
static final String SNIPPET_SENDER_DISPLAY_DESTINATION =
|
||||
"snippet_sender_display_destination";
|
||||
static final String IS_ENTERPRISE = ConversationColumns.IS_ENTERPRISE;
|
||||
}
|
||||
|
||||
public static final String[] PROJECTION = {
|
||||
ConversationListViewColumns._ID,
|
||||
ConversationListViewColumns.NAME,
|
||||
ConversationListViewColumns.ICON,
|
||||
ConversationListViewColumns.SNIPPET_TEXT,
|
||||
ConversationListViewColumns.SORT_TIMESTAMP,
|
||||
ConversationListViewColumns.READ,
|
||||
ConversationListViewColumns.PREVIEW_URI,
|
||||
ConversationListViewColumns.PREVIEW_CONTENT_TYPE,
|
||||
ConversationListViewColumns.PARTICIPANT_CONTACT_ID,
|
||||
ConversationListViewColumns.PARTICIPANT_LOOKUP_KEY,
|
||||
ConversationListViewColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION,
|
||||
ConversationListViewColumns.PARTICIPANT_COUNT,
|
||||
ConversationListViewColumns.CURRENT_SELF_ID,
|
||||
ConversationListViewColumns.INCLUDE_EMAIL_ADDRESS,
|
||||
ConversationListViewColumns.MESSAGE_STATUS,
|
||||
ConversationListViewColumns.SHOW_DRAFT,
|
||||
ConversationListViewColumns.DRAFT_PREVIEW_URI,
|
||||
ConversationListViewColumns.DRAFT_PREVIEW_CONTENT_TYPE,
|
||||
ConversationListViewColumns.DRAFT_SNIPPET_TEXT,
|
||||
ConversationListViewColumns.ARCHIVE_STATUS,
|
||||
ConversationListViewColumns.MESSAGE_ID,
|
||||
ConversationListViewColumns.SUBJECT_TEXT,
|
||||
ConversationListViewColumns.DRAFT_SUBJECT_TEXT,
|
||||
ConversationListViewColumns.MESSAGE_RAW_TELEPHONY_STATUS,
|
||||
ConversationListViewColumns.SNIPPET_SENDER_FIRST_NAME,
|
||||
ConversationListViewColumns.SNIPPET_SENDER_DISPLAY_DESTINATION,
|
||||
ConversationListViewColumns.IS_ENTERPRISE,
|
||||
};
|
||||
|
||||
private static final int INDEX_ID = 0;
|
||||
private static final int INDEX_CONVERSATION_NAME = 1;
|
||||
private static final int INDEX_CONVERSATION_ICON = 2;
|
||||
private static final int INDEX_SNIPPET_TEXT = 3;
|
||||
private static final int INDEX_SORT_TIMESTAMP = 4;
|
||||
private static final int INDEX_READ = 5;
|
||||
private static final int INDEX_PREVIEW_URI = 6;
|
||||
private static final int INDEX_PREVIEW_CONTENT_TYPE = 7;
|
||||
private static final int INDEX_PARTICIPANT_CONTACT_ID = 8;
|
||||
private static final int INDEX_PARTICIPANT_LOOKUP_KEY = 9;
|
||||
private static final int INDEX_OTHER_PARTICIPANT_NORMALIZED_DESTINATION = 10;
|
||||
private static final int INDEX_PARTICIPANT_COUNT = 11;
|
||||
private static final int INDEX_SELF_ID = 12;
|
||||
private static final int INDEX_INCLUDE_EMAIL_ADDRESS = 13;
|
||||
private static final int INDEX_MESSAGE_STATUS = 14;
|
||||
private static final int INDEX_SHOW_DRAFT = 15;
|
||||
private static final int INDEX_DRAFT_PREVIEW_URI = 16;
|
||||
private static final int INDEX_DRAFT_PREVIEW_CONTENT_TYPE = 17;
|
||||
private static final int INDEX_DRAFT_SNIPPET_TEXT = 18;
|
||||
private static final int INDEX_ARCHIVE_STATUS = 19;
|
||||
private static final int INDEX_MESSAGE_ID = 20;
|
||||
private static final int INDEX_SUBJECT_TEXT = 21;
|
||||
private static final int INDEX_DRAFT_SUBJECT_TEXT = 22;
|
||||
private static final int INDEX_MESSAGE_RAW_TELEPHONY_STATUS = 23;
|
||||
private static final int INDEX_SNIPPET_SENDER_FIRST_NAME = 24;
|
||||
private static final int INDEX_SNIPPET_SENDER_DISPLAY_DESTINATION = 25;
|
||||
private static final int INDEX_IS_ENTERPRISE = 26;
|
||||
|
||||
private static final String DIVIDER_TEXT = ", ";
|
||||
|
||||
public static boolean hasAnyEnterpriseContact(
|
||||
final List<ParticipantData> participants) {
|
||||
for (final ParticipantData participant : participants) {
|
||||
if (ContactUtil.isEnterpriseContactId(participant.getContactId())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a conversation from the local DB based on the conversation id.
|
||||
*
|
||||
* @param dbWrapper The database
|
||||
* @param conversationId The conversation Id to read
|
||||
* @return The existing conversation or null
|
||||
*/
|
||||
public static ConversationListItemData getExistingConversation(final DatabaseWrapper dbWrapper,
|
||||
final String conversationId) {
|
||||
ConversationListItemData conversation = null;
|
||||
|
||||
// Look for an existing conversation in the db with this conversation id
|
||||
try (Cursor cursor = dbWrapper.query(getConversationListView(),
|
||||
PROJECTION,
|
||||
ConversationColumns._ID + "=?",
|
||||
new String[]{conversationId},
|
||||
null, null, null)) {
|
||||
// TODO: Should we be able to read a row from just the conversation table?
|
||||
Assert.inRange(cursor.getCount(), 0, 1);
|
||||
if (cursor.moveToFirst()) {
|
||||
conversation = new ConversationListItemData();
|
||||
conversation.bind(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
return conversation;
|
||||
}
|
||||
|
||||
public static String generateConversationName(final List<ParticipantData>
|
||||
participants) {
|
||||
if (participants.size() == 1) {
|
||||
// Prefer full name over first name for 1:1 conversation
|
||||
return participants.get(0).getDisplayName(true);
|
||||
}
|
||||
|
||||
final ArrayList<String> participantNames = new ArrayList<>();
|
||||
for (final ParticipantData participant : participants) {
|
||||
// Prefer first name over full name for group conversation
|
||||
participantNames.add(participant.getDisplayName(false));
|
||||
}
|
||||
|
||||
final Joiner joiner = Joiner.on(DIVIDER_TEXT).skipNulls();
|
||||
return joiner.join(participantNames);
|
||||
}
|
||||
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
/**
|
||||
* Holds data for conversation message bubble which keeps track of whether it's been bound to
|
||||
* a new message.
|
||||
*/
|
||||
public class ConversationMessageBubbleData {
|
||||
private String mMessageId;
|
||||
|
||||
/**
|
||||
* Binds to ConversationMessageData instance.
|
||||
* @return true if we are binding to a different message, false if we are binding to the
|
||||
* same message (e.g. in order to update the status text)
|
||||
*/
|
||||
public boolean bind(final ConversationMessageData data) {
|
||||
final boolean changed = !TextUtils.equals(mMessageId, data.getMessageId());
|
||||
mMessageId = data.getMessageId();
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,910 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
import android.provider.ContactsContract;
|
||||
import android.support.v7.mms.pdu.ContentType;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.BugleGservicesKeys;
|
||||
import com.android.messaging.util.Dates;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class representing a message within a conversation sequence. The message parts
|
||||
* are available via the getParts() method.
|
||||
*
|
||||
* TODO: See if we can delegate to MessageData for the logic that this class duplicates
|
||||
* (e.g. getIsMms).
|
||||
*/
|
||||
public class ConversationMessageData {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
private String mMessageId;
|
||||
private String mConversationId;
|
||||
private String mParticipantId;
|
||||
private int mPartsCount;
|
||||
private List<MessagePartData> mParts;
|
||||
private long mSentTimestamp;
|
||||
private long mReceivedTimestamp;
|
||||
private boolean mSeen;
|
||||
private boolean mRead;
|
||||
private int mProtocol;
|
||||
private int mStatus;
|
||||
private String mSmsMessageUri;
|
||||
private int mSmsPriority;
|
||||
private int mSmsMessageSize;
|
||||
private String mMmsSubject;
|
||||
private long mMmsExpiry;
|
||||
private int mRawTelephonyStatus;
|
||||
private String mSenderFullName;
|
||||
private String mSenderFirstName;
|
||||
private String mSenderDisplayDestination;
|
||||
private String mSenderNormalizedDestination;
|
||||
private String mSenderProfilePhotoUri;
|
||||
private long mSenderContactId;
|
||||
private String mSenderContactLookupKey;
|
||||
private String mSelfParticipantId;
|
||||
|
||||
/** Are we similar enough to the previous/next messages that we can cluster them? */
|
||||
private boolean mCanClusterWithPreviousMessage;
|
||||
private boolean mCanClusterWithNextMessage;
|
||||
|
||||
public ConversationMessageData() {
|
||||
}
|
||||
|
||||
public void bind(final Cursor cursor) {
|
||||
mMessageId = cursor.getString(INDEX_MESSAGE_ID);
|
||||
mConversationId = cursor.getString(INDEX_CONVERSATION_ID);
|
||||
mParticipantId = cursor.getString(INDEX_PARTICIPANT_ID);
|
||||
mPartsCount = cursor.getInt(INDEX_PARTS_COUNT);
|
||||
|
||||
mParts = makeParts(
|
||||
cursor.getString(INDEX_PARTS_IDS),
|
||||
cursor.getString(INDEX_PARTS_CONTENT_TYPES),
|
||||
cursor.getString(INDEX_PARTS_CONTENT_URIS),
|
||||
cursor.getString(INDEX_PARTS_WIDTHS),
|
||||
cursor.getString(INDEX_PARTS_HEIGHTS),
|
||||
cursor.getString(INDEX_PARTS_TEXTS),
|
||||
mPartsCount,
|
||||
mMessageId);
|
||||
|
||||
mSentTimestamp = cursor.getLong(INDEX_SENT_TIMESTAMP);
|
||||
mReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP);
|
||||
mSeen = (cursor.getInt(INDEX_SEEN) != 0);
|
||||
mRead = (cursor.getInt(INDEX_READ) != 0);
|
||||
mProtocol = cursor.getInt(INDEX_PROTOCOL);
|
||||
mStatus = cursor.getInt(INDEX_STATUS);
|
||||
mSmsMessageUri = cursor.getString(INDEX_SMS_MESSAGE_URI);
|
||||
mSmsPriority = cursor.getInt(INDEX_SMS_PRIORITY);
|
||||
mSmsMessageSize = cursor.getInt(INDEX_SMS_MESSAGE_SIZE);
|
||||
mMmsSubject = cursor.getString(INDEX_MMS_SUBJECT);
|
||||
mMmsExpiry = cursor.getLong(INDEX_MMS_EXPIRY);
|
||||
mRawTelephonyStatus = cursor.getInt(INDEX_RAW_TELEPHONY_STATUS);
|
||||
mSenderFullName = cursor.getString(INDEX_SENDER_FULL_NAME);
|
||||
mSenderFirstName = cursor.getString(INDEX_SENDER_FIRST_NAME);
|
||||
mSenderDisplayDestination = cursor.getString(INDEX_SENDER_DISPLAY_DESTINATION);
|
||||
mSenderNormalizedDestination = cursor.getString(INDEX_SENDER_NORMALIZED_DESTINATION);
|
||||
mSenderProfilePhotoUri = cursor.getString(INDEX_SENDER_PROFILE_PHOTO_URI);
|
||||
mSenderContactId = cursor.getLong(INDEX_SENDER_CONTACT_ID);
|
||||
mSenderContactLookupKey = cursor.getString(INDEX_SENDER_CONTACT_LOOKUP_KEY);
|
||||
mSelfParticipantId = cursor.getString(INDEX_SELF_PARTICIPIANT_ID);
|
||||
|
||||
if (!cursor.isFirst() && cursor.moveToPrevious()) {
|
||||
mCanClusterWithPreviousMessage = canClusterWithMessage(cursor);
|
||||
cursor.moveToNext();
|
||||
} else {
|
||||
mCanClusterWithPreviousMessage = false;
|
||||
}
|
||||
if (!cursor.isLast() && cursor.moveToNext()) {
|
||||
mCanClusterWithNextMessage = canClusterWithMessage(cursor);
|
||||
cursor.moveToPrevious();
|
||||
} else {
|
||||
mCanClusterWithNextMessage = false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canClusterWithMessage(final Cursor cursor) {
|
||||
final String otherParticipantId = cursor.getString(INDEX_PARTICIPANT_ID);
|
||||
if (!TextUtils.equals(getParticipantId(), otherParticipantId)) {
|
||||
return false;
|
||||
}
|
||||
final int otherStatus = cursor.getInt(INDEX_STATUS);
|
||||
final boolean otherIsIncoming = (otherStatus >= MessageData.BUGLE_STATUS_FIRST_INCOMING);
|
||||
if (getIsIncoming() != otherIsIncoming) {
|
||||
return false;
|
||||
}
|
||||
final long otherReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP);
|
||||
final long timestampDeltaMillis = Math.abs(mReceivedTimestamp - otherReceivedTimestamp);
|
||||
if (timestampDeltaMillis > DateUtils.MINUTE_IN_MILLIS) {
|
||||
return false;
|
||||
}
|
||||
final String otherSelfId = cursor.getString(INDEX_SELF_PARTICIPIANT_ID);
|
||||
if (!TextUtils.equals(getSelfParticipantId(), otherSelfId)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static final Character QUOTE_CHAR = '\'';
|
||||
private static final char DIVIDER = '|';
|
||||
|
||||
// statics to avoid unnecessary object allocation
|
||||
private static final StringBuilder sUnquoteStringBuilder = new StringBuilder();
|
||||
private static final ArrayList<String> sUnquoteResults = new ArrayList<>();
|
||||
|
||||
// this lock is used to guard access to the above statics
|
||||
private static final Object sUnquoteLock = new Object();
|
||||
|
||||
private static void addResult(final ArrayList<String> results, final StringBuilder value) {
|
||||
if (value.length() > 0) {
|
||||
results.add(value.toString());
|
||||
} else {
|
||||
results.add(EMPTY_STRING);
|
||||
}
|
||||
}
|
||||
|
||||
static String[] splitUnquotedString(final String inputString) {
|
||||
if (TextUtils.isEmpty(inputString)) {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
return inputString.split("\\" + DIVIDER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a group-concated and quoted string and decomposes it into its constituent
|
||||
* parts. A quoted string starts and ends with a single quote. Actual single quotes
|
||||
* within the string are escaped using a second single quote. So, for example, an
|
||||
* input string with 3 constituent parts might look like this:
|
||||
*
|
||||
* 'now is the time'|'I can''t do it'|'foo'
|
||||
*
|
||||
* This would be returned as an array of 3 strings as follows:
|
||||
* now is the time
|
||||
* I can't do it
|
||||
* foo
|
||||
*
|
||||
* This is achieved by walking through the inputString, character by character,
|
||||
* ignoring the outer quotes and the divider and replacing any pair of consecutive
|
||||
* single quotes with a single single quote.
|
||||
*
|
||||
* @return array of constituent strings
|
||||
*/
|
||||
static String[] splitQuotedString(final String inputString) {
|
||||
if (TextUtils.isEmpty(inputString)) {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
// this method can be called from multiple threads but it uses a static
|
||||
// string builder
|
||||
synchronized (sUnquoteLock) {
|
||||
final int length = inputString.length();
|
||||
final ArrayList<String> results = sUnquoteResults;
|
||||
results.clear();
|
||||
|
||||
int characterPos = -1;
|
||||
while (++characterPos < length) {
|
||||
final char mustBeQuote = inputString.charAt(characterPos);
|
||||
Assert.isTrue(QUOTE_CHAR == mustBeQuote);
|
||||
while (++characterPos < length) {
|
||||
final char currentChar = inputString.charAt(characterPos);
|
||||
if (currentChar == QUOTE_CHAR) {
|
||||
final char peekAhead = characterPos < length - 1
|
||||
? inputString.charAt(characterPos + 1) : 0;
|
||||
|
||||
if (peekAhead == QUOTE_CHAR) {
|
||||
characterPos += 1; // skip the second quote
|
||||
} else {
|
||||
addResult(results, sUnquoteStringBuilder);
|
||||
sUnquoteStringBuilder.setLength(0);
|
||||
|
||||
Assert.isTrue((peekAhead == DIVIDER) || (peekAhead == (char) 0));
|
||||
characterPos += 1; // skip the divider
|
||||
break;
|
||||
}
|
||||
}
|
||||
sUnquoteStringBuilder.append(currentChar);
|
||||
}
|
||||
}
|
||||
return results.toArray(new String[results.size()]);
|
||||
}
|
||||
}
|
||||
|
||||
static MessagePartData makePartData(
|
||||
final String partId,
|
||||
final String contentType,
|
||||
final String contentUriString,
|
||||
final String contentWidth,
|
||||
final String contentHeight,
|
||||
final String text,
|
||||
final String messageId) {
|
||||
if (ContentType.isTextType(contentType)) {
|
||||
final MessagePartData textPart = MessagePartData.createTextMessagePart(text);
|
||||
textPart.updatePartId(partId);
|
||||
textPart.updateMessageId(messageId);
|
||||
return textPart;
|
||||
} else {
|
||||
final Uri contentUri = Uri.parse(contentUriString);
|
||||
final int width = Integer.parseInt(contentWidth);
|
||||
final int height = Integer.parseInt(contentHeight);
|
||||
final MessagePartData attachmentPart = MessagePartData.createMediaMessagePart(
|
||||
contentType, contentUri, width, height);
|
||||
attachmentPart.updatePartId(partId);
|
||||
attachmentPart.updateMessageId(messageId);
|
||||
return attachmentPart;
|
||||
}
|
||||
}
|
||||
|
||||
static List<MessagePartData> makeParts(
|
||||
final String rawIds,
|
||||
final String rawContentTypes,
|
||||
final String rawContentUris,
|
||||
final String rawWidths,
|
||||
final String rawHeights,
|
||||
final String rawTexts,
|
||||
final int partsCount,
|
||||
final String messageId) {
|
||||
final List<MessagePartData> parts = new LinkedList<>();
|
||||
if (partsCount == 1) {
|
||||
parts.add(makePartData(
|
||||
rawIds,
|
||||
rawContentTypes,
|
||||
rawContentUris,
|
||||
rawWidths,
|
||||
rawHeights,
|
||||
rawTexts,
|
||||
messageId));
|
||||
} else {
|
||||
unpackMessageParts(
|
||||
parts,
|
||||
splitUnquotedString(rawIds),
|
||||
splitQuotedString(rawContentTypes),
|
||||
splitQuotedString(rawContentUris),
|
||||
splitUnquotedString(rawWidths),
|
||||
splitUnquotedString(rawHeights),
|
||||
splitQuotedString(rawTexts),
|
||||
partsCount,
|
||||
messageId);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
static void unpackMessageParts(
|
||||
final List<MessagePartData> parts,
|
||||
final String[] ids,
|
||||
final String[] contentTypes,
|
||||
final String[] contentUris,
|
||||
final String[] contentWidths,
|
||||
final String[] contentHeights,
|
||||
final String[] texts,
|
||||
final int partsCount,
|
||||
final String messageId) {
|
||||
|
||||
Assert.equals(partsCount, ids.length);
|
||||
Assert.equals(partsCount, contentTypes.length);
|
||||
Assert.equals(partsCount, contentUris.length);
|
||||
Assert.equals(partsCount, contentWidths.length);
|
||||
Assert.equals(partsCount, contentHeights.length);
|
||||
Assert.equals(partsCount, texts.length);
|
||||
|
||||
for (int i = 0; i < partsCount; i++) {
|
||||
parts.add(makePartData(
|
||||
ids[i],
|
||||
contentTypes[i],
|
||||
contentUris[i],
|
||||
contentWidths[i],
|
||||
contentHeights[i],
|
||||
texts[i],
|
||||
messageId));
|
||||
}
|
||||
|
||||
if (parts.size() != partsCount) {
|
||||
LogUtil.wtf(TAG, "Only unpacked " + parts.size() + " parts from message (id="
|
||||
+ messageId + "), expected " + partsCount + " parts");
|
||||
}
|
||||
}
|
||||
|
||||
public final String getMessageId() {
|
||||
return mMessageId;
|
||||
}
|
||||
|
||||
public final String getConversationId() {
|
||||
return mConversationId;
|
||||
}
|
||||
|
||||
public final String getParticipantId() {
|
||||
return mParticipantId;
|
||||
}
|
||||
|
||||
public List<MessagePartData> getParts() {
|
||||
return mParts;
|
||||
}
|
||||
|
||||
public boolean hasText() {
|
||||
for (final MessagePartData part : mParts) {
|
||||
if (part.isText()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a concatenation of all text parts
|
||||
*
|
||||
* @return the text that is a concatenation of all text parts
|
||||
*/
|
||||
public String getText() {
|
||||
// This is optimized for single text part case, which is the majority
|
||||
|
||||
// For single text part, we just return the part without creating the StringBuilder
|
||||
String firstTextPart = null;
|
||||
boolean foundText = false;
|
||||
// For multiple text parts, we need the StringBuilder and the separator for concatenation
|
||||
StringBuilder sb = null;
|
||||
String separator = null;
|
||||
for (final MessagePartData part : mParts) {
|
||||
if (part.isText()) {
|
||||
if (!foundText) {
|
||||
// First text part
|
||||
firstTextPart = part.getText();
|
||||
foundText = true;
|
||||
} else {
|
||||
// Second and beyond
|
||||
if (sb == null) {
|
||||
// Need the StringBuilder and the separator starting from 2nd text part
|
||||
sb = new StringBuilder();
|
||||
if (!TextUtils.isEmpty(firstTextPart)) {
|
||||
sb.append(firstTextPart);
|
||||
}
|
||||
separator = BugleGservicesKeys.MMS_TEXT_CONCAT_SEPARATOR_DEFAULT;
|
||||
}
|
||||
final String partText = part.getText();
|
||||
if (!TextUtils.isEmpty(partText)) {
|
||||
if (!TextUtils.isEmpty(separator) && sb.length() > 0) {
|
||||
sb.append(separator);
|
||||
}
|
||||
sb.append(partText);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sb == null) {
|
||||
// Only one text part
|
||||
return firstTextPart;
|
||||
} else {
|
||||
// More than one
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasAttachments() {
|
||||
for (final MessagePartData part : mParts) {
|
||||
if (part.isAttachment()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<MessagePartData> getAttachments() {
|
||||
return getAttachments(null);
|
||||
}
|
||||
|
||||
public List<MessagePartData> getAttachments(final Predicate<MessagePartData> filter) {
|
||||
if (mParts.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
final List<MessagePartData> attachmentParts = new LinkedList<>();
|
||||
for (final MessagePartData part : mParts) {
|
||||
if (part.isAttachment()) {
|
||||
if (filter == null || filter.apply(part)) {
|
||||
attachmentParts.add(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
return attachmentParts;
|
||||
}
|
||||
|
||||
public final long getSentTimeStamp() {
|
||||
return mSentTimestamp;
|
||||
}
|
||||
|
||||
public final long getReceivedTimeStamp() {
|
||||
return mReceivedTimestamp;
|
||||
}
|
||||
|
||||
public final String getFormattedReceivedTimeStamp() {
|
||||
return Dates.getMessageTimeString(mReceivedTimestamp).toString();
|
||||
}
|
||||
|
||||
public final boolean getIsSeen() {
|
||||
return mSeen;
|
||||
}
|
||||
|
||||
public final boolean getIsRead() {
|
||||
return mRead;
|
||||
}
|
||||
|
||||
public final boolean getIsMms() {
|
||||
return (mProtocol == MessageData.PROTOCOL_MMS ||
|
||||
mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION);
|
||||
}
|
||||
|
||||
public final boolean getIsMmsNotification() {
|
||||
return (mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION);
|
||||
}
|
||||
|
||||
public final boolean getIsSms() {
|
||||
return mProtocol == (MessageData.PROTOCOL_SMS);
|
||||
}
|
||||
|
||||
public final int getStatus() {
|
||||
return mStatus;
|
||||
}
|
||||
|
||||
public final String getSmsMessageUri() {
|
||||
return mSmsMessageUri;
|
||||
}
|
||||
|
||||
public final int getSmsPriority() {
|
||||
return mSmsPriority;
|
||||
}
|
||||
|
||||
public final int getSmsMessageSize() {
|
||||
return mSmsMessageSize;
|
||||
}
|
||||
|
||||
public final String getMmsSubject() {
|
||||
return mMmsSubject;
|
||||
}
|
||||
|
||||
public final long getMmsExpiry() {
|
||||
return mMmsExpiry;
|
||||
}
|
||||
|
||||
public final int getRawTelephonyStatus() {
|
||||
return mRawTelephonyStatus;
|
||||
}
|
||||
|
||||
public final String getSelfParticipantId() {
|
||||
return mSelfParticipantId;
|
||||
}
|
||||
|
||||
public boolean getIsIncoming() {
|
||||
return (mStatus >= MessageData.BUGLE_STATUS_FIRST_INCOMING);
|
||||
}
|
||||
|
||||
public boolean hasIncomingErrorStatus() {
|
||||
return (mStatus == MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE ||
|
||||
mStatus == MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED);
|
||||
}
|
||||
|
||||
public boolean getIsSendComplete() {
|
||||
return (mStatus == MessageData.BUGLE_STATUS_OUTGOING_COMPLETE
|
||||
|| mStatus == MessageData.BUGLE_STATUS_OUTGOING_DELIVERED);
|
||||
}
|
||||
|
||||
public String getSenderFullName() {
|
||||
return mSenderFullName;
|
||||
}
|
||||
|
||||
public String getSenderFirstName() {
|
||||
return mSenderFirstName;
|
||||
}
|
||||
|
||||
public String getSenderDisplayDestination() {
|
||||
return mSenderDisplayDestination;
|
||||
}
|
||||
|
||||
public String getSenderNormalizedDestination() {
|
||||
return mSenderNormalizedDestination;
|
||||
}
|
||||
|
||||
public Uri getSenderProfilePhotoUri() {
|
||||
return mSenderProfilePhotoUri == null ? null : Uri.parse(mSenderProfilePhotoUri);
|
||||
}
|
||||
|
||||
public long getSenderContactId() {
|
||||
return mSenderContactId;
|
||||
}
|
||||
|
||||
public String getSenderDisplayName() {
|
||||
if (!TextUtils.isEmpty(mSenderFullName)) {
|
||||
return mSenderFullName;
|
||||
}
|
||||
if (!TextUtils.isEmpty(mSenderFirstName)) {
|
||||
return mSenderFirstName;
|
||||
}
|
||||
return mSenderDisplayDestination;
|
||||
}
|
||||
|
||||
public String getSenderContactLookupKey() {
|
||||
return mSenderContactLookupKey;
|
||||
}
|
||||
|
||||
public boolean getShowDownloadMessage() {
|
||||
return MessageData.getShowDownloadMessage(mStatus);
|
||||
}
|
||||
|
||||
public boolean getShowResendMessage() {
|
||||
return MessageData.getShowResendMessage(mStatus);
|
||||
}
|
||||
|
||||
public boolean getCanForwardMessage() {
|
||||
// Even for outgoing messages, we only allow forwarding if the message has finished sending
|
||||
// as media often has issues when send isn't complete
|
||||
return (mStatus == MessageData.BUGLE_STATUS_OUTGOING_COMPLETE
|
||||
|| mStatus == MessageData.BUGLE_STATUS_OUTGOING_DELIVERED
|
||||
|| mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE);
|
||||
}
|
||||
|
||||
public boolean getCanCopyMessageToClipboard() {
|
||||
return (hasText() &&
|
||||
(!getIsIncoming() || mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE));
|
||||
}
|
||||
|
||||
public boolean getOneClickResendMessage() {
|
||||
return MessageData.getOneClickResendMessage(mStatus, mRawTelephonyStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sender's lookup uri.
|
||||
* This method doesn't support corp contacts.
|
||||
*
|
||||
* @return Lookup uri of sender's contact
|
||||
*/
|
||||
public Uri getSenderContactLookupUri() {
|
||||
if (mSenderContactId > ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED
|
||||
&& !TextUtils.isEmpty(mSenderContactLookupKey)) {
|
||||
return ContactsContract.Contacts.getLookupUri(mSenderContactId,
|
||||
mSenderContactLookupKey);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean getCanClusterWithPreviousMessage() {
|
||||
return mCanClusterWithPreviousMessage;
|
||||
}
|
||||
|
||||
public boolean getCanClusterWithNextMessage() {
|
||||
return mCanClusterWithNextMessage;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return MessageData.toString(mMessageId, mParts);
|
||||
}
|
||||
|
||||
// Data definitions
|
||||
|
||||
public static String getConversationMessagesQuerySql() {
|
||||
return CONVERSATION_MESSAGES_QUERY_SQL
|
||||
+ " AND "
|
||||
// Inject the conversation id
|
||||
+ DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)"
|
||||
+ CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY;
|
||||
}
|
||||
|
||||
static String getConversationMessageIdsQuerySql() {
|
||||
return CONVERSATION_MESSAGES_IDS_QUERY_SQL
|
||||
+ " AND "
|
||||
// Inject the conversation id
|
||||
+ DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)"
|
||||
+ CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY;
|
||||
}
|
||||
|
||||
public static String getNotificationQuerySql() {
|
||||
return CONVERSATION_MESSAGES_QUERY_SQL
|
||||
+ " AND "
|
||||
+ "(" + DatabaseHelper.MessageColumns.STATUS + " in ("
|
||||
+ MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", "
|
||||
+ MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")"
|
||||
+ " AND "
|
||||
+ DatabaseHelper.MessageColumns.SEEN + " = 0)"
|
||||
+ ")"
|
||||
+ NOTIFICATION_QUERY_SQL_GROUP_BY;
|
||||
}
|
||||
|
||||
public static String getWearableQuerySql() {
|
||||
return CONVERSATION_MESSAGES_QUERY_SQL
|
||||
+ " AND "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?"
|
||||
+ " AND "
|
||||
+ DatabaseHelper.MessageColumns.STATUS + " IN ("
|
||||
+ MessageData.BUGLE_STATUS_OUTGOING_DELIVERED + ", "
|
||||
+ MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + ", "
|
||||
+ MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND + ", "
|
||||
+ MessageData.BUGLE_STATUS_OUTGOING_SENDING + ", "
|
||||
+ MessageData.BUGLE_STATUS_OUTGOING_RESENDING + ", "
|
||||
+ MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY + ", "
|
||||
+ MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", "
|
||||
+ MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")"
|
||||
+ ")"
|
||||
+ NOTIFICATION_QUERY_SQL_GROUP_BY;
|
||||
}
|
||||
|
||||
/*
|
||||
* Generate a sqlite snippet to call the quote function on the columnName argument.
|
||||
* The columnName doesn't strictly have to be a column name (e.g. it could be an
|
||||
* expression).
|
||||
*/
|
||||
private static String quote(final String columnName) {
|
||||
return "quote(" + columnName + ")";
|
||||
}
|
||||
|
||||
private static String makeGroupConcatString(final String column) {
|
||||
return "group_concat(" + column + ", '" + DIVIDER + "')";
|
||||
}
|
||||
|
||||
private static String makeIfNullString(final String column) {
|
||||
return "ifnull(" + column + "," + "''" + ")";
|
||||
}
|
||||
|
||||
private static String makePartsTableColumnString(final String column) {
|
||||
return DatabaseHelper.PARTS_TABLE + '.' + column;
|
||||
}
|
||||
|
||||
private static String makeCaseWhenString(final String column,
|
||||
final boolean quote,
|
||||
final String asColumn) {
|
||||
final String fullColumn = makeIfNullString(makePartsTableColumnString(column));
|
||||
final String groupConcatTerm = quote
|
||||
? makeGroupConcatString(quote(fullColumn))
|
||||
: makeGroupConcatString(fullColumn);
|
||||
return "CASE WHEN (" + CONVERSATION_MESSAGE_VIEW_PARTS_COUNT + ">1) THEN " + groupConcatTerm
|
||||
+ " ELSE " + makePartsTableColumnString(column) + " END AS " + asColumn;
|
||||
}
|
||||
|
||||
private static final String CONVERSATION_MESSAGE_VIEW_PARTS_COUNT =
|
||||
"count(" + DatabaseHelper.PARTS_TABLE + '.' + PartColumns._ID + ")";
|
||||
|
||||
private static final String EMPTY_STRING = "";
|
||||
|
||||
private static final String CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL =
|
||||
DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID
|
||||
+ " as " + ConversationMessageViewColumns._ID + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.CONVERSATION_ID
|
||||
+ " as " + ConversationMessageViewColumns.CONVERSATION_ID + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID
|
||||
+ " as " + ConversationMessageViewColumns.PARTICIPANT_ID + ", "
|
||||
|
||||
+ makeCaseWhenString(PartColumns._ID, false,
|
||||
ConversationMessageViewColumns.PARTS_IDS) + ", "
|
||||
+ makeCaseWhenString(PartColumns.CONTENT_TYPE, true,
|
||||
ConversationMessageViewColumns.PARTS_CONTENT_TYPES) + ", "
|
||||
+ makeCaseWhenString(PartColumns.CONTENT_URI, true,
|
||||
ConversationMessageViewColumns.PARTS_CONTENT_URIS) + ", "
|
||||
+ makeCaseWhenString(PartColumns.WIDTH, false,
|
||||
ConversationMessageViewColumns.PARTS_WIDTHS) + ", "
|
||||
+ makeCaseWhenString(PartColumns.HEIGHT, false,
|
||||
ConversationMessageViewColumns.PARTS_HEIGHTS) + ", "
|
||||
+ makeCaseWhenString(PartColumns.TEXT, true,
|
||||
ConversationMessageViewColumns.PARTS_TEXTS) + ", "
|
||||
|
||||
+ CONVERSATION_MESSAGE_VIEW_PARTS_COUNT
|
||||
+ " as " + ConversationMessageViewColumns.PARTS_COUNT + ", "
|
||||
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENT_TIMESTAMP
|
||||
+ " as " + ConversationMessageViewColumns.SENT_TIMESTAMP + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP
|
||||
+ " as " + ConversationMessageViewColumns.RECEIVED_TIMESTAMP + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SEEN
|
||||
+ " as " + ConversationMessageViewColumns.SEEN + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.READ
|
||||
+ " as " + ConversationMessageViewColumns.READ + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.PROTOCOL
|
||||
+ " as " + ConversationMessageViewColumns.PROTOCOL + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.STATUS
|
||||
+ " as " + ConversationMessageViewColumns.STATUS + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_URI
|
||||
+ " as " + ConversationMessageViewColumns.SMS_MESSAGE_URI + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_PRIORITY
|
||||
+ " as " + ConversationMessageViewColumns.SMS_PRIORITY + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_SIZE
|
||||
+ " as " + ConversationMessageViewColumns.SMS_MESSAGE_SIZE + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_SUBJECT
|
||||
+ " as " + ConversationMessageViewColumns.MMS_SUBJECT + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_EXPIRY
|
||||
+ " as " + ConversationMessageViewColumns.MMS_EXPIRY + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RAW_TELEPHONY_STATUS
|
||||
+ " as " + ConversationMessageViewColumns.RAW_TELEPHONY_STATUS + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SELF_PARTICIPANT_ID
|
||||
+ " as " + ConversationMessageViewColumns.SELF_PARTICIPANT_ID + ", "
|
||||
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FULL_NAME
|
||||
+ " as " + ConversationMessageViewColumns.SENDER_FULL_NAME + ", "
|
||||
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FIRST_NAME
|
||||
+ " as " + ConversationMessageViewColumns.SENDER_FIRST_NAME + ", "
|
||||
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.DISPLAY_DESTINATION
|
||||
+ " as " + ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION + ", "
|
||||
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.NORMALIZED_DESTINATION
|
||||
+ " as " + ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION + ", "
|
||||
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.PROFILE_PHOTO_URI
|
||||
+ " as " + ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI + ", "
|
||||
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.CONTACT_ID
|
||||
+ " as " + ConversationMessageViewColumns.SENDER_CONTACT_ID + ", "
|
||||
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.LOOKUP_KEY
|
||||
+ " as " + ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY + " ";
|
||||
|
||||
private static final String CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL =
|
||||
" FROM " + DatabaseHelper.MESSAGES_TABLE
|
||||
+ " LEFT JOIN " + DatabaseHelper.PARTS_TABLE
|
||||
+ " ON (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns._ID
|
||||
+ "=" + DatabaseHelper.PARTS_TABLE + "." + PartColumns.MESSAGE_ID + ") "
|
||||
+ " LEFT JOIN " + DatabaseHelper.PARTICIPANTS_TABLE
|
||||
+ " ON (" + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID
|
||||
+ '=' + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns._ID + ")"
|
||||
// Exclude draft messages from main view
|
||||
+ " WHERE (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.STATUS
|
||||
+ " <> " + MessageData.BUGLE_STATUS_OUTGOING_DRAFT;
|
||||
|
||||
// This query is mostly static, except for the injection of conversation id. This is for
|
||||
// performance reasons, to ensure that the query uses indices and does not trigger full scans
|
||||
// of the messages table. See b/17160946 for more details.
|
||||
private static final String CONVERSATION_MESSAGES_QUERY_SQL = "SELECT "
|
||||
+ CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL
|
||||
+ CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL;
|
||||
|
||||
private static final String CONVERSATION_MESSAGE_IDS_PROJECTION_SQL =
|
||||
DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID
|
||||
+ " as " + ConversationMessageViewColumns._ID + " ";
|
||||
|
||||
private static final String CONVERSATION_MESSAGES_IDS_QUERY_SQL = "SELECT "
|
||||
+ CONVERSATION_MESSAGE_IDS_PROJECTION_SQL
|
||||
+ CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL;
|
||||
|
||||
// Note that we sort DESC and ConversationData reverses the cursor. This is a performance
|
||||
// issue (improvement) for large cursors.
|
||||
private static final String CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY =
|
||||
" GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID
|
||||
+ " ORDER BY "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC";
|
||||
|
||||
private static final String NOTIFICATION_QUERY_SQL_GROUP_BY =
|
||||
" GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID
|
||||
+ " ORDER BY "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC";
|
||||
|
||||
interface ConversationMessageViewColumns extends BaseColumns {
|
||||
String _ID = MessageColumns._ID;
|
||||
String CONVERSATION_ID = MessageColumns.CONVERSATION_ID;
|
||||
String PARTICIPANT_ID = MessageColumns.SENDER_PARTICIPANT_ID;
|
||||
String PARTS_COUNT = "parts_count";
|
||||
String SENT_TIMESTAMP = MessageColumns.SENT_TIMESTAMP;
|
||||
String RECEIVED_TIMESTAMP = MessageColumns.RECEIVED_TIMESTAMP;
|
||||
String SEEN = MessageColumns.SEEN;
|
||||
String READ = MessageColumns.READ;
|
||||
String PROTOCOL = MessageColumns.PROTOCOL;
|
||||
String STATUS = MessageColumns.STATUS;
|
||||
String SMS_MESSAGE_URI = MessageColumns.SMS_MESSAGE_URI;
|
||||
String SMS_PRIORITY = MessageColumns.SMS_PRIORITY;
|
||||
String SMS_MESSAGE_SIZE = MessageColumns.SMS_MESSAGE_SIZE;
|
||||
String MMS_SUBJECT = MessageColumns.MMS_SUBJECT;
|
||||
String MMS_EXPIRY = MessageColumns.MMS_EXPIRY;
|
||||
String RAW_TELEPHONY_STATUS = MessageColumns.RAW_TELEPHONY_STATUS;
|
||||
String SELF_PARTICIPANT_ID = MessageColumns.SELF_PARTICIPANT_ID;
|
||||
String SENDER_FULL_NAME = ParticipantColumns.FULL_NAME;
|
||||
String SENDER_FIRST_NAME = ParticipantColumns.FIRST_NAME;
|
||||
String SENDER_DISPLAY_DESTINATION = ParticipantColumns.DISPLAY_DESTINATION;
|
||||
String SENDER_NORMALIZED_DESTINATION =
|
||||
ParticipantColumns.NORMALIZED_DESTINATION;
|
||||
String SENDER_PROFILE_PHOTO_URI = ParticipantColumns.PROFILE_PHOTO_URI;
|
||||
String SENDER_CONTACT_ID = ParticipantColumns.CONTACT_ID;
|
||||
String SENDER_CONTACT_LOOKUP_KEY = ParticipantColumns.LOOKUP_KEY;
|
||||
String PARTS_IDS = "parts_ids";
|
||||
String PARTS_CONTENT_TYPES = "parts_content_types";
|
||||
String PARTS_CONTENT_URIS = "parts_content_uris";
|
||||
String PARTS_WIDTHS = "parts_widths";
|
||||
String PARTS_HEIGHTS = "parts_heights";
|
||||
String PARTS_TEXTS = "parts_texts";
|
||||
}
|
||||
|
||||
private static int sIndexIncrementer = 0;
|
||||
|
||||
private static final int INDEX_MESSAGE_ID = sIndexIncrementer++;
|
||||
private static final int INDEX_CONVERSATION_ID = sIndexIncrementer++;
|
||||
private static final int INDEX_PARTICIPANT_ID = sIndexIncrementer++;
|
||||
|
||||
private static final int INDEX_PARTS_IDS = sIndexIncrementer++;
|
||||
private static final int INDEX_PARTS_CONTENT_TYPES = sIndexIncrementer++;
|
||||
private static final int INDEX_PARTS_CONTENT_URIS = sIndexIncrementer++;
|
||||
private static final int INDEX_PARTS_WIDTHS = sIndexIncrementer++;
|
||||
private static final int INDEX_PARTS_HEIGHTS = sIndexIncrementer++;
|
||||
private static final int INDEX_PARTS_TEXTS = sIndexIncrementer++;
|
||||
|
||||
private static final int INDEX_PARTS_COUNT = sIndexIncrementer++;
|
||||
|
||||
private static final int INDEX_SENT_TIMESTAMP = sIndexIncrementer++;
|
||||
private static final int INDEX_RECEIVED_TIMESTAMP = sIndexIncrementer++;
|
||||
private static final int INDEX_SEEN = sIndexIncrementer++;
|
||||
private static final int INDEX_READ = sIndexIncrementer++;
|
||||
private static final int INDEX_PROTOCOL = sIndexIncrementer++;
|
||||
private static final int INDEX_STATUS = sIndexIncrementer++;
|
||||
private static final int INDEX_SMS_MESSAGE_URI = sIndexIncrementer++;
|
||||
private static final int INDEX_SMS_PRIORITY = sIndexIncrementer++;
|
||||
private static final int INDEX_SMS_MESSAGE_SIZE = sIndexIncrementer++;
|
||||
private static final int INDEX_MMS_SUBJECT = sIndexIncrementer++;
|
||||
private static final int INDEX_MMS_EXPIRY = sIndexIncrementer++;
|
||||
private static final int INDEX_RAW_TELEPHONY_STATUS = sIndexIncrementer++;
|
||||
private static final int INDEX_SELF_PARTICIPIANT_ID = sIndexIncrementer++;
|
||||
private static final int INDEX_SENDER_FULL_NAME = sIndexIncrementer++;
|
||||
private static final int INDEX_SENDER_FIRST_NAME = sIndexIncrementer++;
|
||||
private static final int INDEX_SENDER_DISPLAY_DESTINATION = sIndexIncrementer++;
|
||||
private static final int INDEX_SENDER_NORMALIZED_DESTINATION = sIndexIncrementer++;
|
||||
private static final int INDEX_SENDER_PROFILE_PHOTO_URI = sIndexIncrementer++;
|
||||
private static final int INDEX_SENDER_CONTACT_ID = sIndexIncrementer++;
|
||||
private static final int INDEX_SENDER_CONTACT_LOOKUP_KEY = sIndexIncrementer++;
|
||||
|
||||
|
||||
private static final String[] sProjection = {
|
||||
ConversationMessageViewColumns._ID,
|
||||
ConversationMessageViewColumns.CONVERSATION_ID,
|
||||
ConversationMessageViewColumns.PARTICIPANT_ID,
|
||||
|
||||
ConversationMessageViewColumns.PARTS_IDS,
|
||||
ConversationMessageViewColumns.PARTS_CONTENT_TYPES,
|
||||
ConversationMessageViewColumns.PARTS_CONTENT_URIS,
|
||||
ConversationMessageViewColumns.PARTS_WIDTHS,
|
||||
ConversationMessageViewColumns.PARTS_HEIGHTS,
|
||||
ConversationMessageViewColumns.PARTS_TEXTS,
|
||||
|
||||
ConversationMessageViewColumns.PARTS_COUNT,
|
||||
ConversationMessageViewColumns.SENT_TIMESTAMP,
|
||||
ConversationMessageViewColumns.RECEIVED_TIMESTAMP,
|
||||
ConversationMessageViewColumns.SEEN,
|
||||
ConversationMessageViewColumns.READ,
|
||||
ConversationMessageViewColumns.PROTOCOL,
|
||||
ConversationMessageViewColumns.STATUS,
|
||||
ConversationMessageViewColumns.SMS_MESSAGE_URI,
|
||||
ConversationMessageViewColumns.SMS_PRIORITY,
|
||||
ConversationMessageViewColumns.SMS_MESSAGE_SIZE,
|
||||
ConversationMessageViewColumns.MMS_SUBJECT,
|
||||
ConversationMessageViewColumns.MMS_EXPIRY,
|
||||
ConversationMessageViewColumns.RAW_TELEPHONY_STATUS,
|
||||
ConversationMessageViewColumns.SELF_PARTICIPANT_ID,
|
||||
ConversationMessageViewColumns.SENDER_FULL_NAME,
|
||||
ConversationMessageViewColumns.SENDER_FIRST_NAME,
|
||||
ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION,
|
||||
ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION,
|
||||
ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI,
|
||||
ConversationMessageViewColumns.SENDER_CONTACT_ID,
|
||||
ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY,
|
||||
};
|
||||
|
||||
public static String[] getProjection() {
|
||||
return sProjection;
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.collection.SimpleArrayMap;
|
||||
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* A class that contains the list of all participants potentially involved in a conversation.
|
||||
* Includes both the participant records for each participant referenced in conversation
|
||||
* participants table (i.e. "other" phone numbers) plus all participants representing self
|
||||
* (i.e. one per sim recorded in the subscription manager db).
|
||||
*/
|
||||
public class ConversationParticipantsData implements Iterable<ParticipantData> {
|
||||
// A map from a participant id to a participant
|
||||
private final SimpleArrayMap<String, ParticipantData> mConversationParticipantsMap;
|
||||
private int mParticipantCountExcludingSelf = 0;
|
||||
|
||||
public ConversationParticipantsData() {
|
||||
mConversationParticipantsMap = new SimpleArrayMap<>();
|
||||
}
|
||||
|
||||
public void bind(final Cursor cursor) {
|
||||
mConversationParticipantsMap.clear();
|
||||
mParticipantCountExcludingSelf = 0;
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
final ParticipantData newParticipant = ParticipantData.getFromCursor(cursor);
|
||||
if (!newParticipant.isSelf()) {
|
||||
mParticipantCountExcludingSelf++;
|
||||
}
|
||||
mConversationParticipantsMap.put(newParticipant.getId(), newParticipant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<ParticipantData> getParticipantListExcludingSelf() {
|
||||
final ArrayList<ParticipantData> retList =
|
||||
new ArrayList<>(mConversationParticipantsMap.size());
|
||||
for (int i = 0; i < mConversationParticipantsMap.size(); i++) {
|
||||
final ParticipantData participant = mConversationParticipantsMap.valueAt(i);
|
||||
if (!participant.isSelf()) {
|
||||
retList.add(participant);
|
||||
}
|
||||
}
|
||||
return retList;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a 1:1 conversation return the other (not self) participant
|
||||
*/
|
||||
public ParticipantData getOtherParticipant() {
|
||||
if (mParticipantCountExcludingSelf == 1) {
|
||||
for (int i = 0; i < mConversationParticipantsMap.size(); i++) {
|
||||
final ParticipantData participant = mConversationParticipantsMap.valueAt(i);
|
||||
if (!participant.isSelf()) {
|
||||
return participant;
|
||||
}
|
||||
}
|
||||
Assert.fail("Could not find other participant");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getNumberOfParticipantsExcludingSelf() {
|
||||
return mParticipantCountExcludingSelf;
|
||||
}
|
||||
|
||||
public boolean isLoaded() {
|
||||
return !mConversationParticipantsMap.isEmpty();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Iterator<ParticipantData> iterator() {
|
||||
return new Iterator<>() {
|
||||
private int mCurrentIndex = -1;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return mCurrentIndex < mConversationParticipantsMap.size() - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParticipantData next() {
|
||||
mCurrentIndex++;
|
||||
if (mCurrentIndex >= mConversationParticipantsMap.size()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
return mConversationParticipantsMap.valueAt(mCurrentIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,885 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.datamodel.MessageTextStats;
|
||||
import com.android.messaging.datamodel.action.ReadDraftDataAction;
|
||||
import com.android.messaging.datamodel.action.ReadDraftDataAction.ReadDraftDataActionListener;
|
||||
import com.android.messaging.datamodel.action.ReadDraftDataAction.ReadDraftDataActionMonitor;
|
||||
import com.android.messaging.datamodel.action.WriteDraftMessageAction;
|
||||
import com.android.messaging.datamodel.binding.BindableData;
|
||||
import com.android.messaging.datamodel.binding.Binding;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.sms.MmsConfig;
|
||||
import com.android.messaging.sms.MmsSmsUtils;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
|
||||
import com.android.messaging.util.Assert.RunsOnMainThread;
|
||||
import com.android.messaging.util.BugleGservicesKeys;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class DraftMessageData extends BindableData implements ReadDraftDataActionListener {
|
||||
|
||||
/**
|
||||
* Interface for DraftMessageData listeners
|
||||
*/
|
||||
public interface DraftMessageDataListener {
|
||||
@RunsOnMainThread
|
||||
void onDraftChanged(DraftMessageData data, int changeFlags);
|
||||
|
||||
@RunsOnMainThread
|
||||
void onDraftAttachmentLimitReached(DraftMessageData data);
|
||||
|
||||
@RunsOnMainThread
|
||||
void onDraftAttachmentLoadFailed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for providing subscription-related data to DraftMessageData
|
||||
*/
|
||||
public interface DraftMessageSubscriptionDataProvider {
|
||||
int getConversationSelfSubId();
|
||||
}
|
||||
|
||||
// Flags sent to onDraftChanged to help the receiver limit the amount of work done
|
||||
public static final int ATTACHMENTS_CHANGED = 0x0001;
|
||||
public static final int MESSAGE_TEXT_CHANGED = 0x0002;
|
||||
public static final int MESSAGE_SUBJECT_CHANGED = 0x0004;
|
||||
// Whether the self participant data has been loaded
|
||||
public static final int SELF_CHANGED = 0x0008;
|
||||
public static final int ALL_CHANGED = 0x00FF;
|
||||
// ALL_CHANGED intentionally doesn't include WIDGET_CHANGED. ConversationFragment needs to
|
||||
// be notified if the draft it is looking at is changed externally (by a desktop widget) so it
|
||||
// can reload the draft.
|
||||
public static final int WIDGET_CHANGED = 0x0100;
|
||||
|
||||
private final String mConversationId;
|
||||
private ReadDraftDataActionMonitor mMonitor;
|
||||
private final DraftMessageDataEventDispatcher mListeners;
|
||||
private DraftMessageSubscriptionDataProvider mSubscriptionDataProvider;
|
||||
|
||||
private boolean mIncludeEmailAddress;
|
||||
private boolean mIsGroupConversation;
|
||||
private String mMessageText;
|
||||
private String mMessageSubject;
|
||||
private String mSelfId;
|
||||
private final MessageTextStats mMessageTextStats;
|
||||
private boolean mSending;
|
||||
|
||||
/** Keeps track of completed attachments in the message draft. This data is persisted to db */
|
||||
private final List<MessagePartData> mAttachments;
|
||||
|
||||
/** A read-only wrapper on mAttachments surfaced to the UI layer for rendering */
|
||||
private final List<MessagePartData> mReadOnlyAttachments;
|
||||
|
||||
/** Keeps track of pending attachments that are being loaded. The pending attachments are
|
||||
* transient, because they are not persisted to the database and are dropped once we go
|
||||
* to the background (after the UI calls saveToStorage) */
|
||||
private final List<PendingAttachmentData> mPendingAttachments;
|
||||
|
||||
/** A read-only wrapper on mPendingAttachments surfaced to the UI layer for rendering */
|
||||
private final List<PendingAttachmentData> mReadOnlyPendingAttachments;
|
||||
|
||||
/** Is the current draft a cached copy of what's been saved to the database. If so, we
|
||||
* may skip loading from database if we are still bound */
|
||||
private boolean mIsDraftCachedCopy;
|
||||
|
||||
/** Whether we are currently asynchronously validating the draft before sending. */
|
||||
private CheckDraftForSendTask mCheckDraftForSendTask;
|
||||
|
||||
public DraftMessageData(final String conversationId) {
|
||||
mConversationId = conversationId;
|
||||
mAttachments = new ArrayList<>();
|
||||
mReadOnlyAttachments = Collections.unmodifiableList(mAttachments);
|
||||
mPendingAttachments = new ArrayList<>();
|
||||
mReadOnlyPendingAttachments = Collections.unmodifiableList(mPendingAttachments);
|
||||
mListeners = new DraftMessageDataEventDispatcher();
|
||||
mMessageTextStats = new MessageTextStats();
|
||||
}
|
||||
|
||||
public void addListener(final DraftMessageDataListener listener) {
|
||||
mListeners.add(listener);
|
||||
}
|
||||
|
||||
public void setSubscriptionDataProvider(final DraftMessageSubscriptionDataProvider provider) {
|
||||
mSubscriptionDataProvider = provider;
|
||||
}
|
||||
|
||||
public void updateFromMessageData(final MessageData message, final String bindingId) {
|
||||
// New attachments have arrived - only update if the user hasn't already edited
|
||||
Assert.notNull(bindingId);
|
||||
// The draft is now synced with actual MessageData and no longer a cached copy.
|
||||
mIsDraftCachedCopy = false;
|
||||
// Do not use the loaded draft if the user began composing a message before the draft loaded
|
||||
// During config changes (orientation), the text fields preserve their data, so allow them
|
||||
// to be the same and still consider the draft unchanged by the user
|
||||
if (isDraftEmpty() || (TextUtils.equals(mMessageText, message.getMessageText()) &&
|
||||
TextUtils.equals(mMessageSubject, message.getMmsSubject()) &&
|
||||
mAttachments.isEmpty())) {
|
||||
// No need to clear as just checked it was empty or a subset
|
||||
setMessageText(message.getMessageText(), false /* notify */);
|
||||
setMessageSubject(message.getMmsSubject(), false /* notify */);
|
||||
for (final MessagePartData part : message.getParts()) {
|
||||
if (part.isAttachment() && getAttachmentCount() >= getAttachmentLimit()) {
|
||||
dispatchAttachmentLimitReached();
|
||||
break;
|
||||
}
|
||||
|
||||
if (part instanceof PendingAttachmentData) {
|
||||
// This is a pending attachment data from share intent (e.g. an shared image
|
||||
// that we need to persist locally).
|
||||
final PendingAttachmentData data = (PendingAttachmentData) part;
|
||||
Assert.equals(PendingAttachmentData.STATE_PENDING, data.getCurrentState());
|
||||
addOnePendingAttachmentNoNotify(data, bindingId);
|
||||
} else if (part.isAttachment()) {
|
||||
addOneAttachmentNoNotify(part);
|
||||
}
|
||||
}
|
||||
dispatchChanged(ALL_CHANGED);
|
||||
} else {
|
||||
// The user has started a new message so we throw out the draft message data if there
|
||||
// is one but we also loaded the self metadata and need to let our listeners know.
|
||||
dispatchChanged(SELF_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a MessageData object containing a copy of all the parts in this DraftMessageData.
|
||||
*
|
||||
* @param clearLocalCopy whether we should clear out the in-memory copy of the parts. If we
|
||||
* are simply pausing/resuming and not sending the message, then we can keep
|
||||
* @return the MessageData for the draft, null if self id is not set
|
||||
*/
|
||||
public MessageData createMessageWithCurrentAttachments(final boolean clearLocalCopy) {
|
||||
MessageData message;
|
||||
if (getIsMms()) {
|
||||
message = MessageData.createDraftMmsMessage(mConversationId, mSelfId,
|
||||
mMessageText, mMessageSubject);
|
||||
for (final MessagePartData attachment : mAttachments) {
|
||||
message.addPart(attachment);
|
||||
}
|
||||
} else {
|
||||
message = MessageData.createDraftSmsMessage(mConversationId, mSelfId,
|
||||
mMessageText);
|
||||
}
|
||||
|
||||
if (clearLocalCopy) {
|
||||
// The message now owns all the attachments and the text...
|
||||
clearLocalDraftCopy();
|
||||
dispatchChanged(ALL_CHANGED);
|
||||
} else {
|
||||
// The draft message becomes a cached copy for UI.
|
||||
mIsDraftCachedCopy = true;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
private void clearLocalDraftCopy() {
|
||||
mIsDraftCachedCopy = false;
|
||||
mAttachments.clear();
|
||||
setMessageText("");
|
||||
setMessageSubject("");
|
||||
}
|
||||
|
||||
public String getConversationId() {
|
||||
return mConversationId;
|
||||
}
|
||||
|
||||
public String getMessageText() {
|
||||
return mMessageText;
|
||||
}
|
||||
|
||||
public String getMessageSubject() {
|
||||
return mMessageSubject;
|
||||
}
|
||||
|
||||
public boolean getIsMms() {
|
||||
final int selfSubId = getSelfSubId();
|
||||
return MmsSmsUtils.getRequireMmsForEmailAddress(mIncludeEmailAddress, selfSubId) ||
|
||||
(mIsGroupConversation && MmsUtils.groupMmsEnabled(selfSubId)) ||
|
||||
mMessageTextStats.getMessageLengthRequiresMms() || !mAttachments.isEmpty() ||
|
||||
!TextUtils.isEmpty(mMessageSubject);
|
||||
}
|
||||
|
||||
public boolean getIsGroupMmsConversation() {
|
||||
return getIsMms() && mIsGroupConversation;
|
||||
}
|
||||
|
||||
public String getSelfId() {
|
||||
return mSelfId;
|
||||
}
|
||||
|
||||
public int getNumMessagesToBeSent() {
|
||||
return mMessageTextStats.getNumMessagesToBeSent();
|
||||
}
|
||||
|
||||
public int getCodePointsRemainingInCurrentMessage() {
|
||||
return mMessageTextStats.getCodePointsRemainingInCurrentMessage();
|
||||
}
|
||||
|
||||
public int getSelfSubId() {
|
||||
return mSubscriptionDataProvider == null ? ParticipantData.DEFAULT_SELF_SUB_ID :
|
||||
mSubscriptionDataProvider.getConversationSelfSubId();
|
||||
}
|
||||
|
||||
private void setMessageText(final String messageText, final boolean notify) {
|
||||
mMessageText = messageText;
|
||||
mMessageTextStats.updateMessageTextStats(getSelfSubId(), mMessageText);
|
||||
if (notify) {
|
||||
dispatchChanged(MESSAGE_TEXT_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
private void setMessageSubject(final String subject, final boolean notify) {
|
||||
mMessageSubject = subject;
|
||||
if (notify) {
|
||||
dispatchChanged(MESSAGE_SUBJECT_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
public void setMessageText(final String messageText) {
|
||||
setMessageText(messageText, false);
|
||||
}
|
||||
|
||||
public void setMessageSubject(final String subject) {
|
||||
setMessageSubject(subject, false);
|
||||
}
|
||||
|
||||
public void addAttachments(final Collection<? extends MessagePartData> attachments) {
|
||||
// If the incoming attachments contains a single-only attachment, we need to clear
|
||||
// the existing attachments.
|
||||
for (final MessagePartData data : attachments) {
|
||||
if (data.isSinglePartOnly()) {
|
||||
// clear any existing attachments because the attachment we're adding can only
|
||||
// exist by itself.
|
||||
destroyAttachments();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If the existing attachments contain a single-only attachment, we need to clear the
|
||||
// existing attachments to make room for the incoming attachment.
|
||||
for (final MessagePartData data : mAttachments) {
|
||||
if (data.isSinglePartOnly()) {
|
||||
// clear any existing attachments because the single attachment can only exist
|
||||
// by itself
|
||||
destroyAttachments();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If any of the pending attachments contain a single-only attachment, we need to clear the
|
||||
// existing attachments to make room for the incoming attachment.
|
||||
for (final MessagePartData data : mPendingAttachments) {
|
||||
if (data.isSinglePartOnly()) {
|
||||
// clear any existing attachments because the single attachment can only exist
|
||||
// by itself
|
||||
destroyAttachments();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
boolean reachedLimit = false;
|
||||
for (final MessagePartData data : attachments) {
|
||||
// Don't break out of loop even if limit has been reached so we can destroy all
|
||||
// of the over-limit attachments.
|
||||
reachedLimit |= addOneAttachmentNoNotify(data);
|
||||
}
|
||||
if (reachedLimit) {
|
||||
dispatchAttachmentLimitReached();
|
||||
}
|
||||
dispatchChanged(ATTACHMENTS_CHANGED);
|
||||
}
|
||||
|
||||
public boolean containsAttachment(final Uri contentUri) {
|
||||
for (final MessagePartData existingAttachment : mAttachments) {
|
||||
if (existingAttachment.getContentUri().equals(contentUri)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (final PendingAttachmentData pendingAttachment : mPendingAttachments) {
|
||||
if (pendingAttachment.getContentUri().equals(contentUri)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to add one attachment to the attachment list, while guarding against duplicates and
|
||||
* going over the limit.
|
||||
* @return true if the attachment limit was reached, false otherwise
|
||||
*/
|
||||
private boolean addOneAttachmentNoNotify(final MessagePartData attachment) {
|
||||
Assert.isTrue(attachment.isAttachment());
|
||||
// Check duplication.
|
||||
for (final MessagePartData existingAttachment : mAttachments) {
|
||||
if (existingAttachment.getContentUri().equals(attachment.getContentUri())) {
|
||||
// Destroy existing attachment and replace with new attachment instead of destroying
|
||||
// new one so that mSelectedImages in GalleryGridView could be maintained correctly.
|
||||
mAttachments.remove(existingAttachment);
|
||||
existingAttachment.destroyAsync();
|
||||
addAttachment(attachment, null /*pendingAttachment*/);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final boolean reachedLimit = getAttachmentCount() >= getAttachmentLimit();
|
||||
if (reachedLimit) {
|
||||
// Never go over the limit.
|
||||
attachment.destroyAsync();
|
||||
return true;
|
||||
} else {
|
||||
addAttachment(attachment, null /*pendingAttachment*/);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void addAttachment(final MessagePartData attachment,
|
||||
final PendingAttachmentData pendingAttachment) {
|
||||
if (attachment != null && attachment.isSinglePartOnly()) {
|
||||
// clear any existing attachments because the attachment we're adding can only
|
||||
// exist by itself.
|
||||
destroyAttachments();
|
||||
}
|
||||
if (pendingAttachment != null && pendingAttachment.isSinglePartOnly()) {
|
||||
// clear any existing attachments because the attachment we're adding can only
|
||||
// exist by itself.
|
||||
destroyAttachments();
|
||||
}
|
||||
// If the existing attachments contain a single-only attachment, we need to clear the
|
||||
// existing attachments to make room for the incoming attachment.
|
||||
for (final MessagePartData data : mAttachments) {
|
||||
if (data.isSinglePartOnly()) {
|
||||
// clear any existing attachments because the single attachment can only exist
|
||||
// by itself
|
||||
destroyAttachments();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If any of the pending attachments contain a single-only attachment, we need to clear the
|
||||
// existing attachments to make room for the incoming attachment.
|
||||
for (final MessagePartData data : mPendingAttachments) {
|
||||
if (data.isSinglePartOnly()) {
|
||||
// clear any existing attachments because the single attachment can only exist
|
||||
// by itself
|
||||
destroyAttachments();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (attachment != null) {
|
||||
mAttachments.add(attachment);
|
||||
} else if (pendingAttachment != null) {
|
||||
mPendingAttachments.add(pendingAttachment);
|
||||
}
|
||||
}
|
||||
|
||||
public void addPendingAttachment(final PendingAttachmentData pendingAttachment,
|
||||
final BindingBase<DraftMessageData> binding) {
|
||||
final boolean reachedLimit = addOnePendingAttachmentNoNotify(pendingAttachment,
|
||||
binding.getBindingId());
|
||||
if (reachedLimit) {
|
||||
dispatchAttachmentLimitReached();
|
||||
}
|
||||
dispatchChanged(ATTACHMENTS_CHANGED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to add one pending attachment, while guarding against duplicates and
|
||||
* going over the limit.
|
||||
* @return true if the attachment limit was reached, false otherwise
|
||||
*/
|
||||
private boolean addOnePendingAttachmentNoNotify(final PendingAttachmentData pendingAttachment,
|
||||
final String bindingId) {
|
||||
final boolean reachedLimit = getAttachmentCount() >= getAttachmentLimit();
|
||||
if (reachedLimit || containsAttachment(pendingAttachment.getContentUri())) {
|
||||
// Never go over the limit. Never add duplicated attachments.
|
||||
pendingAttachment.destroyAsync();
|
||||
return reachedLimit;
|
||||
} else {
|
||||
Assert.isTrue(!mPendingAttachments.contains(pendingAttachment));
|
||||
Assert.equals(PendingAttachmentData.STATE_PENDING, pendingAttachment.getCurrentState());
|
||||
addAttachment(null /*attachment*/, pendingAttachment);
|
||||
|
||||
pendingAttachment.loadAttachmentForDraft(this, bindingId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSelfId(final String selfId, final boolean notify) {
|
||||
LogUtil.d(LogUtil.BUGLE_TAG, "DraftMessageData: set selfId=" + selfId
|
||||
+ " for conversationId=" + mConversationId);
|
||||
mSelfId = selfId;
|
||||
if (notify) {
|
||||
dispatchChanged(SELF_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasAttachments() {
|
||||
return !mAttachments.isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasPendingAttachments() {
|
||||
return !mPendingAttachments.isEmpty();
|
||||
}
|
||||
|
||||
private int getAttachmentCount() {
|
||||
return mAttachments.size() + mPendingAttachments.size();
|
||||
}
|
||||
|
||||
private int getVideoAttachmentCount() {
|
||||
int count = 0;
|
||||
for (MessagePartData part : mAttachments) {
|
||||
if (part.isVideo()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
for (MessagePartData part : mPendingAttachments) {
|
||||
if (part.isVideo()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private int getAttachmentLimit() {
|
||||
return BugleGservicesKeys.MMS_ATTACHMENT_LIMIT_DEFAULT;
|
||||
}
|
||||
|
||||
public void removeAttachment(final MessagePartData attachment) {
|
||||
for (final MessagePartData existingAttachment : mAttachments) {
|
||||
if (existingAttachment.getContentUri().equals(attachment.getContentUri())) {
|
||||
mAttachments.remove(existingAttachment);
|
||||
existingAttachment.destroyAsync();
|
||||
dispatchChanged(ATTACHMENTS_CHANGED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeExistingAttachments(final Set<MessagePartData> attachmentsToRemove) {
|
||||
boolean removed = false;
|
||||
final Iterator<MessagePartData> iterator = mAttachments.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
final MessagePartData existingAttachment = iterator.next();
|
||||
if (attachmentsToRemove.contains(existingAttachment)) {
|
||||
iterator.remove();
|
||||
existingAttachment.destroyAsync();
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
dispatchChanged(ATTACHMENTS_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
public void removePendingAttachment(final PendingAttachmentData pendingAttachment) {
|
||||
for (final PendingAttachmentData existingAttachment : mPendingAttachments) {
|
||||
if (existingAttachment.getContentUri().equals(pendingAttachment.getContentUri())) {
|
||||
mPendingAttachments.remove(pendingAttachment);
|
||||
pendingAttachment.destroyAsync();
|
||||
dispatchChanged(ATTACHMENTS_CHANGED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updatePendingAttachment(final MessagePartData updatedAttachment,
|
||||
final PendingAttachmentData pendingAttachment) {
|
||||
for (final PendingAttachmentData existingAttachment : mPendingAttachments) {
|
||||
if (existingAttachment.getContentUri().equals(pendingAttachment.getContentUri())) {
|
||||
mPendingAttachments.remove(pendingAttachment);
|
||||
if (pendingAttachment.isSinglePartOnly()) {
|
||||
updatedAttachment.setSinglePartOnly(true);
|
||||
}
|
||||
mAttachments.add(updatedAttachment);
|
||||
dispatchChanged(ATTACHMENTS_CHANGED);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we are here, this means the pending attachment has been dropped before the task
|
||||
// to load it was completed. In this case destroy the temporarily staged file since it
|
||||
// is no longer needed.
|
||||
updatedAttachment.destroyAsync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the attachments from the draft and notify any listeners.
|
||||
* @param flags typically this will be ATTACHMENTS_CHANGED. When attachments are cleared in a
|
||||
* widget, flags will also contain WIDGET_CHANGED.
|
||||
*/
|
||||
public void clearAttachments(final int flags) {
|
||||
destroyAttachments();
|
||||
dispatchChanged(flags);
|
||||
}
|
||||
|
||||
public List<MessagePartData> getReadOnlyAttachments() {
|
||||
return mReadOnlyAttachments;
|
||||
}
|
||||
|
||||
public List<PendingAttachmentData> getReadOnlyPendingAttachments() {
|
||||
return mReadOnlyPendingAttachments;
|
||||
}
|
||||
|
||||
public boolean loadFromStorage(final BindingBase<DraftMessageData> binding,
|
||||
final MessageData optionalIncomingDraft, boolean clearLocalDraft) {
|
||||
LogUtil.d(LogUtil.BUGLE_TAG, "DraftMessageData: "
|
||||
+ (optionalIncomingDraft == null ? "loading" : "setting")
|
||||
+ " for conversationId=" + mConversationId);
|
||||
if (clearLocalDraft) {
|
||||
clearLocalDraftCopy();
|
||||
}
|
||||
final boolean isDraftCachedCopy = mIsDraftCachedCopy;
|
||||
mIsDraftCachedCopy = false;
|
||||
// Before reading message from db ensure the caller is bound to us (and knows the id)
|
||||
if (mMonitor == null && !isDraftCachedCopy && isBound(binding.getBindingId())) {
|
||||
mMonitor = ReadDraftDataAction.readDraftData(mConversationId,
|
||||
optionalIncomingDraft, binding.getBindingId(), this);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current draft to db. This will save the draft and drop any pending attachments
|
||||
* we have. The UI typically goes into the background when this is called, and instead of
|
||||
* trying to persist the state of the pending attachments (the app may be killed, the activity
|
||||
* may be destroyed), we simply drop the pending attachments for consistency.
|
||||
*/
|
||||
public void saveToStorage(final BindingBase<DraftMessageData> binding) {
|
||||
saveToStorageInternal(binding);
|
||||
dropPendingAttachments();
|
||||
}
|
||||
|
||||
private void saveToStorageInternal(final BindingBase<DraftMessageData> binding) {
|
||||
// Create MessageData to store to db, but don't clear the in-memory copy so UI will
|
||||
// continue to display it.
|
||||
// If self id is null then we'll not attempt to change the conversation's self id.
|
||||
final MessageData message = createMessageWithCurrentAttachments(false /* clearLocalCopy */);
|
||||
// Before writing message to db ensure the caller is bound to us (and knows the id)
|
||||
if (isBound(binding.getBindingId())){
|
||||
WriteDraftMessageAction.writeDraftMessage(mConversationId, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when we are ready to send the message. This will assemble/return the MessageData for
|
||||
* sending and clear the local draft data, both from memory and from DB. This will also bind
|
||||
* the message data with a self Id through which the message will be sent.
|
||||
*
|
||||
* @param binding the binding object from our consumer. We need to make sure we are still bound
|
||||
* to that binding before saving to storage.
|
||||
*/
|
||||
public MessageData prepareMessageForSending(final BindingBase<DraftMessageData> binding) {
|
||||
// We can't send the message while there's still stuff pending.
|
||||
Assert.isTrue(!hasPendingAttachments());
|
||||
mSending = true;
|
||||
// Assembles the message to send and empty working draft data.
|
||||
// If self id is null then message is sent with conversation's self id.
|
||||
final MessageData messageToSend =
|
||||
createMessageWithCurrentAttachments(true /* clearLocalCopy */);
|
||||
// Note sending message will empty the draft data in DB.
|
||||
mSending = false;
|
||||
return messageToSend;
|
||||
}
|
||||
|
||||
public boolean isSending() {
|
||||
return mSending;
|
||||
}
|
||||
|
||||
@Override // ReadDraftMessageActionListener.onReadDraftMessageSucceeded
|
||||
public void onReadDraftDataSucceeded(final ReadDraftDataAction action, final Object data,
|
||||
final MessageData message, final ConversationListItemData conversation) {
|
||||
final String bindingId = (String) data;
|
||||
|
||||
// Before passing draft message on to ui ensure the data is bound to the same bindingid
|
||||
if (isBound(bindingId)) {
|
||||
mSelfId = message.getSelfId();
|
||||
mIsGroupConversation = conversation.getIsGroup();
|
||||
mIncludeEmailAddress = conversation.getIncludeEmailAddress();
|
||||
updateFromMessageData(message, bindingId);
|
||||
LogUtil.d(LogUtil.BUGLE_TAG, "DraftMessageData: draft loaded. "
|
||||
+ "conversationId=" + mConversationId + " selfId=" + mSelfId);
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "DraftMessageData: draft loaded but not bound. "
|
||||
+ "conversationId=" + mConversationId);
|
||||
}
|
||||
mMonitor = null;
|
||||
}
|
||||
|
||||
@Override // ReadDraftMessageActionListener.onReadDraftDataFailed
|
||||
public void onReadDraftDataFailed(final ReadDraftDataAction action, final Object data) {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "DraftMessageData: draft not loaded. "
|
||||
+ "conversationId=" + mConversationId);
|
||||
// The draft is now synced with actual MessageData and no longer a cached copy.
|
||||
mIsDraftCachedCopy = false;
|
||||
// Just clear the monitor - no update to draft data
|
||||
mMonitor = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Bugle is default sms app
|
||||
*/
|
||||
public boolean getIsDefaultSmsApp() {
|
||||
return PhoneUtils.getDefault().isDefaultSmsApp();
|
||||
}
|
||||
|
||||
@Override //BindableData.unregisterListeners
|
||||
protected void unregisterListeners() {
|
||||
if (mMonitor != null) {
|
||||
mMonitor.unregister();
|
||||
}
|
||||
mMonitor = null;
|
||||
mListeners.clear();
|
||||
}
|
||||
|
||||
private void destroyAttachments() {
|
||||
for (final MessagePartData attachment : mAttachments) {
|
||||
attachment.destroyAsync();
|
||||
}
|
||||
mAttachments.clear();
|
||||
mPendingAttachments.clear();
|
||||
}
|
||||
|
||||
private void dispatchChanged(final int changeFlags) {
|
||||
// No change is expected to be made to the draft if it is in cached copy state.
|
||||
if (mIsDraftCachedCopy) {
|
||||
return;
|
||||
}
|
||||
// Any change in the draft will cancel any pending draft checking task, since the
|
||||
// size/status of the draft may have changed.
|
||||
if (mCheckDraftForSendTask != null) {
|
||||
mCheckDraftForSendTask.cancel();
|
||||
mCheckDraftForSendTask = null;
|
||||
}
|
||||
mListeners.onDraftChanged(this, changeFlags);
|
||||
}
|
||||
|
||||
private void dispatchAttachmentLimitReached() {
|
||||
mListeners.onDraftAttachmentLimitReached(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop any pending attachments that haven't finished. This is called after the UI goes to
|
||||
* the background and we persist the draft data to the database.
|
||||
*/
|
||||
private void dropPendingAttachments() {
|
||||
mPendingAttachments.clear();
|
||||
}
|
||||
|
||||
private boolean isDraftEmpty() {
|
||||
return TextUtils.isEmpty(mMessageText) && mAttachments.isEmpty() &&
|
||||
TextUtils.isEmpty(mMessageSubject);
|
||||
}
|
||||
|
||||
public boolean isCheckingDraft() {
|
||||
return mCheckDraftForSendTask != null && !mCheckDraftForSendTask.isCancelled();
|
||||
}
|
||||
|
||||
public void checkDraftForAction(final boolean checkMessageSize, final int selfSubId,
|
||||
final CheckDraftTaskCallback callback, final Binding<DraftMessageData> binding) {
|
||||
new CheckDraftForSendTask(checkMessageSize, selfSubId, callback, binding)
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows us to have multiple data listeners for DraftMessageData
|
||||
*/
|
||||
private class DraftMessageDataEventDispatcher
|
||||
extends ArrayList<DraftMessageDataListener>
|
||||
implements DraftMessageDataListener {
|
||||
|
||||
@Override
|
||||
@RunsOnMainThread
|
||||
public void onDraftChanged(DraftMessageData data, int changeFlags) {
|
||||
Assert.isMainThread();
|
||||
for (final DraftMessageDataListener listener : this) {
|
||||
listener.onDraftChanged(data, changeFlags);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RunsOnMainThread
|
||||
public void onDraftAttachmentLimitReached(DraftMessageData data) {
|
||||
Assert.isMainThread();
|
||||
for (final DraftMessageDataListener listener : this) {
|
||||
listener.onDraftAttachmentLimitReached(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RunsOnMainThread
|
||||
public void onDraftAttachmentLoadFailed() {
|
||||
Assert.isMainThread();
|
||||
for (final DraftMessageDataListener listener : this) {
|
||||
listener.onDraftAttachmentLoadFailed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface CheckDraftTaskCallback {
|
||||
void onDraftChecked(DraftMessageData data, int result);
|
||||
}
|
||||
|
||||
public class CheckDraftForSendTask {
|
||||
public static final int RESULT_PASSED = 0;
|
||||
public static final int RESULT_HAS_PENDING_ATTACHMENTS = 1;
|
||||
public static final int RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS = 2;
|
||||
public static final int RESULT_MESSAGE_OVER_LIMIT = 3;
|
||||
public static final int RESULT_VIDEO_ATTACHMENT_LIMIT_EXCEEDED = 4;
|
||||
public static final int RESULT_SIM_NOT_READY = 5;
|
||||
private final boolean mCheckMessageSize;
|
||||
private final int mSelfSubId;
|
||||
private final CheckDraftTaskCallback mCallback;
|
||||
private final String mBindingId;
|
||||
private final List<MessagePartData> mAttachmentsCopy;
|
||||
ScheduledExecutorService mExecutor = Executors.newScheduledThreadPool(2);
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
private Future<?> mFuture;
|
||||
private boolean mCancelled;
|
||||
private int mPreExecuteResult = RESULT_PASSED;
|
||||
|
||||
public CheckDraftForSendTask(final boolean checkMessageSize, final int selfSubId,
|
||||
final CheckDraftTaskCallback callback, final Binding<DraftMessageData> binding) {
|
||||
mCheckMessageSize = checkMessageSize;
|
||||
mSelfSubId = selfSubId;
|
||||
mCallback = callback;
|
||||
mBindingId = binding.getBindingId();
|
||||
// Obtain an immutable copy of the attachment list so we can operate on it in the
|
||||
// background thread.
|
||||
mAttachmentsCopy = new ArrayList<>(mAttachments);
|
||||
|
||||
mCheckDraftForSendTask = this;
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
onPreExecute();
|
||||
|
||||
mFuture = mExecutor.submit(() -> {
|
||||
final int result;
|
||||
if (mPreExecuteResult != RESULT_PASSED) {
|
||||
result = mPreExecuteResult;
|
||||
} else if (mCheckMessageSize && getIsMessageOverLimit()) {
|
||||
result = RESULT_MESSAGE_OVER_LIMIT;
|
||||
} else {
|
||||
result = RESULT_PASSED;
|
||||
}
|
||||
|
||||
mHandler.post(() -> {
|
||||
mCheckDraftForSendTask = null;
|
||||
// Only call back if we are bound to the original binding.
|
||||
if (isBound(mBindingId) && !mCancelled) {
|
||||
mCallback.onDraftChecked(DraftMessageData.this, result);
|
||||
} else {
|
||||
if (!isBound(mBindingId)) {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: draft not bound");
|
||||
}
|
||||
if (mCancelled) {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG,
|
||||
"Message can't be sent: draft is cancelled");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mExecutor.schedule(this::cancel, 10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
if (!mFuture.isDone()) {
|
||||
mFuture.cancel(true);
|
||||
mCancelled = true;
|
||||
mCheckDraftForSendTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCancelled() {
|
||||
return mCancelled;
|
||||
}
|
||||
|
||||
protected void onPreExecute() {
|
||||
// Perform checking work that can happen on the main thread.
|
||||
if (hasPendingAttachments()) {
|
||||
mPreExecuteResult = RESULT_HAS_PENDING_ATTACHMENTS;
|
||||
return;
|
||||
}
|
||||
if (getIsGroupMmsConversation()) {
|
||||
try {
|
||||
if (TextUtils.isEmpty(PhoneUtils.get(mSelfSubId).getSelfRawNumber(true))) {
|
||||
mPreExecuteResult = RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS;
|
||||
return;
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
// This happens when there is no active subscription, e.g. on Nova
|
||||
// when the phone switches carrier.
|
||||
mPreExecuteResult = RESULT_SIM_NOT_READY;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (getVideoAttachmentCount() > MmsUtils.MAX_VIDEO_ATTACHMENT_COUNT) {
|
||||
mPreExecuteResult = RESULT_VIDEO_ATTACHMENT_LIMIT_EXCEEDED;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Check if the draft message contains too many attachments to send
|
||||
* 2. Computes the minimum size that this message could be compressed/downsampled/encoded
|
||||
* before sending and check if it meets the carrier max size for sending.
|
||||
* @see MessagePartData#getMinimumSizeInBytesForSending()
|
||||
*/
|
||||
@DoesNotRunOnMainThread
|
||||
private boolean getIsMessageOverLimit() {
|
||||
Assert.isNotMainThread();
|
||||
if (mAttachmentsCopy.size() > getAttachmentLimit()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Aggregate the size from all the attachments.
|
||||
long totalSize = 0;
|
||||
for (final MessagePartData attachment : mAttachmentsCopy) {
|
||||
totalSize += attachment.getMinimumSizeInBytesForSending();
|
||||
}
|
||||
return totalSize > MmsConfig.get(mSelfSubId).getMaxMessageSize();
|
||||
}
|
||||
}
|
||||
|
||||
public void onPendingAttachmentLoadFailed(PendingAttachmentData data) {
|
||||
mListeners.onDraftAttachmentLoadFailed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import com.android.messaging.datamodel.action.ActionMonitor;
|
||||
import com.android.messaging.datamodel.action.GetOrCreateConversationAction;
|
||||
import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionListener;
|
||||
import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionMonitor;
|
||||
import com.android.messaging.datamodel.binding.BindableData;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.RunsOnMainThread;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
public class LaunchConversationData extends BindableData implements
|
||||
GetOrCreateConversationActionListener {
|
||||
public interface LaunchConversationDataListener {
|
||||
void onGetOrCreateNewConversation(String conversationId);
|
||||
void onGetOrCreateNewConversationFailed();
|
||||
}
|
||||
|
||||
private LaunchConversationDataListener mListener;
|
||||
private GetOrCreateConversationActionMonitor mMonitor;
|
||||
|
||||
public LaunchConversationData(final LaunchConversationDataListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unregisterListeners() {
|
||||
mListener = null;
|
||||
if (mMonitor != null) {
|
||||
mMonitor.unregister();
|
||||
}
|
||||
mMonitor = null;
|
||||
}
|
||||
|
||||
public void getOrCreateConversation(final BindingBase<LaunchConversationData> binding,
|
||||
final String[] recipients) {
|
||||
final String bindingId = binding.getBindingId();
|
||||
|
||||
// Start a new conversation from the list of contacts.
|
||||
if (isBound(bindingId) && mMonitor == null) {
|
||||
mMonitor = GetOrCreateConversationAction.getOrCreateConversation(recipients,
|
||||
bindingId, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RunsOnMainThread
|
||||
public void onGetOrCreateConversationSucceeded(final ActionMonitor monitor,
|
||||
final Object data, final String conversationId) {
|
||||
Assert.isTrue(monitor == mMonitor);
|
||||
Assert.isTrue(conversationId != null);
|
||||
|
||||
final String bindingId = (String) data;
|
||||
if (isBound(bindingId) && mListener != null) {
|
||||
mListener.onGetOrCreateNewConversation(conversationId);
|
||||
}
|
||||
|
||||
mMonitor = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@RunsOnMainThread
|
||||
public void onGetOrCreateConversationFailed(final ActionMonitor monitor,
|
||||
final Object data) {
|
||||
Assert.isTrue(monitor == mMonitor);
|
||||
final String bindingId = (String) data;
|
||||
if (isBound(bindingId) && mListener != null) {
|
||||
mListener.onGetOrCreateNewConversationFailed();
|
||||
}
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "onGetOrCreateConversationFailed");
|
||||
mMonitor = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.android.messaging.datamodel.binding.BindableData;
|
||||
import com.android.messaging.util.BuglePrefs;
|
||||
import com.android.messaging.util.BuglePrefsKeys;
|
||||
|
||||
/**
|
||||
* Services data needs for MediaPicker.
|
||||
*/
|
||||
public class MediaPickerData extends BindableData {
|
||||
private final Context mContext;
|
||||
|
||||
public MediaPickerData(final Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last selected chooser index, or -1 if no selection has been saved.
|
||||
*/
|
||||
public int getSelectedChooserIndex() {
|
||||
return BuglePrefs.getApplicationPrefs().getInt(
|
||||
BuglePrefsKeys.SELECTED_MEDIA_PICKER_CHOOSER_INDEX,
|
||||
BuglePrefsKeys.SELECTED_MEDIA_PICKER_CHOOSER_INDEX_DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the selected media chooser index.
|
||||
* @param selectedIndex the selected media chooser index.
|
||||
*/
|
||||
public void saveSelectedChooserIndex(final int selectedIndex) {
|
||||
BuglePrefs.getApplicationPrefs().putInt(BuglePrefsKeys.SELECTED_MEDIA_PICKER_CHOOSER_INDEX,
|
||||
selectedIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unregisterListeners() {
|
||||
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
|
||||
public class MediaPickerMessagePartData extends MessagePartData {
|
||||
private final Rect mStartRect;
|
||||
|
||||
public MediaPickerMessagePartData(final Rect startRect, final String contentType,
|
||||
final Uri contentUri, final int width, final int height) {
|
||||
this(startRect, null /* messageText */, contentType, contentUri, width, height);
|
||||
}
|
||||
|
||||
public MediaPickerMessagePartData(final Rect startRect, final String messageText,
|
||||
final String contentType, final Uri contentUri, final int width, final int height) {
|
||||
this(startRect, messageText, contentType, contentUri, width, height,
|
||||
false /*onlySingleAttachment*/);
|
||||
}
|
||||
|
||||
public MediaPickerMessagePartData(final Rect startRect, final String contentType,
|
||||
final Uri contentUri, final int width, final int height,
|
||||
final boolean onlySingleAttachment) {
|
||||
this(startRect, null /* messageText */, contentType, contentUri, width, height,
|
||||
onlySingleAttachment);
|
||||
}
|
||||
|
||||
public MediaPickerMessagePartData(final Rect startRect, final String messageText,
|
||||
final String contentType, final Uri contentUri, final int width, final int height,
|
||||
final boolean onlySingleAttachment) {
|
||||
super(messageText, contentType, contentUri, width, height, onlySingleAttachment);
|
||||
mStartRect = startRect;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The starting rect to animate the attachment preview from in order to perform a smooth
|
||||
* transition
|
||||
*/
|
||||
public Rect getStartRect() {
|
||||
return mStartRect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the start rect of the attachment.
|
||||
*/
|
||||
public void setStartRect(final Rect startRect) {
|
||||
mStartRect.set(startRect);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,911 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 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.data;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.BugleGservicesKeys;
|
||||
import com.android.messaging.util.Dates;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class MessageData implements Parcelable {
|
||||
private static final String[] sProjection = {
|
||||
MessageColumns._ID,
|
||||
MessageColumns.CONVERSATION_ID,
|
||||
MessageColumns.SENDER_PARTICIPANT_ID,
|
||||
MessageColumns.SELF_PARTICIPANT_ID,
|
||||
MessageColumns.SENT_TIMESTAMP,
|
||||
MessageColumns.RECEIVED_TIMESTAMP,
|
||||
MessageColumns.SEEN,
|
||||
MessageColumns.READ,
|
||||
MessageColumns.PROTOCOL,
|
||||
MessageColumns.STATUS,
|
||||
MessageColumns.SMS_MESSAGE_URI,
|
||||
MessageColumns.SMS_PRIORITY,
|
||||
MessageColumns.SMS_MESSAGE_SIZE,
|
||||
MessageColumns.MMS_SUBJECT,
|
||||
MessageColumns.MMS_TRANSACTION_ID,
|
||||
MessageColumns.MMS_CONTENT_LOCATION,
|
||||
MessageColumns.MMS_EXPIRY,
|
||||
MessageColumns.RAW_TELEPHONY_STATUS,
|
||||
MessageColumns.RETRY_START_TIMESTAMP,
|
||||
};
|
||||
|
||||
private static final int INDEX_ID = 0;
|
||||
private static final int INDEX_CONVERSATION_ID = 1;
|
||||
private static final int INDEX_PARTICIPANT_ID = 2;
|
||||
private static final int INDEX_SELF_ID = 3;
|
||||
private static final int INDEX_SENT_TIMESTAMP = 4;
|
||||
private static final int INDEX_RECEIVED_TIMESTAMP = 5;
|
||||
private static final int INDEX_SEEN = 6;
|
||||
private static final int INDEX_READ = 7;
|
||||
private static final int INDEX_PROTOCOL = 8;
|
||||
private static final int INDEX_BUGLE_STATUS = 9;
|
||||
private static final int INDEX_SMS_MESSAGE_URI = 10;
|
||||
private static final int INDEX_SMS_PRIORITY = 11;
|
||||
private static final int INDEX_SMS_MESSAGE_SIZE = 12;
|
||||
private static final int INDEX_MMS_SUBJECT = 13;
|
||||
private static final int INDEX_MMS_TRANSACTION_ID = 14;
|
||||
private static final int INDEX_MMS_CONTENT_LOCATION = 15;
|
||||
private static final int INDEX_MMS_EXPIRY = 16;
|
||||
private static final int INDEX_RAW_TELEPHONY_STATUS = 17;
|
||||
private static final int INDEX_RETRY_START_TIMESTAMP = 18;
|
||||
|
||||
// SQL statement to insert a "complete" message row (columns based on the projection above).
|
||||
private static final String INSERT_MESSAGE_SQL =
|
||||
"INSERT INTO " + DatabaseHelper.MESSAGES_TABLE + " ( "
|
||||
+ TextUtils.join(", ", Arrays.copyOfRange(sProjection, 1,
|
||||
INDEX_RETRY_START_TIMESTAMP + 1))
|
||||
+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
private String mMessageId;
|
||||
private String mConversationId;
|
||||
private String mParticipantId;
|
||||
private String mSelfId;
|
||||
private long mSentTimestamp;
|
||||
private long mReceivedTimestamp;
|
||||
private boolean mSeen;
|
||||
private boolean mRead;
|
||||
private int mProtocol;
|
||||
private Uri mSmsMessageUri;
|
||||
private int mSmsPriority;
|
||||
private long mSmsMessageSize;
|
||||
private String mMmsSubject;
|
||||
private String mMmsTransactionId;
|
||||
private String mMmsContentLocation;
|
||||
private long mMmsExpiry;
|
||||
private int mRawStatus;
|
||||
private int mStatus;
|
||||
private final ArrayList<MessagePartData> mParts;
|
||||
private long mRetryStartTimestamp;
|
||||
|
||||
// PROTOCOL Values
|
||||
public static final int PROTOCOL_UNKNOWN = -1; // Unknown type
|
||||
public static final int PROTOCOL_SMS = 0; // SMS message
|
||||
public static final int PROTOCOL_MMS = 1; // MMS message
|
||||
public static final int PROTOCOL_MMS_PUSH_NOTIFICATION = 2; // MMS WAP push notification
|
||||
|
||||
// Bugle STATUS Values
|
||||
public static final int BUGLE_STATUS_UNKNOWN = 0;
|
||||
|
||||
// Outgoing
|
||||
public static final int BUGLE_STATUS_OUTGOING_COMPLETE = 1;
|
||||
public static final int BUGLE_STATUS_OUTGOING_DELIVERED = 2;
|
||||
// Transitions to either YET_TO_SEND or SEND_AFTER_PROCESSING depending attachments.
|
||||
public static final int BUGLE_STATUS_OUTGOING_DRAFT = 3;
|
||||
public static final int BUGLE_STATUS_OUTGOING_YET_TO_SEND = 4;
|
||||
public static final int BUGLE_STATUS_OUTGOING_SENDING = 5;
|
||||
public static final int BUGLE_STATUS_OUTGOING_RESENDING = 6;
|
||||
public static final int BUGLE_STATUS_OUTGOING_AWAITING_RETRY = 7;
|
||||
public static final int BUGLE_STATUS_OUTGOING_FAILED = 8;
|
||||
public static final int BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER = 9;
|
||||
|
||||
// Incoming
|
||||
public static final int BUGLE_STATUS_INCOMING_COMPLETE = 100;
|
||||
public static final int BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD = 101;
|
||||
public static final int BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD = 102;
|
||||
public static final int BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING = 103;
|
||||
public static final int BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD = 104;
|
||||
public static final int BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING = 105;
|
||||
public static final int BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED = 106;
|
||||
public static final int BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE = 107;
|
||||
|
||||
public static String getStatusDescription(int status) {
|
||||
switch (status) {
|
||||
case BUGLE_STATUS_UNKNOWN:
|
||||
return "UNKNOWN";
|
||||
case BUGLE_STATUS_OUTGOING_COMPLETE:
|
||||
return "OUTGOING_COMPLETE";
|
||||
case BUGLE_STATUS_OUTGOING_DELIVERED:
|
||||
return "OUTGOING_DELIVERED";
|
||||
case BUGLE_STATUS_OUTGOING_DRAFT:
|
||||
return "OUTGOING_DRAFT";
|
||||
case BUGLE_STATUS_OUTGOING_YET_TO_SEND:
|
||||
return "OUTGOING_YET_TO_SEND";
|
||||
case BUGLE_STATUS_OUTGOING_SENDING:
|
||||
return "OUTGOING_SENDING";
|
||||
case BUGLE_STATUS_OUTGOING_RESENDING:
|
||||
return "OUTGOING_RESENDING";
|
||||
case BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
|
||||
return "OUTGOING_AWAITING_RETRY";
|
||||
case BUGLE_STATUS_OUTGOING_FAILED:
|
||||
return "OUTGOING_FAILED";
|
||||
case BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
|
||||
return "OUTGOING_FAILED_EMERGENCY_NUMBER";
|
||||
case BUGLE_STATUS_INCOMING_COMPLETE:
|
||||
return "INCOMING_COMPLETE";
|
||||
case BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD:
|
||||
return "INCOMING_YET_TO_MANUAL_DOWNLOAD";
|
||||
case BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD:
|
||||
return "INCOMING_RETRYING_MANUAL_DOWNLOAD";
|
||||
case BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING:
|
||||
return "INCOMING_MANUAL_DOWNLOADING";
|
||||
case BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD:
|
||||
return "INCOMING_RETRYING_AUTO_DOWNLOAD";
|
||||
case BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING:
|
||||
return "INCOMING_AUTO_DOWNLOADING";
|
||||
case BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED:
|
||||
return "INCOMING_DOWNLOAD_FAILED";
|
||||
case BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE:
|
||||
return "INCOMING_EXPIRED_OR_NOT_AVAILABLE";
|
||||
default:
|
||||
return status + " (check MessageData)";
|
||||
}
|
||||
}
|
||||
|
||||
// All incoming messages expect to have status >= BUGLE_STATUS_FIRST_INCOMING
|
||||
public static final int BUGLE_STATUS_FIRST_INCOMING = BUGLE_STATUS_INCOMING_COMPLETE;
|
||||
|
||||
// Detailed MMS failures. Most of the values are defined in PduHeaders. However, a few are
|
||||
// defined here instead. These are never returned in the MMS HTTP response, but are used
|
||||
// internally. The values here must not conflict with any of the existing PduHeader values.
|
||||
public static final int RAW_TELEPHONY_STATUS_UNDEFINED = MmsUtils.PDU_HEADER_VALUE_UNDEFINED;
|
||||
public static final int RAW_TELEPHONY_STATUS_MESSAGE_TOO_BIG = 10000;
|
||||
|
||||
// Unknown result code for MMS sending/downloading. This is used as the default value
|
||||
// for result code returned from platform MMS API.
|
||||
public static final int UNKNOWN_RESULT_CODE = 0;
|
||||
|
||||
/**
|
||||
* Create an "empty" message
|
||||
*/
|
||||
public MessageData() {
|
||||
mParts = new ArrayList<>();
|
||||
}
|
||||
|
||||
public static String[] getProjection() {
|
||||
return sProjection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a draft message for a particular conversation based on supplied content
|
||||
*/
|
||||
public static MessageData createDraftMessage(final String conversationId,
|
||||
final String selfId, final MessageData content) {
|
||||
final MessageData message = new MessageData();
|
||||
message.mStatus = BUGLE_STATUS_OUTGOING_DRAFT;
|
||||
message.mProtocol = PROTOCOL_UNKNOWN;
|
||||
message.mConversationId = conversationId;
|
||||
message.mParticipantId = selfId;
|
||||
message.mReceivedTimestamp = System.currentTimeMillis();
|
||||
if (content == null) {
|
||||
message.mParts.add(MessagePartData.createTextMessagePart(""));
|
||||
} else {
|
||||
if (!TextUtils.isEmpty(content.mParticipantId)) {
|
||||
message.mParticipantId = content.mParticipantId;
|
||||
}
|
||||
if (!TextUtils.isEmpty(content.mMmsSubject)) {
|
||||
message.mMmsSubject = content.mMmsSubject;
|
||||
}
|
||||
for (final MessagePartData part : content.getParts()) {
|
||||
message.mParts.add(part);
|
||||
}
|
||||
}
|
||||
message.mSelfId = selfId;
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a draft sms message for a particular conversation
|
||||
*/
|
||||
public static MessageData createDraftSmsMessage(final String conversationId,
|
||||
final String selfId, final String messageText) {
|
||||
final MessageData message = new MessageData();
|
||||
message.mStatus = BUGLE_STATUS_OUTGOING_DRAFT;
|
||||
message.mProtocol = PROTOCOL_SMS;
|
||||
message.mConversationId = conversationId;
|
||||
message.mParticipantId = selfId;
|
||||
message.mSelfId = selfId;
|
||||
message.mParts.add(MessagePartData.createTextMessagePart(messageText));
|
||||
message.mReceivedTimestamp = System.currentTimeMillis();
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a draft mms message for a particular conversation
|
||||
*/
|
||||
public static MessageData createDraftMmsMessage(final String conversationId,
|
||||
final String selfId, final String messageText, final String subjectText) {
|
||||
final MessageData message = new MessageData();
|
||||
message.mStatus = BUGLE_STATUS_OUTGOING_DRAFT;
|
||||
message.mProtocol = PROTOCOL_MMS;
|
||||
message.mConversationId = conversationId;
|
||||
message.mParticipantId = selfId;
|
||||
message.mSelfId = selfId;
|
||||
message.mMmsSubject = subjectText;
|
||||
message.mReceivedTimestamp = System.currentTimeMillis();
|
||||
if (!TextUtils.isEmpty(messageText)) {
|
||||
message.mParts.add(MessagePartData.createTextMessagePart(messageText));
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a message received from a particular number in a particular conversation
|
||||
*/
|
||||
public static MessageData createReceivedSmsMessage(final Uri uri, final String conversationId,
|
||||
final String participantId, final String selfId, final String messageText,
|
||||
final String subject, final long sent, final long recieved,
|
||||
final boolean seen, final boolean read) {
|
||||
final MessageData message = new MessageData();
|
||||
message.mSmsMessageUri = uri;
|
||||
message.mConversationId = conversationId;
|
||||
message.mParticipantId = participantId;
|
||||
message.mSelfId = selfId;
|
||||
message.mProtocol = PROTOCOL_SMS;
|
||||
message.mStatus = BUGLE_STATUS_INCOMING_COMPLETE;
|
||||
message.mMmsSubject = subject;
|
||||
message.mReceivedTimestamp = recieved;
|
||||
message.mSentTimestamp = sent;
|
||||
message.mParts.add(MessagePartData.createTextMessagePart(messageText));
|
||||
message.mSeen = seen;
|
||||
message.mRead = read;
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a message not yet associated with a particular conversation
|
||||
*/
|
||||
public static MessageData createSharedMessage(final String messageText,
|
||||
final String subjectText) {
|
||||
final MessageData message = new MessageData();
|
||||
message.mStatus = BUGLE_STATUS_OUTGOING_DRAFT;
|
||||
message.mMmsSubject = subjectText;
|
||||
if (!TextUtils.isEmpty(messageText)) {
|
||||
message.mParts.add(MessagePartData.createTextMessagePart(messageText));
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a message from Sms table fields
|
||||
*/
|
||||
public static MessageData createSmsMessage(final String messageUri, final String participantId,
|
||||
final String selfId, final String conversationId, final int bugleStatus,
|
||||
final boolean seen, final boolean read, final long sent,
|
||||
final long recieved, final String messageText) {
|
||||
final MessageData message = new MessageData();
|
||||
message.mParticipantId = participantId;
|
||||
message.mSelfId = selfId;
|
||||
message.mConversationId = conversationId;
|
||||
message.mSentTimestamp = sent;
|
||||
message.mReceivedTimestamp = recieved;
|
||||
message.mSeen = seen;
|
||||
message.mRead = read;
|
||||
message.mProtocol = PROTOCOL_SMS;
|
||||
message.mStatus = bugleStatus;
|
||||
message.mSmsMessageUri = Uri.parse(messageUri);
|
||||
message.mParts.add(MessagePartData.createTextMessagePart(messageText));
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a message from Mms table fields
|
||||
*/
|
||||
public static MessageData createMmsMessage(final String messageUri, final String participantId,
|
||||
final String selfId, final String conversationId, final boolean isNotification,
|
||||
final int bugleStatus, final String contentLocation, final String transactionId,
|
||||
final int smsPriority, final String subject, final boolean seen, final boolean read,
|
||||
final long size, final int rawStatus, final long expiry, final long sent,
|
||||
final long received) {
|
||||
final MessageData message = new MessageData();
|
||||
message.mParticipantId = participantId;
|
||||
message.mSelfId = selfId;
|
||||
message.mConversationId = conversationId;
|
||||
message.mSentTimestamp = sent;
|
||||
message.mReceivedTimestamp = received;
|
||||
message.mMmsContentLocation = contentLocation;
|
||||
message.mMmsTransactionId = transactionId;
|
||||
message.mSeen = seen;
|
||||
message.mRead = read;
|
||||
message.mStatus = bugleStatus;
|
||||
message.mProtocol = (isNotification ? PROTOCOL_MMS_PUSH_NOTIFICATION : PROTOCOL_MMS);
|
||||
message.mSmsMessageUri = Uri.parse(messageUri);
|
||||
message.mSmsPriority = smsPriority;
|
||||
message.mSmsMessageSize = size;
|
||||
message.mMmsSubject = subject;
|
||||
message.mMmsExpiry = expiry;
|
||||
message.mRawStatus = rawStatus;
|
||||
if (bugleStatus == BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD ||
|
||||
bugleStatus == BUGLE_STATUS_OUTGOING_RESENDING) {
|
||||
// Set the retry start timestamp if this message is already in process of retrying
|
||||
// Either as autodownload is starting or sending already in progress (MMS update)
|
||||
message.mRetryStartTimestamp = received;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
public void addPart(final MessagePartData part) {
|
||||
if (part instanceof PendingAttachmentData) {
|
||||
// Pending attachments may only be added to shared message data that's not associated
|
||||
// with any particular conversation, in order to store shared images.
|
||||
Assert.isTrue(mConversationId == null);
|
||||
}
|
||||
mParts.add(part);
|
||||
}
|
||||
|
||||
public Iterable<MessagePartData> getParts() {
|
||||
return mParts;
|
||||
}
|
||||
|
||||
public void bind(final Cursor cursor) {
|
||||
mMessageId = cursor.getString(INDEX_ID);
|
||||
mConversationId = cursor.getString(INDEX_CONVERSATION_ID);
|
||||
mParticipantId = cursor.getString(INDEX_PARTICIPANT_ID);
|
||||
mSelfId = cursor.getString(INDEX_SELF_ID);
|
||||
mSentTimestamp = cursor.getLong(INDEX_SENT_TIMESTAMP);
|
||||
mReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP);
|
||||
mSeen = (cursor.getInt(INDEX_SEEN) != 0);
|
||||
mRead = (cursor.getInt(INDEX_READ) != 0);
|
||||
mProtocol = cursor.getInt(INDEX_PROTOCOL);
|
||||
mStatus = cursor.getInt(INDEX_BUGLE_STATUS);
|
||||
final String smsMessageUri = cursor.getString(INDEX_SMS_MESSAGE_URI);
|
||||
mSmsMessageUri = (smsMessageUri == null) ? null : Uri.parse(smsMessageUri);
|
||||
mSmsPriority = cursor.getInt(INDEX_SMS_PRIORITY);
|
||||
mSmsMessageSize = cursor.getLong(INDEX_SMS_MESSAGE_SIZE);
|
||||
mMmsExpiry = cursor.getLong(INDEX_MMS_EXPIRY);
|
||||
mRawStatus = cursor.getInt(INDEX_RAW_TELEPHONY_STATUS);
|
||||
mMmsSubject = cursor.getString(INDEX_MMS_SUBJECT);
|
||||
mMmsTransactionId = cursor.getString(INDEX_MMS_TRANSACTION_ID);
|
||||
mMmsContentLocation = cursor.getString(INDEX_MMS_CONTENT_LOCATION);
|
||||
mRetryStartTimestamp = cursor.getLong(INDEX_RETRY_START_TIMESTAMP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to the draft message data for a conversation. The conversation's self id is used as
|
||||
* the draft's self id.
|
||||
*/
|
||||
public void bindDraft(final Cursor cursor, final String conversationSelfId) {
|
||||
bind(cursor);
|
||||
mSelfId = conversationSelfId;
|
||||
}
|
||||
|
||||
protected static String getParticipantId(final Cursor cursor) {
|
||||
return cursor.getString(INDEX_PARTICIPANT_ID);
|
||||
}
|
||||
|
||||
public void populate(final ContentValues values) {
|
||||
values.put(MessageColumns.CONVERSATION_ID, mConversationId);
|
||||
values.put(MessageColumns.SENDER_PARTICIPANT_ID, mParticipantId);
|
||||
values.put(MessageColumns.SELF_PARTICIPANT_ID, mSelfId);
|
||||
values.put(MessageColumns.SENT_TIMESTAMP, mSentTimestamp);
|
||||
values.put(MessageColumns.RECEIVED_TIMESTAMP, mReceivedTimestamp);
|
||||
values.put(MessageColumns.SEEN, mSeen ? 1 : 0);
|
||||
values.put(MessageColumns.READ, mRead ? 1 : 0);
|
||||
values.put(MessageColumns.PROTOCOL, mProtocol);
|
||||
values.put(MessageColumns.STATUS, mStatus);
|
||||
final String smsMessageUri = ((mSmsMessageUri == null) ? null : mSmsMessageUri.toString());
|
||||
values.put(MessageColumns.SMS_MESSAGE_URI, smsMessageUri);
|
||||
values.put(MessageColumns.SMS_PRIORITY, mSmsPriority);
|
||||
values.put(MessageColumns.SMS_MESSAGE_SIZE, mSmsMessageSize);
|
||||
values.put(MessageColumns.MMS_EXPIRY, mMmsExpiry);
|
||||
values.put(MessageColumns.MMS_SUBJECT, mMmsSubject);
|
||||
values.put(MessageColumns.MMS_TRANSACTION_ID, mMmsTransactionId);
|
||||
values.put(MessageColumns.MMS_CONTENT_LOCATION, mMmsContentLocation);
|
||||
values.put(MessageColumns.RAW_TELEPHONY_STATUS, mRawStatus);
|
||||
values.put(MessageColumns.RETRY_START_TIMESTAMP, mRetryStartTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Note this is not thread safe so callers need to make sure they own the wrapper + statements
|
||||
* while they call this and use the returned value.
|
||||
*/
|
||||
public SQLiteStatement getInsertStatement(final DatabaseWrapper db) {
|
||||
final SQLiteStatement insert = db.getStatementInTransaction(
|
||||
DatabaseWrapper.INDEX_INSERT_MESSAGE, INSERT_MESSAGE_SQL);
|
||||
insert.clearBindings();
|
||||
insert.bindString(INDEX_CONVERSATION_ID, mConversationId);
|
||||
insert.bindString(INDEX_PARTICIPANT_ID, mParticipantId);
|
||||
insert.bindString(INDEX_SELF_ID, mSelfId);
|
||||
insert.bindLong(INDEX_SENT_TIMESTAMP, mSentTimestamp);
|
||||
insert.bindLong(INDEX_RECEIVED_TIMESTAMP, mReceivedTimestamp);
|
||||
insert.bindLong(INDEX_SEEN, mSeen ? 1 : 0);
|
||||
insert.bindLong(INDEX_READ, mRead ? 1 : 0);
|
||||
insert.bindLong(INDEX_PROTOCOL, mProtocol);
|
||||
insert.bindLong(INDEX_BUGLE_STATUS, mStatus);
|
||||
if (mSmsMessageUri != null) {
|
||||
insert.bindString(INDEX_SMS_MESSAGE_URI, mSmsMessageUri.toString());
|
||||
}
|
||||
insert.bindLong(INDEX_SMS_PRIORITY, mSmsPriority);
|
||||
insert.bindLong(INDEX_SMS_MESSAGE_SIZE, mSmsMessageSize);
|
||||
insert.bindLong(INDEX_MMS_EXPIRY, mMmsExpiry);
|
||||
if (mMmsSubject != null) {
|
||||
insert.bindString(INDEX_MMS_SUBJECT, mMmsSubject);
|
||||
}
|
||||
if (mMmsTransactionId != null) {
|
||||
insert.bindString(INDEX_MMS_TRANSACTION_ID, mMmsTransactionId);
|
||||
}
|
||||
if (mMmsContentLocation != null) {
|
||||
insert.bindString(INDEX_MMS_CONTENT_LOCATION, mMmsContentLocation);
|
||||
}
|
||||
insert.bindLong(INDEX_RAW_TELEPHONY_STATUS, mRawStatus);
|
||||
insert.bindLong(INDEX_RETRY_START_TIMESTAMP, mRetryStartTimestamp);
|
||||
return insert;
|
||||
}
|
||||
|
||||
public final String getMessageId() {
|
||||
return mMessageId;
|
||||
}
|
||||
|
||||
public final String getConversationId() {
|
||||
return mConversationId;
|
||||
}
|
||||
|
||||
public final String getParticipantId() {
|
||||
return mParticipantId;
|
||||
}
|
||||
|
||||
public final String getSelfId() {
|
||||
return mSelfId;
|
||||
}
|
||||
|
||||
public final long getSentTimeStamp() {
|
||||
return mSentTimestamp;
|
||||
}
|
||||
|
||||
public final long getReceivedTimeStamp() {
|
||||
return mReceivedTimestamp;
|
||||
}
|
||||
|
||||
public final String getFormattedReceivedTimeStamp() {
|
||||
return Dates.getMessageTimeString(mReceivedTimestamp).toString();
|
||||
}
|
||||
|
||||
public final int getProtocol() {
|
||||
return mProtocol;
|
||||
}
|
||||
|
||||
public final int getStatus() {
|
||||
return mStatus;
|
||||
}
|
||||
|
||||
public final Uri getSmsMessageUri() {
|
||||
return mSmsMessageUri;
|
||||
}
|
||||
|
||||
public final int getSmsPriority() {
|
||||
return mSmsPriority;
|
||||
}
|
||||
|
||||
public final long getSmsMessageSize() {
|
||||
return mSmsMessageSize;
|
||||
}
|
||||
|
||||
public final String getMmsSubject() {
|
||||
return mMmsSubject;
|
||||
}
|
||||
|
||||
public final void setMmsSubject(final String subject) {
|
||||
mMmsSubject = subject;
|
||||
}
|
||||
|
||||
public final String getMmsContentLocation() {
|
||||
return mMmsContentLocation;
|
||||
}
|
||||
|
||||
public final String getMmsTransactionId() {
|
||||
return mMmsTransactionId;
|
||||
}
|
||||
|
||||
public final boolean getMessageSeen() {
|
||||
return mSeen;
|
||||
}
|
||||
|
||||
public final long getMmsExpiry() {
|
||||
return mMmsExpiry;
|
||||
}
|
||||
|
||||
/**
|
||||
* For incoming MMS messages this returns the retrieve-status value
|
||||
* For sent MMS messages this returns the response-status value
|
||||
* See PduHeaders.java for possible values
|
||||
* Otherwise (SMS etc) this is RAW_TELEPHONY_STATUS_UNDEFINED
|
||||
*/
|
||||
public final int getRawTelephonyStatus() {
|
||||
return mRawStatus;
|
||||
}
|
||||
|
||||
public final void setMessageSeen(final boolean hasSeen) {
|
||||
mSeen = hasSeen;
|
||||
}
|
||||
|
||||
public final boolean getInResendWindow(final long now) {
|
||||
final long maxAgeToResend = BugleGservicesKeys.MESSAGE_RESEND_TIMEOUT_MS_DEFAULT;
|
||||
final long age = now - mRetryStartTimestamp;
|
||||
return age < maxAgeToResend;
|
||||
}
|
||||
|
||||
public final boolean getInDownloadWindow(final long now) {
|
||||
final long maxAgeToRedownload = BugleGservicesKeys.MESSAGE_DOWNLOAD_TIMEOUT_MS_DEFAULT;
|
||||
final long age = now - mRetryStartTimestamp;
|
||||
return age < maxAgeToRedownload;
|
||||
}
|
||||
|
||||
static boolean getShowDownloadMessage(final int status) {
|
||||
if (OsUtil.isSecondaryUser()) {
|
||||
// Secondary users can't download mms's. Mms's are downloaded by bugle running as the
|
||||
// primary user.
|
||||
return false;
|
||||
}
|
||||
// Should show option for manual download if status is manual download or failed
|
||||
// If debug is enabled, allow to download an expired or unavailable message.
|
||||
return (status == BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED ||
|
||||
status == BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD);
|
||||
}
|
||||
|
||||
public boolean canDownloadMessage() {
|
||||
if (OsUtil.isSecondaryUser()) {
|
||||
// Secondary users can't download mms's. Mms's are downloaded by bugle running as the
|
||||
// primary user.
|
||||
return false;
|
||||
}
|
||||
// Can download if status is retrying auto/manual downloading
|
||||
return (mStatus == BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD ||
|
||||
mStatus == BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD);
|
||||
}
|
||||
|
||||
public boolean canRedownloadMessage() {
|
||||
if (OsUtil.isSecondaryUser()) {
|
||||
// Secondary users can't download mms's. Mms's are downloaded by bugle running as the
|
||||
// primary user.
|
||||
return false;
|
||||
}
|
||||
// Can redownload if status is manual download not started or download failed
|
||||
// If debug is enabled, allow to download an expired or unavailable message.
|
||||
return (mStatus == BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED ||
|
||||
mStatus == BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD);
|
||||
}
|
||||
|
||||
static boolean getShowResendMessage(final int status) {
|
||||
// Should show option to resend if status is failed
|
||||
return (status == BUGLE_STATUS_OUTGOING_FAILED);
|
||||
}
|
||||
|
||||
static boolean getOneClickResendMessage(final int status, final int rawStatus) {
|
||||
// Should show option to resend if status is failed
|
||||
return (status == BUGLE_STATUS_OUTGOING_FAILED
|
||||
&& rawStatus == RAW_TELEPHONY_STATUS_UNDEFINED);
|
||||
}
|
||||
|
||||
public boolean canResendMessage() {
|
||||
// Manual retry allowed only from failed
|
||||
return (mStatus == BUGLE_STATUS_OUTGOING_FAILED);
|
||||
}
|
||||
|
||||
public boolean canSendMessage() {
|
||||
// Sending messages must be in yet_to_send or awaiting_retry state
|
||||
return (mStatus == BUGLE_STATUS_OUTGOING_YET_TO_SEND ||
|
||||
mStatus == BUGLE_STATUS_OUTGOING_AWAITING_RETRY);
|
||||
}
|
||||
|
||||
public final boolean getYetToSend() {
|
||||
return (mStatus == BUGLE_STATUS_OUTGOING_YET_TO_SEND);
|
||||
}
|
||||
|
||||
public final boolean getIsMms() {
|
||||
return mProtocol == MessageData.PROTOCOL_MMS
|
||||
|| mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION;
|
||||
}
|
||||
|
||||
public static boolean getIsSms(final int protocol) {
|
||||
return protocol == (MessageData.PROTOCOL_SMS);
|
||||
}
|
||||
|
||||
public final boolean getIsSms() {
|
||||
return getIsSms(mProtocol);
|
||||
}
|
||||
|
||||
public static boolean getIsIncoming(final int status) {
|
||||
return (status >= MessageData.BUGLE_STATUS_FIRST_INCOMING);
|
||||
}
|
||||
|
||||
public boolean getIsIncoming() {
|
||||
return getIsIncoming(mStatus);
|
||||
}
|
||||
|
||||
public long getRetryStartTimestamp() {
|
||||
return mRetryStartTimestamp;
|
||||
}
|
||||
|
||||
public final String getMessageText() {
|
||||
final String separator = System.lineSeparator();
|
||||
final StringBuilder text = new StringBuilder();
|
||||
for (final MessagePartData part : mParts) {
|
||||
if (!part.isAttachment() && !TextUtils.isEmpty(part.getText())) {
|
||||
if (text.length() > 0) {
|
||||
text.append(separator);
|
||||
}
|
||||
text.append(part.getText());
|
||||
}
|
||||
}
|
||||
return text.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes all captions from attachments and adds them as a prefix to the first text part or
|
||||
* appends a text part
|
||||
*/
|
||||
public final void consolidateText() {
|
||||
final String separator = System.lineSeparator();
|
||||
final StringBuilder captionText = new StringBuilder();
|
||||
MessagePartData firstTextPart = null;
|
||||
int firstTextPartIndex = -1;
|
||||
for (int i = 0; i < mParts.size(); i++) {
|
||||
final MessagePartData part = mParts.get(i);
|
||||
if (firstTextPart == null && !part.isAttachment()) {
|
||||
firstTextPart = part;
|
||||
firstTextPartIndex = i;
|
||||
}
|
||||
if (part.isAttachment() && !TextUtils.isEmpty(part.getText())) {
|
||||
if (captionText.length() > 0) {
|
||||
captionText.append(separator);
|
||||
}
|
||||
captionText.append(part.getText());
|
||||
}
|
||||
}
|
||||
|
||||
if (captionText.length() == 0) {
|
||||
// Nothing to consolidate
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstTextPart == null) {
|
||||
addPart(MessagePartData.createTextMessagePart(captionText.toString()));
|
||||
} else {
|
||||
final String partText = firstTextPart.getText();
|
||||
if (partText.length() > 0) {
|
||||
captionText.append(separator);
|
||||
captionText.append(partText);
|
||||
}
|
||||
mParts.set(firstTextPartIndex,
|
||||
MessagePartData.createTextMessagePart(captionText.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
public final MessagePartData getFirstAttachment() {
|
||||
for (final MessagePartData part : mParts) {
|
||||
if (part.isAttachment()) {
|
||||
return part;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the messageId for this message.
|
||||
* Can be used to reset the messageId prior to persisting (which will assign a new messageId)
|
||||
* or can be called on a message that does not yet have a valid messageId to set it.
|
||||
*/
|
||||
public void updateMessageId(final String messageId) {
|
||||
Assert.isTrue(TextUtils.isEmpty(messageId) || TextUtils.isEmpty(mMessageId));
|
||||
mMessageId = messageId;
|
||||
|
||||
// TODO : This should probably also call updateMessageId on the message parts. We
|
||||
// may also want to make messages effectively immutable once they have a valid message id.
|
||||
}
|
||||
|
||||
public final void updateSendingMessage(final String conversationId, final Uri messageUri,
|
||||
final long timestamp) {
|
||||
mConversationId = conversationId;
|
||||
mSmsMessageUri = messageUri;
|
||||
mRead = true;
|
||||
mSeen = true;
|
||||
mReceivedTimestamp = timestamp;
|
||||
mSentTimestamp = timestamp;
|
||||
mStatus = BUGLE_STATUS_OUTGOING_YET_TO_SEND;
|
||||
mRetryStartTimestamp = timestamp;
|
||||
}
|
||||
|
||||
public final void markMessageManualResend(final long timestamp) {
|
||||
// Manual send updates timestamp and transitions back to initial sending status.
|
||||
mReceivedTimestamp = timestamp;
|
||||
mSentTimestamp = timestamp;
|
||||
mStatus = BUGLE_STATUS_OUTGOING_SENDING;
|
||||
}
|
||||
|
||||
public final void markMessageSending(final long timestamp) {
|
||||
// Initial send
|
||||
mStatus = BUGLE_STATUS_OUTGOING_SENDING;
|
||||
mSentTimestamp = timestamp;
|
||||
}
|
||||
|
||||
public final void markMessageResending(final long timestamp) {
|
||||
// Auto resend of message
|
||||
mStatus = BUGLE_STATUS_OUTGOING_RESENDING;
|
||||
mSentTimestamp = timestamp;
|
||||
}
|
||||
|
||||
public final void markMessageSent(final long timestamp) {
|
||||
mSentTimestamp = timestamp;
|
||||
mStatus = BUGLE_STATUS_OUTGOING_COMPLETE;
|
||||
}
|
||||
|
||||
public final void markMessageFailed(final long timestamp) {
|
||||
mSentTimestamp = timestamp;
|
||||
mStatus = BUGLE_STATUS_OUTGOING_FAILED;
|
||||
}
|
||||
|
||||
public final void markMessageFailedEmergencyNumber(final long timestamp) {
|
||||
mSentTimestamp = timestamp;
|
||||
mStatus = BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER;
|
||||
}
|
||||
|
||||
public final void markMessageNotSent(final long timestamp) {
|
||||
mSentTimestamp = timestamp;
|
||||
mStatus = BUGLE_STATUS_OUTGOING_AWAITING_RETRY;
|
||||
}
|
||||
|
||||
public final void updateSizesForImageParts() {
|
||||
for (final MessagePartData part : getParts()) {
|
||||
part.decodeAndSaveSizeIfImage(false /* saveToStorage */);
|
||||
}
|
||||
}
|
||||
|
||||
public final void setRawTelephonyStatus(final int rawStatus) {
|
||||
mRawStatus = rawStatus;
|
||||
}
|
||||
|
||||
public boolean hasContent() {
|
||||
return !TextUtils.isEmpty(mMmsSubject) ||
|
||||
getFirstAttachment() != null ||
|
||||
!TextUtils.isEmpty(getMessageText());
|
||||
}
|
||||
|
||||
public final void bindSelfId(final String selfId) {
|
||||
mSelfId = selfId;
|
||||
}
|
||||
|
||||
public final void bindParticipantId(final String participantId) {
|
||||
mParticipantId = participantId;
|
||||
}
|
||||
|
||||
protected MessageData(final Parcel in) {
|
||||
mMessageId = in.readString();
|
||||
mConversationId = in.readString();
|
||||
mParticipantId = in.readString();
|
||||
mSelfId = in.readString();
|
||||
mSentTimestamp = in.readLong();
|
||||
mReceivedTimestamp = in.readLong();
|
||||
mSeen = (in.readInt() != 0);
|
||||
mRead = (in.readInt() != 0);
|
||||
mProtocol = in.readInt();
|
||||
mStatus = in.readInt();
|
||||
final String smsMessageUri = in.readString();
|
||||
mSmsMessageUri = (smsMessageUri == null ? null : Uri.parse(smsMessageUri));
|
||||
mSmsPriority = in.readInt();
|
||||
mSmsMessageSize = in.readLong();
|
||||
mMmsExpiry = in.readLong();
|
||||
mMmsSubject = in.readString();
|
||||
mMmsTransactionId = in.readString();
|
||||
mMmsContentLocation = in.readString();
|
||||
mRawStatus = in.readInt();
|
||||
mRetryStartTimestamp = in.readLong();
|
||||
|
||||
// Read parts
|
||||
mParts = new ArrayList<>();
|
||||
final int partCount = in.readInt();
|
||||
for (int i = 0; i < partCount; i++) {
|
||||
mParts.add(in.readParcelable(MessagePartData.class.getClassLoader(),
|
||||
MessagePartData.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(mMessageId);
|
||||
dest.writeString(mConversationId);
|
||||
dest.writeString(mParticipantId);
|
||||
dest.writeString(mSelfId);
|
||||
dest.writeLong(mSentTimestamp);
|
||||
dest.writeLong(mReceivedTimestamp);
|
||||
dest.writeInt(mRead ? 1 : 0);
|
||||
dest.writeInt(mSeen ? 1 : 0);
|
||||
dest.writeInt(mProtocol);
|
||||
dest.writeInt(mStatus);
|
||||
final String smsMessageUri = (mSmsMessageUri == null) ? null : mSmsMessageUri.toString();
|
||||
dest.writeString(smsMessageUri);
|
||||
dest.writeInt(mSmsPriority);
|
||||
dest.writeLong(mSmsMessageSize);
|
||||
dest.writeLong(mMmsExpiry);
|
||||
dest.writeString(mMmsSubject);
|
||||
dest.writeString(mMmsTransactionId);
|
||||
dest.writeString(mMmsContentLocation);
|
||||
dest.writeInt(mRawStatus);
|
||||
dest.writeLong(mRetryStartTimestamp);
|
||||
|
||||
// Write parts
|
||||
dest.writeInt(mParts.size());
|
||||
for (final MessagePartData messagePartData : mParts) {
|
||||
dest.writeParcelable(messagePartData, flags);
|
||||
}
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<MessageData> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public MessageData createFromParcel(final Parcel in) {
|
||||
return new MessageData(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageData[] newArray(final int size) {
|
||||
return new MessageData[size];
|
||||
}
|
||||
};
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString(mMessageId, mParts);
|
||||
}
|
||||
|
||||
public static String toString(String messageId, List<MessagePartData> parts) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (messageId != null) {
|
||||
sb.append(messageId);
|
||||
sb.append(": ");
|
||||
}
|
||||
for (MessagePartData part : parts) {
|
||||
sb.append(part.toString());
|
||||
sb.append(" ");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.v7.mms.pdu.ContentType;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MediaScratchFileProvider;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.action.UpdateMessagePartSizeAction;
|
||||
import com.android.messaging.datamodel.media.ImageRequest;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
|
||||
import com.android.messaging.util.GifTranscoder;
|
||||
import com.android.messaging.util.ImageUtils;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.UriUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* Represents a single message part. Messages consist of one or more parts which may contain
|
||||
* either text or media.
|
||||
*/
|
||||
public class MessagePartData implements Parcelable {
|
||||
public static final int UNSPECIFIED_SIZE = MessagingContentProvider.UNSPECIFIED_SIZE;
|
||||
|
||||
public static final String[] ACCEPTABLE_GALLERY_MEDIA_TYPES =
|
||||
new String[] {
|
||||
// Acceptable image types
|
||||
ContentType.IMAGE_JPEG, ContentType.IMAGE_JPG, ContentType.IMAGE_PNG,
|
||||
ContentType.IMAGE_GIF, ContentType.IMAGE_WBMP, ContentType.IMAGE_X_MS_BMP,
|
||||
// Acceptable video types
|
||||
ContentType.VIDEO_3GP, ContentType.VIDEO_3GPP, ContentType.VIDEO_3G2,
|
||||
ContentType.VIDEO_H263, ContentType.VIDEO_M4V, ContentType.VIDEO_MP4,
|
||||
ContentType.VIDEO_MPEG, ContentType.VIDEO_MPEG4, ContentType.VIDEO_WEBM,
|
||||
// Acceptable audio types
|
||||
ContentType.AUDIO_MP3, ContentType.AUDIO_MP4, ContentType.AUDIO_MIDI,
|
||||
ContentType.AUDIO_MID, ContentType.AUDIO_AMR, ContentType.AUDIO_X_WAV,
|
||||
ContentType.AUDIO_AAC, ContentType.AUDIO_X_MIDI, ContentType.AUDIO_X_MID,
|
||||
ContentType.AUDIO_X_MP3
|
||||
};
|
||||
|
||||
private static final String[] sProjection = {
|
||||
PartColumns._ID,
|
||||
PartColumns.MESSAGE_ID,
|
||||
PartColumns.TEXT,
|
||||
PartColumns.CONTENT_URI,
|
||||
PartColumns.CONTENT_TYPE,
|
||||
PartColumns.WIDTH,
|
||||
PartColumns.HEIGHT,
|
||||
};
|
||||
|
||||
private static final int INDEX_ID = 0;
|
||||
private static final int INDEX_MESSAGE_ID = 1;
|
||||
private static final int INDEX_TEXT = 2;
|
||||
private static final int INDEX_CONTENT_URI = 3;
|
||||
private static final int INDEX_CONTENT_TYPE = 4;
|
||||
private static final int INDEX_WIDTH = 5;
|
||||
private static final int INDEX_HEIGHT = 6;
|
||||
// This isn't part of the projection
|
||||
private static final int INDEX_CONVERSATION_ID = 7;
|
||||
|
||||
// SQL statement to insert a "complete" message part row (columns based on projection above).
|
||||
private static final String INSERT_MESSAGE_PART_SQL =
|
||||
"INSERT INTO " + DatabaseHelper.PARTS_TABLE + " ( "
|
||||
+ TextUtils.join(",", Arrays.copyOfRange(sProjection, 1, INDEX_CONVERSATION_ID))
|
||||
+ ", " + PartColumns.CONVERSATION_ID
|
||||
+ ") VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
// Used for stuff that's ignored or arbitrarily compressed.
|
||||
private static final long NO_MINIMUM_SIZE = 0;
|
||||
|
||||
private String mPartId;
|
||||
private String mMessageId;
|
||||
private String mText;
|
||||
private Uri mContentUri;
|
||||
private String mContentType;
|
||||
private int mWidth;
|
||||
private int mHeight;
|
||||
// This kind of part can only be attached once and with no other attachment
|
||||
private boolean mSinglePartOnly;
|
||||
|
||||
/** Transient data: true if destroy was already called */
|
||||
private boolean mDestroyed;
|
||||
|
||||
/**
|
||||
* Create an "empty" message part
|
||||
*/
|
||||
protected MessagePartData() {
|
||||
this(null, null, UNSPECIFIED_SIZE, UNSPECIFIED_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a populated text message part
|
||||
*/
|
||||
protected MessagePartData(final String messageText) {
|
||||
this(null, messageText, ContentType.TEXT_PLAIN, null, UNSPECIFIED_SIZE, UNSPECIFIED_SIZE,
|
||||
false /*singlePartOnly*/);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a populated attachment message part
|
||||
*/
|
||||
protected MessagePartData(final String contentType, final Uri contentUri,
|
||||
final int width, final int height) {
|
||||
this(null, null, contentType, contentUri, width, height, false /*singlePartOnly*/);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a populated attachment message part, with additional caption text
|
||||
*/
|
||||
protected MessagePartData(final String messageText, final String contentType,
|
||||
final Uri contentUri, final int width, final int height) {
|
||||
this(null, messageText, contentType, contentUri, width, height, false /*singlePartOnly*/);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a populated attachment message part, with additional caption text, single part only
|
||||
*/
|
||||
protected MessagePartData(final String messageText, final String contentType,
|
||||
final Uri contentUri, final int width, final int height, final boolean singlePartOnly) {
|
||||
this(null, messageText, contentType, contentUri, width, height, singlePartOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a populated message part
|
||||
*/
|
||||
private MessagePartData(final String messageId, final String messageText,
|
||||
final String contentType, final Uri contentUri, final int width, final int height,
|
||||
final boolean singlePartOnly) {
|
||||
mMessageId = messageId;
|
||||
mText = messageText;
|
||||
mContentType = contentType;
|
||||
mContentUri = contentUri;
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
mSinglePartOnly = singlePartOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a "text" message part
|
||||
*/
|
||||
public static MessagePartData createTextMessagePart(final String messageText) {
|
||||
return new MessagePartData(messageText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a "media" message part
|
||||
*/
|
||||
public static MessagePartData createMediaMessagePart(final String contentType,
|
||||
final Uri contentUri, final int width, final int height) {
|
||||
return new MessagePartData(contentType, contentUri, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a "media" message part with caption
|
||||
*/
|
||||
public static MessagePartData createMediaMessagePart(final String caption,
|
||||
final String contentType, final Uri contentUri, final int width, final int height) {
|
||||
return new MessagePartData(null, caption, contentType, contentUri, width, height,
|
||||
false /*singlePartOnly*/
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty "text" message part
|
||||
*/
|
||||
public static MessagePartData createEmptyMessagePart() {
|
||||
return new MessagePartData("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new message part reading from the cursor
|
||||
*/
|
||||
public static MessagePartData createFromCursor(final Cursor cursor) {
|
||||
final MessagePartData part = new MessagePartData();
|
||||
part.bind(cursor);
|
||||
return part;
|
||||
}
|
||||
|
||||
public static String[] getProjection() {
|
||||
return sProjection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the part id.
|
||||
* Can be used to reset the partId just prior to persisting (which will assign a new partId)
|
||||
* or can be called on a part that does not yet have a valid part id to set it.
|
||||
*/
|
||||
public void updatePartId(final String partId) {
|
||||
Assert.isTrue(TextUtils.isEmpty(partId) || TextUtils.isEmpty(mPartId));
|
||||
mPartId = partId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the messageId for the part.
|
||||
* Can be used to reset the messageId prior to persisting (which will assign a new messageId)
|
||||
* or can be called on a part that does not yet have a valid messageId to set it.
|
||||
*/
|
||||
public void updateMessageId(final String messageId) {
|
||||
Assert.isTrue(TextUtils.isEmpty(messageId) || TextUtils.isEmpty(mMessageId));
|
||||
mMessageId = messageId;
|
||||
}
|
||||
|
||||
protected static String getMessageId(final Cursor cursor) {
|
||||
return cursor.getString(INDEX_MESSAGE_ID);
|
||||
}
|
||||
|
||||
protected void bind(final Cursor cursor) {
|
||||
mPartId = cursor.getString(INDEX_ID);
|
||||
mMessageId = cursor.getString(INDEX_MESSAGE_ID);
|
||||
mText = cursor.getString(INDEX_TEXT);
|
||||
mContentUri = UriUtil.uriFromString(cursor.getString(INDEX_CONTENT_URI));
|
||||
mContentType = cursor.getString(INDEX_CONTENT_TYPE);
|
||||
mWidth = cursor.getInt(INDEX_WIDTH);
|
||||
mHeight = cursor.getInt(INDEX_HEIGHT);
|
||||
}
|
||||
|
||||
public final void populate(final ContentValues values) {
|
||||
// Must have a valid messageId on a part
|
||||
Assert.isTrue(!TextUtils.isEmpty(mMessageId));
|
||||
values.put(PartColumns.MESSAGE_ID, mMessageId);
|
||||
values.put(PartColumns.TEXT, mText);
|
||||
values.put(PartColumns.CONTENT_URI, UriUtil.stringFromUri(mContentUri));
|
||||
values.put(PartColumns.CONTENT_TYPE, mContentType);
|
||||
if (mWidth != UNSPECIFIED_SIZE) {
|
||||
values.put(PartColumns.WIDTH, mWidth);
|
||||
}
|
||||
if (mHeight != UNSPECIFIED_SIZE) {
|
||||
values.put(PartColumns.HEIGHT, mHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Note this is not thread safe so callers need to make sure they own the wrapper + statements
|
||||
* while they call this and use the returned value.
|
||||
*/
|
||||
public SQLiteStatement getInsertStatement(final DatabaseWrapper db,
|
||||
final String conversationId) {
|
||||
final SQLiteStatement insert = db.getStatementInTransaction(
|
||||
DatabaseWrapper.INDEX_INSERT_MESSAGE_PART, INSERT_MESSAGE_PART_SQL);
|
||||
insert.clearBindings();
|
||||
insert.bindString(INDEX_MESSAGE_ID, mMessageId);
|
||||
if (mText != null) {
|
||||
insert.bindString(INDEX_TEXT, mText);
|
||||
}
|
||||
if (mContentUri != null) {
|
||||
insert.bindString(INDEX_CONTENT_URI, mContentUri.toString());
|
||||
}
|
||||
if (mContentType != null) {
|
||||
insert.bindString(INDEX_CONTENT_TYPE, mContentType);
|
||||
}
|
||||
insert.bindLong(INDEX_WIDTH, mWidth);
|
||||
insert.bindLong(INDEX_HEIGHT, mHeight);
|
||||
insert.bindString(INDEX_CONVERSATION_ID, conversationId);
|
||||
return insert;
|
||||
}
|
||||
|
||||
public final String getPartId() {
|
||||
return mPartId;
|
||||
}
|
||||
|
||||
public final String getMessageId() {
|
||||
return mMessageId;
|
||||
}
|
||||
|
||||
public final String getText() {
|
||||
return mText;
|
||||
}
|
||||
|
||||
public final Uri getContentUri() {
|
||||
return mContentUri;
|
||||
}
|
||||
|
||||
public boolean isAttachment() {
|
||||
return mContentUri != null;
|
||||
}
|
||||
|
||||
public boolean isText() {
|
||||
return ContentType.isTextType(mContentType);
|
||||
}
|
||||
|
||||
public boolean isImage() {
|
||||
return ContentType.isImageType(mContentType);
|
||||
}
|
||||
|
||||
public boolean isMedia() {
|
||||
return ContentType.isMediaType(mContentType);
|
||||
}
|
||||
|
||||
public boolean isVCard() {
|
||||
return ContentType.isVCardType(mContentType);
|
||||
}
|
||||
|
||||
public boolean isAudio() {
|
||||
return ContentType.isAudioType(mContentType);
|
||||
}
|
||||
|
||||
public boolean isVideo() {
|
||||
return ContentType.isVideoType(mContentType);
|
||||
}
|
||||
|
||||
public final String getContentType() {
|
||||
return mContentType;
|
||||
}
|
||||
|
||||
public final int getWidth() {
|
||||
return mWidth;
|
||||
}
|
||||
|
||||
public final int getHeight() {
|
||||
return mHeight;
|
||||
}
|
||||
|
||||
public static boolean isSupportedMediaType(final String contentType) {
|
||||
return ContentType.isVCardType(contentType)
|
||||
|| Arrays.asList(ACCEPTABLE_GALLERY_MEDIA_TYPES).contains(contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return true if this part can only exist by itself, with no other attachments
|
||||
*/
|
||||
public boolean getSinglePartOnly() {
|
||||
return mSinglePartOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected MessagePartData(final Parcel in) {
|
||||
mMessageId = in.readString();
|
||||
mText = in.readString();
|
||||
mContentUri = UriUtil.uriFromString(in.readString());
|
||||
mContentType = in.readString();
|
||||
mWidth = in.readInt();
|
||||
mHeight = in.readInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
Assert.isTrue(!mDestroyed);
|
||||
dest.writeString(mMessageId);
|
||||
dest.writeString(mText);
|
||||
dest.writeString(UriUtil.stringFromUri(mContentUri));
|
||||
dest.writeString(mContentType);
|
||||
dest.writeInt(mWidth);
|
||||
dest.writeInt(mHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(o instanceof MessagePartData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MessagePartData lhs = (MessagePartData) o;
|
||||
return mWidth == lhs.mWidth && mHeight == lhs.mHeight &&
|
||||
TextUtils.equals(mMessageId, lhs.mMessageId) &&
|
||||
TextUtils.equals(mText, lhs.mText) &&
|
||||
TextUtils.equals(mContentType, lhs.mContentType) &&
|
||||
(mContentUri == null ? lhs.mContentUri == null
|
||||
: mContentUri.equals(lhs.mContentUri));
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
int result = 17;
|
||||
result = 31 * result + mWidth;
|
||||
result = 31 * result + mHeight;
|
||||
result = 31 * result + (mMessageId == null ? 0 : mMessageId.hashCode());
|
||||
result = 31 * result + (mText == null ? 0 : mText.hashCode());
|
||||
result = 31 * result + (mContentType == null ? 0 : mContentType.hashCode());
|
||||
result = 31 * result + (mContentUri == null ? 0 : mContentUri.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<MessagePartData> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public MessagePartData createFromParcel(final Parcel in) {
|
||||
return new MessagePartData(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessagePartData[] newArray(final int size) {
|
||||
return new MessagePartData[size];
|
||||
}
|
||||
};
|
||||
|
||||
protected Uri shouldDestroy() {
|
||||
// We should never double-destroy.
|
||||
Assert.isTrue(!mDestroyed);
|
||||
mDestroyed = true;
|
||||
Uri contentUri = mContentUri;
|
||||
mContentUri = null;
|
||||
mContentType = null;
|
||||
// Only destroy the image if it's staged in our scratch space.
|
||||
if (!MediaScratchFileProvider.isMediaScratchSpaceUri(contentUri)) {
|
||||
contentUri = null;
|
||||
}
|
||||
return contentUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* If application owns content associated with this part delete it (on background thread)
|
||||
*/
|
||||
public void destroyAsync() {
|
||||
final Uri contentUri = shouldDestroy();
|
||||
if (contentUri != null) {
|
||||
Executors.newSingleThreadExecutor().execute(() ->
|
||||
Factory.get().getApplicationContext().getContentResolver().delete(
|
||||
contentUri, null, null));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If application owns content associated with this part delete it
|
||||
*/
|
||||
public void destroySync() {
|
||||
final Uri contentUri = shouldDestroy();
|
||||
if (contentUri != null) {
|
||||
Factory.get().getApplicationContext().getContentResolver().delete(
|
||||
contentUri, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is an image part, decode the image header and potentially save the size to the db.
|
||||
*/
|
||||
public void decodeAndSaveSizeIfImage(final boolean saveToStorage) {
|
||||
if (isImage()) {
|
||||
final Rect imageSize = ImageUtils.decodeImageBounds(
|
||||
Factory.get().getApplicationContext(), mContentUri);
|
||||
if (imageSize.width() != ImageRequest.UNSPECIFIED_SIZE &&
|
||||
imageSize.height() != ImageRequest.UNSPECIFIED_SIZE) {
|
||||
mWidth = imageSize.width();
|
||||
mHeight = imageSize.height();
|
||||
if (saveToStorage) {
|
||||
UpdateMessagePartSizeAction.updateSize(mPartId, mWidth, mHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the minimum size that this MessagePartData could be compressed/downsampled/encoded
|
||||
* before sending to meet the maximum message size imposed by the carriers. This is used to
|
||||
* determine right before sending a message whether a message could possibly be sent. If not
|
||||
* then the user is given a chance to unselect some/all of the attachments.
|
||||
*
|
||||
* TODO: computing the minimum size could be expensive. Should we cache the
|
||||
* computed value in db to be retrieved later?
|
||||
*
|
||||
* @return the carrier-independent minimum size, in bytes.
|
||||
*/
|
||||
@DoesNotRunOnMainThread
|
||||
public long getMinimumSizeInBytesForSending() {
|
||||
Assert.isNotMainThread();
|
||||
if (!isAttachment()) {
|
||||
// No limit is imposed on non-attachment part (i.e. plain text), so treat it as zero.
|
||||
return NO_MINIMUM_SIZE;
|
||||
} else if (isImage()) {
|
||||
// GIFs are resized by the native transcoder (exposed by GifTranscoder).
|
||||
if (ImageUtils.isGif(mContentType, mContentUri)) {
|
||||
final long originalImageSize = UriUtil.getContentSize(mContentUri);
|
||||
// Wish we could save the size here, but we don't have a part id yet
|
||||
decodeAndSaveSizeIfImage(false /* saveToStorage */);
|
||||
return GifTranscoder.canBeTranscoded(mWidth, mHeight) ?
|
||||
GifTranscoder.estimateFileSizeAfterTranscode(originalImageSize)
|
||||
: originalImageSize;
|
||||
}
|
||||
// Other images should be arbitrarily resized by ImageResizer before sending.
|
||||
return MmsUtils.MIN_IMAGE_BYTE_SIZE;
|
||||
} else if (isMedia()) {
|
||||
// We can't compress attachments except images.
|
||||
return UriUtil.getContentSize(mContentUri);
|
||||
} else {
|
||||
// This is some unknown media type that we don't know how to handle. Log an error
|
||||
// and try sending it anyway.
|
||||
LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "Unknown attachment type " + getContentType());
|
||||
return NO_MINIMUM_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
if (isText()) {
|
||||
return LogUtil.sanitizePII(getText());
|
||||
} else {
|
||||
return getContentType() + " (" + getContentUri() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return true if this part can only exist by itself, with no other attachments
|
||||
*/
|
||||
public boolean isSinglePartOnly() {
|
||||
return mSinglePartOnly;
|
||||
}
|
||||
|
||||
public void setSinglePartOnly(final boolean isSinglePartOnly) {
|
||||
mSinglePartOnly = isSinglePartOnly;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,596 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.telephony.SubscriptionInfo;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import android.support.v7.mms.MmsManager;
|
||||
import androidx.collection.ArrayMap;
|
||||
|
||||
import com.android.ex.chips.RecipientEntry;
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.sms.MmsSmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
import com.android.messaging.util.TextUtil;
|
||||
|
||||
/**
|
||||
* A class that encapsulates all of the data for a specific participant in a conversation.
|
||||
*/
|
||||
public class ParticipantData implements Parcelable {
|
||||
|
||||
private static final ArrayMap<Integer, String> sSubIdtoParticipantIdCache = new ArrayMap<>();
|
||||
|
||||
// We always use -1 as default/invalid sub id although system may give us anything negative
|
||||
public static final int DEFAULT_SELF_SUB_ID = MmsManager.DEFAULT_SUB_ID;
|
||||
|
||||
// This needs to be something apart from valid or DEFAULT_SELF_SUB_ID
|
||||
public static final int OTHER_THAN_SELF_SUB_ID = DEFAULT_SELF_SUB_ID - 1;
|
||||
|
||||
// Active slot ids are non-negative. Using -1 to designate to inactive self participants.
|
||||
public static final int INVALID_SLOT_ID = -1;
|
||||
|
||||
// TODO: may make sense to move this to common place?
|
||||
public static final long PARTICIPANT_CONTACT_ID_NOT_RESOLVED = -1;
|
||||
public static final long PARTICIPANT_CONTACT_ID_NOT_FOUND = -2;
|
||||
|
||||
public static class ParticipantsQuery {
|
||||
public static final String[] PROJECTION = new String[] {
|
||||
ParticipantColumns._ID,
|
||||
ParticipantColumns.SUB_ID,
|
||||
ParticipantColumns.SIM_SLOT_ID,
|
||||
ParticipantColumns.NORMALIZED_DESTINATION,
|
||||
ParticipantColumns.SEND_DESTINATION,
|
||||
ParticipantColumns.DISPLAY_DESTINATION,
|
||||
ParticipantColumns.FULL_NAME,
|
||||
ParticipantColumns.FIRST_NAME,
|
||||
ParticipantColumns.PROFILE_PHOTO_URI,
|
||||
ParticipantColumns.CONTACT_ID,
|
||||
ParticipantColumns.LOOKUP_KEY,
|
||||
ParticipantColumns.BLOCKED,
|
||||
ParticipantColumns.SUBSCRIPTION_COLOR,
|
||||
ParticipantColumns.SUBSCRIPTION_NAME,
|
||||
ParticipantColumns.CONTACT_DESTINATION,
|
||||
};
|
||||
|
||||
public static final int INDEX_ID = 0;
|
||||
public static final int INDEX_SUB_ID = 1;
|
||||
public static final int INDEX_SIM_SLOT_ID = 2;
|
||||
public static final int INDEX_NORMALIZED_DESTINATION = 3;
|
||||
public static final int INDEX_SEND_DESTINATION = 4;
|
||||
public static final int INDEX_DISPLAY_DESTINATION = 5;
|
||||
public static final int INDEX_FULL_NAME = 6;
|
||||
public static final int INDEX_FIRST_NAME = 7;
|
||||
public static final int INDEX_PROFILE_PHOTO_URI = 8;
|
||||
public static final int INDEX_CONTACT_ID = 9;
|
||||
public static final int INDEX_LOOKUP_KEY = 10;
|
||||
public static final int INDEX_BLOCKED = 11;
|
||||
public static final int INDEX_SUBSCRIPTION_COLOR = 12;
|
||||
public static final int INDEX_SUBSCRIPTION_NAME = 13;
|
||||
public static final int INDEX_CONTACT_DESTINATION = 14;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The MMS unknown sender participant entity
|
||||
*/
|
||||
public static String getUnknownSenderDestination() {
|
||||
// This is a hard coded string rather than a localized one because we don't want it to
|
||||
// change when you change locale.
|
||||
return "\u02BCUNKNOWN_SENDER!\u02BC";
|
||||
}
|
||||
|
||||
private String mParticipantId;
|
||||
private int mSubId;
|
||||
private int mSlotId;
|
||||
private String mNormalizedDestination;
|
||||
private String mSendDestination;
|
||||
private String mDisplayDestination;
|
||||
private String mContactDestination;
|
||||
private String mFullName;
|
||||
private String mFirstName;
|
||||
private String mProfilePhotoUri;
|
||||
private long mContactId;
|
||||
private String mLookupKey;
|
||||
private int mSubscriptionColor;
|
||||
private String mSubscriptionName;
|
||||
private boolean mIsEmailAddress;
|
||||
private boolean mBlocked;
|
||||
|
||||
// Don't call constructor directly
|
||||
private ParticipantData() {
|
||||
}
|
||||
|
||||
public static ParticipantData getFromCursor(final Cursor cursor) {
|
||||
final ParticipantData pd = new ParticipantData();
|
||||
pd.mParticipantId = cursor.getString(ParticipantsQuery.INDEX_ID);
|
||||
pd.mSubId = cursor.getInt(ParticipantsQuery.INDEX_SUB_ID);
|
||||
pd.mSlotId = cursor.getInt(ParticipantsQuery.INDEX_SIM_SLOT_ID);
|
||||
pd.mNormalizedDestination = cursor.getString(
|
||||
ParticipantsQuery.INDEX_NORMALIZED_DESTINATION);
|
||||
pd.mSendDestination = cursor.getString(ParticipantsQuery.INDEX_SEND_DESTINATION);
|
||||
pd.mDisplayDestination = cursor.getString(ParticipantsQuery.INDEX_DISPLAY_DESTINATION);
|
||||
pd.mContactDestination = cursor.getString(ParticipantsQuery.INDEX_CONTACT_DESTINATION);
|
||||
pd.mFullName = cursor.getString(ParticipantsQuery.INDEX_FULL_NAME);
|
||||
pd.mFirstName = cursor.getString(ParticipantsQuery.INDEX_FIRST_NAME);
|
||||
pd.mProfilePhotoUri = cursor.getString(ParticipantsQuery.INDEX_PROFILE_PHOTO_URI);
|
||||
pd.mContactId = cursor.getLong(ParticipantsQuery.INDEX_CONTACT_ID);
|
||||
pd.mLookupKey = cursor.getString(ParticipantsQuery.INDEX_LOOKUP_KEY);
|
||||
pd.mIsEmailAddress = MmsSmsUtils.isEmailAddress(pd.mSendDestination);
|
||||
pd.mBlocked = cursor.getInt(ParticipantsQuery.INDEX_BLOCKED) != 0;
|
||||
pd.mSubscriptionColor = cursor.getInt(ParticipantsQuery.INDEX_SUBSCRIPTION_COLOR);
|
||||
pd.mSubscriptionName = cursor.getString(ParticipantsQuery.INDEX_SUBSCRIPTION_NAME);
|
||||
pd.maybeSetupUnknownSender();
|
||||
return pd;
|
||||
}
|
||||
|
||||
public static ParticipantData getFromId(final DatabaseWrapper dbWrapper,
|
||||
final String participantId) {
|
||||
try (Cursor cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
|
||||
ParticipantsQuery.PROJECTION,
|
||||
ParticipantColumns._ID + " =?",
|
||||
new String[]{participantId}, null, null, null)) {
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
return ParticipantData.getFromCursor(cursor);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ParticipantData getFromRecipientEntry(final RecipientEntry recipientEntry) {
|
||||
final ParticipantData pd = new ParticipantData();
|
||||
pd.mParticipantId = null;
|
||||
pd.mSubId = OTHER_THAN_SELF_SUB_ID;
|
||||
pd.mSlotId = INVALID_SLOT_ID;
|
||||
pd.mSendDestination = TextUtil.replaceUnicodeDigits(recipientEntry.getDestination());
|
||||
pd.mIsEmailAddress = MmsSmsUtils.isEmailAddress(pd.mSendDestination);
|
||||
pd.mNormalizedDestination = pd.mIsEmailAddress ?
|
||||
pd.mSendDestination :
|
||||
PhoneUtils.getDefault().getCanonicalBySystemLocale(pd.mSendDestination);
|
||||
pd.mDisplayDestination = pd.mIsEmailAddress ?
|
||||
pd.mNormalizedDestination :
|
||||
PhoneUtils.getDefault().formatForDisplay(pd.mNormalizedDestination);
|
||||
pd.mFullName = recipientEntry.getDisplayName();
|
||||
pd.mFirstName = null;
|
||||
pd.mProfilePhotoUri = (recipientEntry.getPhotoThumbnailUri() == null) ? null :
|
||||
recipientEntry.getPhotoThumbnailUri().toString();
|
||||
pd.mContactId = recipientEntry.getContactId();
|
||||
if (pd.mContactId < 0) {
|
||||
// ParticipantData only supports real contact ids (>=0) based on faith that the contacts
|
||||
// provider will continue to only use non-negative ids. The UI uses contactId < 0 for
|
||||
// special handling. We convert those to 'not resolved'
|
||||
pd.mContactId = PARTICIPANT_CONTACT_ID_NOT_RESOLVED;
|
||||
}
|
||||
pd.mLookupKey = recipientEntry.getLookupKey();
|
||||
pd.mBlocked = false;
|
||||
pd.mSubscriptionColor = Color.TRANSPARENT;
|
||||
pd.mSubscriptionName = null;
|
||||
pd.maybeSetupUnknownSender();
|
||||
return pd;
|
||||
}
|
||||
|
||||
// Shared code for getFromRawPhoneBySystemLocale and getFromRawPhoneBySimLocale
|
||||
private static ParticipantData getFromRawPhone(final String phoneNumber) {
|
||||
Assert.isTrue(phoneNumber != null);
|
||||
final ParticipantData pd = new ParticipantData();
|
||||
pd.mParticipantId = null;
|
||||
pd.mSubId = OTHER_THAN_SELF_SUB_ID;
|
||||
pd.mSlotId = INVALID_SLOT_ID;
|
||||
pd.mSendDestination = TextUtil.replaceUnicodeDigits(phoneNumber);
|
||||
pd.mIsEmailAddress = MmsSmsUtils.isEmailAddress(pd.mSendDestination);
|
||||
pd.mFullName = null;
|
||||
pd.mFirstName = null;
|
||||
pd.mProfilePhotoUri = null;
|
||||
pd.mContactId = PARTICIPANT_CONTACT_ID_NOT_RESOLVED;
|
||||
pd.mLookupKey = null;
|
||||
pd.mBlocked = false;
|
||||
pd.mSubscriptionColor = Color.TRANSPARENT;
|
||||
pd.mSubscriptionName = null;
|
||||
return pd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance from a raw phone number and using system locale to normalize it.
|
||||
*
|
||||
* Use this when creating a participant that is for displaying UI and not associated
|
||||
* with a specific SIM. For example, when creating a conversation using user entered
|
||||
* phone number.
|
||||
*
|
||||
* @param phoneNumber The raw phone number
|
||||
* @return instance
|
||||
*/
|
||||
public static ParticipantData getFromRawPhoneBySystemLocale(final String phoneNumber) {
|
||||
final ParticipantData pd = getFromRawPhone(phoneNumber);
|
||||
pd.mNormalizedDestination = pd.mIsEmailAddress ?
|
||||
pd.mSendDestination :
|
||||
PhoneUtils.getDefault().getCanonicalBySystemLocale(pd.mSendDestination);
|
||||
pd.mDisplayDestination = pd.mIsEmailAddress ?
|
||||
pd.mNormalizedDestination :
|
||||
PhoneUtils.getDefault().formatForDisplay(pd.mNormalizedDestination);
|
||||
pd.maybeSetupUnknownSender();
|
||||
return pd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance from a raw phone number and using SIM or system locale to normalize it.
|
||||
*
|
||||
* Use this when creating a participant that is associated with a specific SIM. For example,
|
||||
* the sender of a received message or the recipient of a sending message that is already
|
||||
* targeted at a specific SIM.
|
||||
*
|
||||
* @param phoneNumber The raw phone number
|
||||
* @return instance
|
||||
*/
|
||||
public static ParticipantData getFromRawPhoneBySimLocale(
|
||||
final String phoneNumber, final int subId) {
|
||||
final ParticipantData pd = getFromRawPhone(phoneNumber);
|
||||
pd.mNormalizedDestination = pd.mIsEmailAddress ?
|
||||
pd.mSendDestination :
|
||||
PhoneUtils.get(subId).getCanonicalBySimLocale(pd.mSendDestination);
|
||||
pd.mDisplayDestination = pd.mIsEmailAddress ?
|
||||
pd.mNormalizedDestination :
|
||||
PhoneUtils.getDefault().formatForDisplay(pd.mNormalizedDestination);
|
||||
pd.maybeSetupUnknownSender();
|
||||
return pd;
|
||||
}
|
||||
|
||||
public static ParticipantData getSelfParticipant(final int subId) {
|
||||
Assert.isTrue(subId != OTHER_THAN_SELF_SUB_ID);
|
||||
final ParticipantData pd = new ParticipantData();
|
||||
pd.mParticipantId = null;
|
||||
pd.mSubId = subId;
|
||||
pd.mSlotId = INVALID_SLOT_ID;
|
||||
pd.mIsEmailAddress = false;
|
||||
pd.mSendDestination = null;
|
||||
pd.mNormalizedDestination = null;
|
||||
pd.mDisplayDestination = null;
|
||||
pd.mFullName = null;
|
||||
pd.mFirstName = null;
|
||||
pd.mProfilePhotoUri = null;
|
||||
pd.mContactId = PARTICIPANT_CONTACT_ID_NOT_RESOLVED;
|
||||
pd.mLookupKey = null;
|
||||
pd.mBlocked = false;
|
||||
pd.mSubscriptionColor = Color.TRANSPARENT;
|
||||
pd.mSubscriptionName = null;
|
||||
return pd;
|
||||
}
|
||||
|
||||
public static String getParticipantId(final DatabaseWrapper db, final int subId) {
|
||||
String id;
|
||||
synchronized (sSubIdtoParticipantIdCache) {
|
||||
id = sSubIdtoParticipantIdCache.get(subId);
|
||||
}
|
||||
|
||||
if (id != null) {
|
||||
return id;
|
||||
}
|
||||
|
||||
try (final Cursor cursor =
|
||||
db.query(DatabaseHelper.PARTICIPANTS_TABLE,
|
||||
new String[] {ParticipantColumns._ID},
|
||||
ParticipantColumns.SUB_ID + " =?",
|
||||
new String[] {Integer.toString(subId)}, null, null, null)) {
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
// We found an existing participant in the database
|
||||
id = cursor.getString(0);
|
||||
synchronized (sSubIdtoParticipantIdCache) {
|
||||
// Add it to the cache for next time
|
||||
sSubIdtoParticipantIdCache.put(subId, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
private void maybeSetupUnknownSender() {
|
||||
if (isUnknownSender()) {
|
||||
// Because your locale may change, we setup the display string for the unknown sender
|
||||
// on the fly rather than relying on the version in the database.
|
||||
final Resources resources = Factory.get().getApplicationContext().getResources();
|
||||
mDisplayDestination = resources.getString(R.string.unknown_sender);
|
||||
mFullName = mDisplayDestination;
|
||||
}
|
||||
}
|
||||
|
||||
public String getNormalizedDestination() {
|
||||
return mNormalizedDestination;
|
||||
}
|
||||
|
||||
public String getSendDestination() {
|
||||
return mSendDestination;
|
||||
}
|
||||
|
||||
public String getDisplayDestination() {
|
||||
return mDisplayDestination;
|
||||
}
|
||||
|
||||
public String getContactDestination() {
|
||||
return mContactDestination;
|
||||
}
|
||||
|
||||
public String getFullName() {
|
||||
return mFullName;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return mFirstName;
|
||||
}
|
||||
|
||||
public String getDisplayName(final boolean preferFullName) {
|
||||
if (preferFullName) {
|
||||
// Prefer full name over first name
|
||||
if (!TextUtils.isEmpty(mFullName)) {
|
||||
return mFullName;
|
||||
}
|
||||
if (!TextUtils.isEmpty(mFirstName)) {
|
||||
return mFirstName;
|
||||
}
|
||||
} else {
|
||||
// Prefer first name over full name
|
||||
if (!TextUtils.isEmpty(mFirstName)) {
|
||||
return mFirstName;
|
||||
}
|
||||
if (!TextUtils.isEmpty(mFullName)) {
|
||||
return mFullName;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to the display destination
|
||||
if (!TextUtils.isEmpty(mDisplayDestination)) {
|
||||
return mDisplayDestination;
|
||||
}
|
||||
|
||||
return Factory.get().getApplicationContext().getResources().getString(
|
||||
R.string.unknown_sender);
|
||||
}
|
||||
|
||||
public String getProfilePhotoUri() {
|
||||
return mProfilePhotoUri;
|
||||
}
|
||||
|
||||
public long getContactId() {
|
||||
return mContactId;
|
||||
}
|
||||
|
||||
public String getLookupKey() {
|
||||
return mLookupKey;
|
||||
}
|
||||
|
||||
public boolean updatePhoneNumberForSelfIfChanged() {
|
||||
final String phoneNumber =
|
||||
PhoneUtils.get(mSubId).getCanonicalForSelf(true/*allowOverride*/);
|
||||
boolean changed = false;
|
||||
if (isSelf() && !TextUtils.equals(phoneNumber, mNormalizedDestination)) {
|
||||
mNormalizedDestination = phoneNumber;
|
||||
mSendDestination = phoneNumber;
|
||||
mDisplayDestination = mIsEmailAddress ?
|
||||
phoneNumber :
|
||||
PhoneUtils.getDefault().formatForDisplay(phoneNumber);
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
public boolean updateSubscriptionInfoForSelfIfChanged(final SubscriptionInfo subscriptionInfo) {
|
||||
boolean changed = false;
|
||||
if (isSelf()) {
|
||||
if (subscriptionInfo == null) {
|
||||
// The subscription is inactive. Check if the participant is still active.
|
||||
if (isActiveSubscription()) {
|
||||
mSlotId = INVALID_SLOT_ID;
|
||||
mSubscriptionColor = Color.TRANSPARENT;
|
||||
mSubscriptionName = "";
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
final int slotId = subscriptionInfo.getSimSlotIndex();
|
||||
final int color = subscriptionInfo.getIconTint();
|
||||
final CharSequence name = subscriptionInfo.getDisplayName();
|
||||
if (mSlotId != slotId || mSubscriptionColor != color || mSubscriptionName != name) {
|
||||
mSlotId = slotId;
|
||||
mSubscriptionColor = color;
|
||||
mSubscriptionName = name.toString();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
public void setFullName(final String fullName) {
|
||||
mFullName = fullName;
|
||||
}
|
||||
|
||||
public void setFirstName(final String firstName) {
|
||||
mFirstName = firstName;
|
||||
}
|
||||
|
||||
public void setProfilePhotoUri(final String profilePhotoUri) {
|
||||
mProfilePhotoUri = profilePhotoUri;
|
||||
}
|
||||
|
||||
public void setContactId(final long contactId) {
|
||||
mContactId = contactId;
|
||||
}
|
||||
|
||||
public void setLookupKey(final String lookupKey) {
|
||||
mLookupKey = lookupKey;
|
||||
}
|
||||
|
||||
public void setSendDestination(final String destination) {
|
||||
mSendDestination = destination;
|
||||
}
|
||||
|
||||
public void setContactDestination(final String destination) {
|
||||
mContactDestination = destination;
|
||||
}
|
||||
|
||||
public int getSubId() {
|
||||
return mSubId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether this sub is active. Note that {@link ParticipantData#DEFAULT_SELF_SUB_ID} is
|
||||
* is considered as active if there is any active SIM.
|
||||
*/
|
||||
public boolean isActiveSubscription() {
|
||||
return mSlotId != INVALID_SLOT_ID;
|
||||
}
|
||||
|
||||
public boolean isDefaultSelf() {
|
||||
return mSubId == ParticipantData.DEFAULT_SELF_SUB_ID;
|
||||
}
|
||||
|
||||
public int getSlotId() {
|
||||
return mSlotId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slot IDs in the subscription manager is zero-based, but we want to show it
|
||||
* as 1-based in UI.
|
||||
*/
|
||||
public int getDisplaySlotId() {
|
||||
return getSlotId() + 1;
|
||||
}
|
||||
|
||||
public int getSubscriptionColor() {
|
||||
Assert.isTrue(isActiveSubscription());
|
||||
// Force the alpha channel to 0xff to ensure the returned color is solid.
|
||||
return mSubscriptionColor | 0xff000000;
|
||||
}
|
||||
|
||||
public String getSubscriptionName() {
|
||||
Assert.isTrue(isActiveSubscription());
|
||||
return mSubscriptionName;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return mParticipantId;
|
||||
}
|
||||
|
||||
public boolean isSelf() {
|
||||
return (mSubId != OTHER_THAN_SELF_SUB_ID);
|
||||
}
|
||||
|
||||
public boolean isEmail() {
|
||||
return mIsEmailAddress;
|
||||
}
|
||||
|
||||
public boolean isContactIdResolved() {
|
||||
return (mContactId != PARTICIPANT_CONTACT_ID_NOT_RESOLVED);
|
||||
}
|
||||
|
||||
public boolean isBlocked() {
|
||||
return mBlocked;
|
||||
}
|
||||
|
||||
public boolean isUnknownSender() {
|
||||
final String unknownSender = ParticipantData.getUnknownSenderDestination();
|
||||
return (TextUtils.equals(mSendDestination, unknownSender));
|
||||
}
|
||||
|
||||
public ContentValues toContentValues() {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(ParticipantColumns.SUB_ID, mSubId);
|
||||
values.put(ParticipantColumns.SIM_SLOT_ID, mSlotId);
|
||||
values.put(DatabaseHelper.ParticipantColumns.SEND_DESTINATION, mSendDestination);
|
||||
|
||||
if (!isUnknownSender()) {
|
||||
values.put(DatabaseHelper.ParticipantColumns.DISPLAY_DESTINATION, mDisplayDestination);
|
||||
values.put(DatabaseHelper.ParticipantColumns.NORMALIZED_DESTINATION,
|
||||
mNormalizedDestination);
|
||||
values.put(ParticipantColumns.FULL_NAME, mFullName);
|
||||
values.put(ParticipantColumns.FIRST_NAME, mFirstName);
|
||||
}
|
||||
|
||||
values.put(ParticipantColumns.PROFILE_PHOTO_URI, mProfilePhotoUri);
|
||||
values.put(ParticipantColumns.CONTACT_ID, mContactId);
|
||||
values.put(ParticipantColumns.LOOKUP_KEY, mLookupKey);
|
||||
values.put(ParticipantColumns.BLOCKED, mBlocked);
|
||||
values.put(ParticipantColumns.SUBSCRIPTION_COLOR, mSubscriptionColor);
|
||||
values.put(ParticipantColumns.SUBSCRIPTION_NAME, mSubscriptionName);
|
||||
return values;
|
||||
}
|
||||
|
||||
public ParticipantData(final Parcel in) {
|
||||
mParticipantId = in.readString();
|
||||
mSubId = in.readInt();
|
||||
mSlotId = in.readInt();
|
||||
mNormalizedDestination = in.readString();
|
||||
mSendDestination = in.readString();
|
||||
mDisplayDestination = in.readString();
|
||||
mFullName = in.readString();
|
||||
mFirstName = in.readString();
|
||||
mProfilePhotoUri = in.readString();
|
||||
mContactId = in.readLong();
|
||||
mLookupKey = in.readString();
|
||||
mIsEmailAddress = in.readInt() != 0;
|
||||
mBlocked = in.readInt() != 0;
|
||||
mSubscriptionColor = in.readInt();
|
||||
mSubscriptionName = in.readString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(mParticipantId);
|
||||
dest.writeInt(mSubId);
|
||||
dest.writeInt(mSlotId);
|
||||
dest.writeString(mNormalizedDestination);
|
||||
dest.writeString(mSendDestination);
|
||||
dest.writeString(mDisplayDestination);
|
||||
dest.writeString(mFullName);
|
||||
dest.writeString(mFirstName);
|
||||
dest.writeString(mProfilePhotoUri);
|
||||
dest.writeLong(mContactId);
|
||||
dest.writeString(mLookupKey);
|
||||
dest.writeInt(mIsEmailAddress ? 1 : 0);
|
||||
dest.writeInt(mBlocked ? 1 : 0);
|
||||
dest.writeInt(mSubscriptionColor);
|
||||
dest.writeString(mSubscriptionName);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ParticipantData> CREATOR = new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public ParticipantData createFromParcel(final Parcel in) {
|
||||
return new ParticipantData(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParticipantData[] newArray(final int size) {
|
||||
return new ParticipantData[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.datamodel.action.BugleActionToasts;
|
||||
import com.android.messaging.datamodel.action.UpdateDestinationBlockedAction;
|
||||
import com.android.messaging.util.AvatarUriUtil;
|
||||
|
||||
/**
|
||||
* Helps visualize a ParticipantData in a PersonItemView
|
||||
*/
|
||||
public class ParticipantListItemData extends PersonItemData {
|
||||
private final Uri mAvatarUri;
|
||||
private final String mDisplayName;
|
||||
private final String mDetails;
|
||||
private final long mContactId;
|
||||
private final String mLookupKey;
|
||||
private final String mNormalizedDestination;
|
||||
|
||||
/**
|
||||
* Constructor. Takes necessary info from the incoming ParticipantData.
|
||||
*/
|
||||
public ParticipantListItemData(final ParticipantData participant) {
|
||||
mAvatarUri = AvatarUriUtil.createAvatarUri(participant);
|
||||
mContactId = participant.getContactId();
|
||||
mLookupKey = participant.getLookupKey();
|
||||
mNormalizedDestination = participant.getNormalizedDestination();
|
||||
if (TextUtils.isEmpty(participant.getFullName())) {
|
||||
mDisplayName = participant.getSendDestination();
|
||||
mDetails = null;
|
||||
} else {
|
||||
mDisplayName = participant.getFullName();
|
||||
mDetails = (participant.isUnknownSender()) ? null : participant.getSendDestination();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getAvatarUri() {
|
||||
return mAvatarUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return mDisplayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDetails() {
|
||||
return mDetails;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent getClickIntent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContactId() {
|
||||
return mContactId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLookupKey() {
|
||||
return mLookupKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNormalizedDestination() {
|
||||
return mNormalizedDestination;
|
||||
}
|
||||
|
||||
public void unblock(final Context context) {
|
||||
UpdateDestinationBlockedAction.updateDestinationBlocked(
|
||||
mNormalizedDestination, false, null,
|
||||
BugleActionToasts.makeUpdateDestinationBlockedActionListener(context));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.v7.mms.pdu.ContentType;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.UriUtil;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Represents a "pending" message part that acts as a placeholder for the actual attachment being
|
||||
* loaded. It handles the task to load and persist the attachment from a Uri to local scratch
|
||||
* folder. This item is not persisted to the database.
|
||||
*/
|
||||
public class PendingAttachmentData extends MessagePartData {
|
||||
/** The pending state. This is the initial state where we haven't started loading yet */
|
||||
public static final int STATE_PENDING = 0;
|
||||
|
||||
/** The state for when we are currently loading the attachment to the scratch space */
|
||||
public static final int STATE_LOADING = 1;
|
||||
|
||||
/** The attachment has been successfully loaded and no longer pending */
|
||||
public static final int STATE_LOADED = 2;
|
||||
|
||||
/** The attachment failed to load */
|
||||
public static final int STATE_FAILED = 3;
|
||||
|
||||
private static final int LOAD_MEDIA_TIME_LIMIT_MILLIS = 60 * 1000; // 60s
|
||||
|
||||
/** The current state of the pending attachment. Refer to the STATE_* states above */
|
||||
private int mCurrentState;
|
||||
|
||||
/**
|
||||
* Create a new instance of PendingAttachmentData with an output Uri.
|
||||
* @param sourceUri the source Uri of the attachment. The Uri maybe temporary or remote,
|
||||
* so we need to persist it to local storage.
|
||||
*/
|
||||
protected PendingAttachmentData(final String caption, final String contentType,
|
||||
@NonNull final Uri sourceUri, final int width, final int height,
|
||||
final boolean onlySingleAttachment) {
|
||||
super(caption, contentType, sourceUri, width, height, onlySingleAttachment);
|
||||
mCurrentState = STATE_PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pending attachment data that is able to load from the given source uri and
|
||||
* persist the media resource locally in the scratch folder.
|
||||
*/
|
||||
public static PendingAttachmentData createPendingAttachmentData(final String contentType,
|
||||
final Uri sourceUri) {
|
||||
return createPendingAttachmentData(null, contentType, sourceUri, UNSPECIFIED_SIZE,
|
||||
UNSPECIFIED_SIZE);
|
||||
}
|
||||
|
||||
public static PendingAttachmentData createPendingAttachmentData(final String caption,
|
||||
final String contentType, final Uri sourceUri, final int width, final int height) {
|
||||
Assert.isTrue(ContentType.isMediaType(contentType));
|
||||
return new PendingAttachmentData(caption, contentType, sourceUri, width, height,
|
||||
false /*onlySingleAttachment*/);
|
||||
}
|
||||
|
||||
public static PendingAttachmentData createPendingAttachmentData(final String caption,
|
||||
final String contentType, final Uri sourceUri, final int width, final int height,
|
||||
final boolean onlySingleAttachment) {
|
||||
Assert.isTrue(ContentType.isMediaType(contentType));
|
||||
return new PendingAttachmentData(caption, contentType, sourceUri, width, height,
|
||||
onlySingleAttachment);
|
||||
}
|
||||
|
||||
public int getCurrentState() {
|
||||
return mCurrentState;
|
||||
}
|
||||
|
||||
public void loadAttachmentForDraft(final DraftMessageData draftMessageData,
|
||||
final String bindingId) {
|
||||
if (mCurrentState != STATE_PENDING) {
|
||||
return;
|
||||
}
|
||||
mCurrentState = STATE_LOADING;
|
||||
|
||||
// Kick off loading the content of the media and persist it locally.
|
||||
// Note: we need to persist the media locally even if it's not remote, because we
|
||||
// want to be able to resend the media in case the message failed to send.
|
||||
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
final Future<?> future = executor.submit(() -> {
|
||||
MessagePartData data = null;
|
||||
final Uri contentUri = getContentUri();
|
||||
final Uri persistedUri = UriUtil.persistContentToScratchSpace(contentUri);
|
||||
if (persistedUri != null) {
|
||||
data = MessagePartData.createMediaMessagePart(
|
||||
getText(),
|
||||
getContentType(),
|
||||
persistedUri,
|
||||
getWidth(),
|
||||
getHeight());
|
||||
}
|
||||
|
||||
final MessagePartData attachment = data;
|
||||
handler.post(() -> {
|
||||
if (attachment != null) {
|
||||
mCurrentState = STATE_LOADED;
|
||||
if (draftMessageData.isBound(bindingId)) {
|
||||
draftMessageData.updatePendingAttachment(attachment,
|
||||
PendingAttachmentData.this);
|
||||
} else {
|
||||
// The draft message data is no longer bound, drop the loaded attachment.
|
||||
attachment.destroyAsync();
|
||||
}
|
||||
} else {
|
||||
// Media load failed. We already logged in doInBackground() so don't need to
|
||||
// do that again.
|
||||
mCurrentState = STATE_FAILED;
|
||||
if (draftMessageData.isBound(bindingId)) {
|
||||
draftMessageData.onPendingAttachmentLoadFailed(PendingAttachmentData.this);
|
||||
draftMessageData.removePendingAttachment(PendingAttachmentData.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
executor.schedule(() -> {
|
||||
if (!future.isDone()) {
|
||||
future.cancel(true);
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Timeout while retrieving media");
|
||||
mCurrentState = STATE_FAILED;
|
||||
if (draftMessageData.isBound(bindingId)) {
|
||||
draftMessageData.removePendingAttachment(PendingAttachmentData.this);
|
||||
}
|
||||
}
|
||||
}, LOAD_MEDIA_TIME_LIMIT_MILLIS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
protected PendingAttachmentData(final Parcel in) {
|
||||
super(in);
|
||||
mCurrentState = in.readInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel out, final int flags) {
|
||||
super.writeToParcel(out, flags);
|
||||
out.writeInt(mCurrentState);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<PendingAttachmentData> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public PendingAttachmentData createFromParcel(final Parcel in) {
|
||||
return new PendingAttachmentData(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingAttachmentData[] newArray(final int size) {
|
||||
return new PendingAttachmentData[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import com.android.messaging.datamodel.BoundCursorLoader;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.action.BugleActionToasts;
|
||||
import com.android.messaging.datamodel.action.UpdateDestinationBlockedAction;
|
||||
import com.android.messaging.datamodel.binding.BindableData;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Services data needs for PeopleAndOptionsFragment.
|
||||
*/
|
||||
public class PeopleAndOptionsData extends BindableData implements
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
public interface PeopleAndOptionsDataListener {
|
||||
void onOptionsCursorUpdated(PeopleAndOptionsData data, Cursor cursor);
|
||||
void onParticipantsListLoaded(PeopleAndOptionsData data,
|
||||
List<ParticipantData> participants);
|
||||
}
|
||||
|
||||
private static final String BINDING_ID = "bindingId";
|
||||
private final Context mContext;
|
||||
private final String mConversationId;
|
||||
private final ConversationParticipantsData mParticipantData;
|
||||
private LoaderManager mLoaderManager;
|
||||
private PeopleAndOptionsDataListener mListener;
|
||||
|
||||
public PeopleAndOptionsData(final String conversationId, final Context context,
|
||||
final PeopleAndOptionsDataListener listener) {
|
||||
mListener = listener;
|
||||
mContext = context;
|
||||
mConversationId = conversationId;
|
||||
mParticipantData = new ConversationParticipantsData();
|
||||
}
|
||||
|
||||
private static final int CONVERSATION_OPTIONS_LOADER = 1;
|
||||
private static final int PARTICIPANT_LOADER = 2;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
|
||||
final String bindingId = args.getString(BINDING_ID);
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(bindingId)) {
|
||||
switch (id) {
|
||||
case CONVERSATION_OPTIONS_LOADER: {
|
||||
final Uri uri =
|
||||
MessagingContentProvider.buildConversationMetadataUri(mConversationId);
|
||||
return new BoundCursorLoader(bindingId, mContext, uri,
|
||||
new String[]{}, null, null, null);
|
||||
}
|
||||
|
||||
case PARTICIPANT_LOADER: {
|
||||
final Uri uri =
|
||||
MessagingContentProvider
|
||||
.buildConversationParticipantsUri(mConversationId);
|
||||
return new BoundCursorLoader(bindingId, mContext, uri,
|
||||
ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
|
||||
}
|
||||
|
||||
default:
|
||||
Assert.fail("Unknown loader id for PeopleAndOptionsFragment!");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Loader created after unbinding PeopleAndOptionsFragment");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull final Loader<Cursor> loader, final Cursor data) {
|
||||
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
|
||||
if (isBound(cursorLoader.getBindingId())) {
|
||||
switch (loader.getId()) {
|
||||
case CONVERSATION_OPTIONS_LOADER:
|
||||
mListener.onOptionsCursorUpdated(this, data);
|
||||
break;
|
||||
|
||||
case PARTICIPANT_LOADER:
|
||||
mParticipantData.bind(data);
|
||||
mListener.onParticipantsListLoaded(this,
|
||||
mParticipantData.getParticipantListExcludingSelf());
|
||||
break;
|
||||
|
||||
default:
|
||||
Assert.fail("Unknown loader id for PeopleAndOptionsFragment!");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG,
|
||||
"Loader finished after unbinding PeopleAndOptionsFragment");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull final Loader<Cursor> loader) {
|
||||
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
|
||||
if (isBound(cursorLoader.getBindingId())) {
|
||||
switch (loader.getId()) {
|
||||
case CONVERSATION_OPTIONS_LOADER:
|
||||
mListener.onOptionsCursorUpdated(this, null);
|
||||
break;
|
||||
|
||||
case PARTICIPANT_LOADER:
|
||||
mParticipantData.bind(null);
|
||||
break;
|
||||
|
||||
default:
|
||||
Assert.fail("Unknown loader id for PeopleAndOptionsFragment!");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Loader reset after unbinding PeopleAndOptionsFragment");
|
||||
}
|
||||
}
|
||||
|
||||
public void init(final LoaderManager loaderManager,
|
||||
final BindingBase<PeopleAndOptionsData> binding) {
|
||||
final Bundle args = new Bundle();
|
||||
args.putString(BINDING_ID, binding.getBindingId());
|
||||
mLoaderManager = loaderManager;
|
||||
mLoaderManager.initLoader(CONVERSATION_OPTIONS_LOADER, args, this);
|
||||
mLoaderManager.initLoader(PARTICIPANT_LOADER, args, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unregisterListeners() {
|
||||
mListener = null;
|
||||
|
||||
// This could be null if we bind but the caller doesn't init the BindableData
|
||||
if (mLoaderManager != null) {
|
||||
mLoaderManager.destroyLoader(CONVERSATION_OPTIONS_LOADER);
|
||||
mLoaderManager.destroyLoader(PARTICIPANT_LOADER);
|
||||
mLoaderManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setDestinationBlocked(final BindingBase<PeopleAndOptionsData> binding,
|
||||
final boolean blocked) {
|
||||
final String bindingId = binding.getBindingId();
|
||||
final ParticipantData participantData = mParticipantData.getOtherParticipant();
|
||||
if (isBound(bindingId) && participantData != null) {
|
||||
UpdateDestinationBlockedAction.updateDestinationBlocked(
|
||||
participantData.getNormalizedDestination(),
|
||||
blocked, mConversationId,
|
||||
BugleActionToasts.makeUpdateDestinationBlockedActionListener(mContext));
|
||||
}
|
||||
}
|
||||
|
||||
public String getConversationId() {
|
||||
return mConversationId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
public class PeopleOptionsItemData {
|
||||
// Identification for each setting that's surfaced to the UI layer.
|
||||
public static final int SETTING_NOTIFICATION = 0;
|
||||
public static final int SETTING_BLOCKED = 1;
|
||||
public static final int SETTINGS_COUNT = 2;
|
||||
|
||||
private String mTitle;
|
||||
private int mItemId;
|
||||
private ParticipantData mOtherParticipant;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
public PeopleOptionsItemData(final Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to a specific setting column on conversation metadata cursor. (Note
|
||||
* that it binds to columns because it treats individual columns of the cursor as
|
||||
* separate options to display for the conversation, e.g. notification settings).
|
||||
*/
|
||||
public void bind(
|
||||
final Cursor cursor, final ParticipantData otherParticipant, final int settingType) {
|
||||
mItemId = settingType;
|
||||
mOtherParticipant = otherParticipant;
|
||||
|
||||
switch (settingType) {
|
||||
case SETTING_NOTIFICATION:
|
||||
mTitle = mContext.getString(R.string.notifications_enabled_conversation_pref_title);
|
||||
break;
|
||||
|
||||
case SETTING_BLOCKED:
|
||||
Assert.notNull(otherParticipant);
|
||||
final int resourceId = otherParticipant.isBlocked() ?
|
||||
R.string.unblock_contact_title : R.string.block_contact_title;
|
||||
mTitle = mContext.getString(resourceId, otherParticipant.getDisplayDestination());
|
||||
break;
|
||||
|
||||
default:
|
||||
Assert.fail("Unsupported conversation option type!");
|
||||
}
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return mTitle;
|
||||
}
|
||||
|
||||
public int getItemId() {
|
||||
return mItemId;
|
||||
}
|
||||
|
||||
public ParticipantData getOtherParticipant() {
|
||||
return mOtherParticipant;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.android.messaging.datamodel.binding.BindableData;
|
||||
|
||||
/**
|
||||
* Bridges between any particpant/contact related data and data displayed in the PersonItemView.
|
||||
*/
|
||||
public abstract class PersonItemData extends BindableData {
|
||||
/**
|
||||
* The UI component that listens for data change and update accordingly.
|
||||
*/
|
||||
public interface PersonItemDataListener {
|
||||
void onPersonDataUpdated(PersonItemData data);
|
||||
void onPersonDataFailed(PersonItemData data, Exception exception);
|
||||
}
|
||||
|
||||
private PersonItemDataListener mListener;
|
||||
|
||||
public abstract Uri getAvatarUri();
|
||||
public abstract String getDisplayName();
|
||||
public abstract String getDetails();
|
||||
public abstract Intent getClickIntent();
|
||||
public abstract long getContactId();
|
||||
public abstract String getLookupKey();
|
||||
public abstract String getNormalizedDestination();
|
||||
|
||||
public void setListener(final PersonItemDataListener listener) {
|
||||
if (isBound()) {
|
||||
mListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
protected void notifyDataUpdated() {
|
||||
if (isBound() && mListener != null) {
|
||||
mListener.onPersonDataUpdated(this);
|
||||
}
|
||||
}
|
||||
|
||||
protected void notifyDataFailed(final Exception exception) {
|
||||
if (isBound() && mListener != null) {
|
||||
mListener.onPersonDataFailed(this, exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unregisterListeners() {
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.collection.ArrayMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A class that contains the list of all self participants potentially involved in a conversation.
|
||||
* This class contains both active/inactive self entries when there is multi-SIM support.
|
||||
*/
|
||||
public class SelfParticipantsData {
|
||||
/**
|
||||
* The map from self participant ids to self-participant data entries in the participants table.
|
||||
* This includes both active, inactive and default (with subId ==
|
||||
* {@link ParticipantData#DEFAULT_SELF_SUB_ID}) subscriptions.
|
||||
*/
|
||||
private final ArrayMap<String, ParticipantData> mSelfParticipantMap;
|
||||
|
||||
public SelfParticipantsData() {
|
||||
mSelfParticipantMap = new ArrayMap<>();
|
||||
}
|
||||
|
||||
public void bind(final Cursor cursor) {
|
||||
mSelfParticipantMap.clear();
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
final ParticipantData newParticipant = ParticipantData.getFromCursor(cursor);
|
||||
mSelfParticipantMap.put(newParticipant.getId(), newParticipant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of self participants for all subscriptions.
|
||||
* @param activeOnly if set, returns active self entries only (i.e. those with SIMs plugged in).
|
||||
*/
|
||||
public List<ParticipantData> getSelfParticipants(final boolean activeOnly) {
|
||||
List<ParticipantData> list = new ArrayList<>();
|
||||
for (final ParticipantData self : mSelfParticipantMap.values()) {
|
||||
if (!activeOnly || self.isActiveSubscription()) {
|
||||
list.add(self);
|
||||
}
|
||||
}
|
||||
list.sort((Comparator) (o1, o2) -> {
|
||||
int slotId1 = ((ParticipantData) o1).getSlotId();
|
||||
int slotId2 = ((ParticipantData) o2).getSlotId();
|
||||
return slotId1 > slotId2 ? 1 : -1;
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the self participant corresponding to the given self id.
|
||||
*/
|
||||
ParticipantData getSelfParticipantById(final String selfId) {
|
||||
return mSelfParticipantMap.get(selfId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if a given self id represents the default self.
|
||||
*/
|
||||
boolean isDefaultSelf(final String selfId) {
|
||||
final ParticipantData self = getSelfParticipantById(selfId);
|
||||
return self == null ? false : self.getSubId() == ParticipantData.DEFAULT_SELF_SUB_ID;
|
||||
}
|
||||
|
||||
public int getSelfParticipantsCountExcludingDefault(final boolean activeOnly) {
|
||||
int count = 0;
|
||||
for (final ParticipantData self : mSelfParticipantMap.values()) {
|
||||
if (!self.isDefaultSelf() && (!activeOnly || self.isActiveSubscription())) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public ParticipantData getDefaultSelfParticipant() {
|
||||
for (final ParticipantData self : mSelfParticipantMap.values()) {
|
||||
if (self.isDefaultSelf()) {
|
||||
return self;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean isLoaded() {
|
||||
return !mSelfParticipantMap.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.BoundCursorLoader;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.binding.BindableData;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Services SettingsFragment's data needs for loading active self participants to display
|
||||
* the list of active subscriptions.
|
||||
*/
|
||||
public class SettingsData extends BindableData implements
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
public interface SettingsDataListener {
|
||||
void onSelfParticipantDataLoaded(SettingsData data);
|
||||
}
|
||||
|
||||
public static class SettingsItem {
|
||||
public static final int TYPE_GENERAL_SETTINGS = 1;
|
||||
public static final int TYPE_PER_SUBSCRIPTION_SETTINGS = 2;
|
||||
|
||||
private final String mDisplayName;
|
||||
private final String mDisplayDetail;
|
||||
private final String mActivityTitle;
|
||||
private final int mType;
|
||||
private final int mSubId;
|
||||
|
||||
private SettingsItem(final String displayName, final String displayDetail,
|
||||
final String activityTitle, final int type, final int subId) {
|
||||
mDisplayName = displayName;
|
||||
mDisplayDetail = displayDetail;
|
||||
mActivityTitle = activityTitle;
|
||||
mType = type;
|
||||
mSubId = subId;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return mDisplayName;
|
||||
}
|
||||
|
||||
public String getDisplayDetail() {
|
||||
return mDisplayDetail;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
public int getSubId() {
|
||||
return mSubId;
|
||||
}
|
||||
|
||||
public String getActivityTitle() {
|
||||
return mActivityTitle;
|
||||
}
|
||||
|
||||
public static SettingsItem fromSelfParticipant(final Context context,
|
||||
final ParticipantData self) {
|
||||
Assert.isTrue(self.isSelf());
|
||||
Assert.isTrue(self.isActiveSubscription());
|
||||
final String displayDetail = TextUtils.isEmpty(self.getDisplayDestination()) ?
|
||||
context.getString(R.string.sim_settings_unknown_number) :
|
||||
self.getDisplayDestination();
|
||||
final String displayName = context.getString(R.string.sim_specific_settings,
|
||||
self.getSubscriptionName());
|
||||
return new SettingsItem(displayName, displayDetail, displayName,
|
||||
TYPE_PER_SUBSCRIPTION_SETTINGS, self.getSubId());
|
||||
}
|
||||
|
||||
public static SettingsItem createGeneralSettingsItem(final Context context) {
|
||||
return new SettingsItem(context.getString(R.string.general_settings),
|
||||
null, context.getString(R.string.general_settings_activity_title),
|
||||
TYPE_GENERAL_SETTINGS, -1);
|
||||
}
|
||||
|
||||
public static SettingsItem createDefaultMmsSettingsItem(final Context context,
|
||||
final int subId) {
|
||||
return new SettingsItem(context.getString(R.string.advanced_settings),
|
||||
null, context.getString(R.string.advanced_settings_activity_title),
|
||||
TYPE_PER_SUBSCRIPTION_SETTINGS, subId);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String BINDING_ID = "bindingId";
|
||||
private final Context mContext;
|
||||
private final SelfParticipantsData mSelfParticipantsData;
|
||||
private LoaderManager mLoaderManager;
|
||||
private SettingsDataListener mListener;
|
||||
|
||||
public SettingsData(final Context context,
|
||||
final SettingsDataListener listener) {
|
||||
mListener = listener;
|
||||
mContext = context;
|
||||
mSelfParticipantsData = new SelfParticipantsData();
|
||||
}
|
||||
|
||||
private static final int SELF_PARTICIPANT_LOADER = 1;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
|
||||
Assert.equals(SELF_PARTICIPANT_LOADER, id);
|
||||
Loader<Cursor> loader = null;
|
||||
|
||||
final String bindingId = args.getString(BINDING_ID);
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(bindingId)) {
|
||||
loader = new BoundCursorLoader(bindingId, mContext,
|
||||
MessagingContentProvider.PARTICIPANTS_URI,
|
||||
ParticipantData.ParticipantsQuery.PROJECTION,
|
||||
ParticipantColumns.SUB_ID + " <> ?",
|
||||
new String[] { String.valueOf(ParticipantData.OTHER_THAN_SELF_SUB_ID) },
|
||||
null);
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Creating self loader after unbinding");
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull final Loader<Cursor> generic, final Cursor data) {
|
||||
final BoundCursorLoader loader = (BoundCursorLoader) generic;
|
||||
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(loader.getBindingId())) {
|
||||
mSelfParticipantsData.bind(data);
|
||||
mListener.onSelfParticipantDataLoaded(this);
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Self loader finished after unbinding");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(final Loader<Cursor> generic) {
|
||||
final BoundCursorLoader loader = (BoundCursorLoader) generic;
|
||||
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(loader.getBindingId())) {
|
||||
mSelfParticipantsData.bind(null);
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Self loader reset after unbinding");
|
||||
}
|
||||
}
|
||||
|
||||
public void init(final LoaderManager loaderManager,
|
||||
final BindingBase<SettingsData> binding) {
|
||||
final Bundle args = new Bundle();
|
||||
args.putString(BINDING_ID, binding.getBindingId());
|
||||
mLoaderManager = loaderManager;
|
||||
mLoaderManager.initLoader(SELF_PARTICIPANT_LOADER, args, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unregisterListeners() {
|
||||
mListener = null;
|
||||
|
||||
// This could be null if we bind but the caller doesn't init the BindableData
|
||||
if (mLoaderManager != null) {
|
||||
mLoaderManager.destroyLoader(SELF_PARTICIPANT_LOADER);
|
||||
mLoaderManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<SettingsItem> getSettingsItems() {
|
||||
final List<ParticipantData> selfs = mSelfParticipantsData.getSelfParticipants(true);
|
||||
final List<SettingsItem> settingsItems = new ArrayList<>();
|
||||
// First goes the general settings, followed by per-subscription settings.
|
||||
settingsItems.add(SettingsItem.createGeneralSettingsItem(mContext));
|
||||
// For per-subscription settings, show the actual SIM name with phone number if the
|
||||
// platorm is at least L-MR1 and there are multiple active SIMs.
|
||||
final int activeSubCountExcludingDefault =
|
||||
mSelfParticipantsData.getSelfParticipantsCountExcludingDefault(true);
|
||||
if (activeSubCountExcludingDefault > 0) {
|
||||
for (ParticipantData self : selfs) {
|
||||
if (!self.isDefaultSelf()) {
|
||||
if (activeSubCountExcludingDefault > 1) {
|
||||
settingsItems.add(SettingsItem.fromSelfParticipant(mContext, self));
|
||||
} else {
|
||||
// This is the only active non-default SIM.
|
||||
settingsItems.add(SettingsItem.createDefaultMmsSettingsItem(mContext,
|
||||
self.getSubId()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Either pre-L-MR1, or there's no active SIM, so show the default MMS settings.
|
||||
settingsItems.add(SettingsItem.createDefaultMmsSettingsItem(mContext,
|
||||
ParticipantData.DEFAULT_SELF_SUB_ID));
|
||||
}
|
||||
return settingsItems;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.AvatarUriUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* This is a UI facing data model component that holds a list of
|
||||
* {@link SubscriptionListData.SubscriptionListEntry}'s, one for each *active* subscriptions.
|
||||
*
|
||||
* This is used to:
|
||||
* 1) Show a list of SIMs in the SIM Selector
|
||||
* 2) Show the currently selected SIM in the compose message view
|
||||
* 3) Show SIM indicators on conversation message views
|
||||
*
|
||||
* It builds on top of SelfParticipantsData and performs additional logic such as determining
|
||||
* the set of icons to use for the individual Subs.
|
||||
*/
|
||||
public class SubscriptionListData {
|
||||
/**
|
||||
* Represents a single sub that backs UI.
|
||||
*/
|
||||
public static class SubscriptionListEntry {
|
||||
public final String selfParticipantId;
|
||||
public final Uri iconUri;
|
||||
public final Uri selectedIconUri;
|
||||
public final String displayName;
|
||||
public final int displayColor;
|
||||
public final String displayDestination;
|
||||
|
||||
private SubscriptionListEntry(final String selfParticipantId, final Uri iconUri,
|
||||
final Uri selectedIconUri, final String displayName, final int displayColor,
|
||||
final String displayDestination) {
|
||||
this.selfParticipantId = selfParticipantId;
|
||||
this.iconUri = iconUri;
|
||||
this.selectedIconUri = selectedIconUri;
|
||||
this.displayName = displayName;
|
||||
this.displayColor = displayColor;
|
||||
this.displayDestination = displayDestination;
|
||||
}
|
||||
|
||||
static SubscriptionListEntry fromSelfParticipantData(
|
||||
final ParticipantData selfParticipantData, final Context context) {
|
||||
Assert.isTrue(selfParticipantData.isSelf());
|
||||
Assert.isTrue(selfParticipantData.isActiveSubscription());
|
||||
final int slotId = selfParticipantData.getDisplaySlotId();
|
||||
final String iconIdentifier = String.format(Locale.getDefault(), "%d", slotId);
|
||||
final String subscriptionName = selfParticipantData.getSubscriptionName();
|
||||
final String displayName = TextUtils.isEmpty(subscriptionName) ?
|
||||
context.getString(R.string.sim_slot_identifier, slotId) : subscriptionName;
|
||||
return new SubscriptionListEntry(selfParticipantData.getId(),
|
||||
AvatarUriUtil.createAvatarUri(selfParticipantData, iconIdentifier,
|
||||
false /* selected */, false /* incoming */),
|
||||
AvatarUriUtil.createAvatarUri(selfParticipantData, iconIdentifier,
|
||||
true /* selected */, false /* incoming */),
|
||||
displayName, selfParticipantData.getSubscriptionColor(),
|
||||
selfParticipantData.getDisplayDestination());
|
||||
}
|
||||
}
|
||||
|
||||
private final List<SubscriptionListEntry> mEntriesExcludingDefault;
|
||||
private SubscriptionListEntry mDefaultEntry;
|
||||
private final Context mContext;
|
||||
|
||||
public SubscriptionListData(final Context context) {
|
||||
mEntriesExcludingDefault = new ArrayList<>();
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public void bind(final List<ParticipantData> subs) {
|
||||
mEntriesExcludingDefault.clear();
|
||||
mDefaultEntry = null;
|
||||
for (final ParticipantData sub : subs) {
|
||||
final SubscriptionListEntry entry =
|
||||
SubscriptionListEntry.fromSelfParticipantData(sub, mContext);
|
||||
if (!sub.isDefaultSelf()) {
|
||||
mEntriesExcludingDefault.add(entry);
|
||||
} else {
|
||||
mDefaultEntry = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<SubscriptionListEntry> getActiveSubscriptionEntriesExcludingDefault() {
|
||||
return mEntriesExcludingDefault;
|
||||
}
|
||||
|
||||
public SubscriptionListEntry getActiveSubscriptionEntryBySelfId(final String selfId,
|
||||
final boolean excludeDefault) {
|
||||
if (mDefaultEntry != null && TextUtils.equals(mDefaultEntry.selfParticipantId, selfId)) {
|
||||
return excludeDefault ? null : mDefaultEntry;
|
||||
}
|
||||
|
||||
for (final SubscriptionListEntry entry : mEntriesExcludingDefault) {
|
||||
if (TextUtils.equals(entry.selfParticipantId, selfId)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean hasData() {
|
||||
return !mEntriesExcludingDefault.isEmpty() || mDefaultEntry != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.binding.Binding;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.datamodel.media.BindableMediaRequest;
|
||||
import com.android.messaging.datamodel.media.MediaRequest;
|
||||
import com.android.messaging.datamodel.media.MediaResourceManager;
|
||||
import com.android.messaging.datamodel.media.MediaResourceManager.MediaResourceLoadListener;
|
||||
import com.android.messaging.datamodel.media.VCardRequestDescriptor;
|
||||
import com.android.messaging.datamodel.media.VCardResource;
|
||||
import com.android.messaging.datamodel.media.VCardResourceEntry;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.AvatarUriUtil;
|
||||
import com.android.messaging.util.ContactUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Data class for visualizing and loading data for a VCard contact.
|
||||
*/
|
||||
public class VCardContactItemData extends PersonItemData
|
||||
implements MediaResourceLoadListener<VCardResource> {
|
||||
private final Context mContext;
|
||||
private final Uri mVCardUri;
|
||||
private String mDetails;
|
||||
private final Binding<BindableMediaRequest<VCardResource>> mBinding =
|
||||
BindingBase.createBinding(this);
|
||||
private VCardResource mVCardResource;
|
||||
|
||||
private static final Uri sDefaultAvatarUri =
|
||||
AvatarUriUtil.createAvatarUri(null, null, null, null);
|
||||
|
||||
/**
|
||||
* Constructor. This parses data from the given MessagePartData describing the vcard
|
||||
*/
|
||||
public VCardContactItemData(final Context context, final MessagePartData messagePartData) {
|
||||
this(context, messagePartData.getContentUri());
|
||||
Assert.isTrue(messagePartData.isVCard());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor. This parses data from the given VCard Uri
|
||||
*/
|
||||
public VCardContactItemData(final Context context, final Uri vCardUri) {
|
||||
mContext = context;
|
||||
mDetails = mContext.getString(R.string.loading_vcard);
|
||||
mVCardUri = vCardUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getAvatarUri() {
|
||||
if (hasValidVCard()) {
|
||||
final List<VCardResourceEntry> vcards = mVCardResource.getVCards();
|
||||
Assert.isTrue(vcards.size() > 0);
|
||||
if (vcards.size() == 1) {
|
||||
return vcards.get(0).getAvatarUri();
|
||||
}
|
||||
}
|
||||
return sDefaultAvatarUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
if (hasValidVCard()) {
|
||||
final List<VCardResourceEntry> vcards = mVCardResource.getVCards();
|
||||
Assert.isTrue(vcards.size() > 0);
|
||||
if (vcards.size() == 1) {
|
||||
return vcards.get(0).getDisplayName();
|
||||
} else {
|
||||
return mContext.getResources().getQuantityString(
|
||||
R.plurals.vcard_multiple_display_name, vcards.size(), vcards.size());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDetails() {
|
||||
return mDetails;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent getClickIntent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContactId() {
|
||||
return ContactUtil.INVALID_CONTACT_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLookupKey() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNormalizedDestination() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public VCardResource getVCardResource() {
|
||||
return hasValidVCard() ? mVCardResource : null;
|
||||
}
|
||||
|
||||
public Uri getVCardUri() {
|
||||
return hasValidVCard() ? mVCardUri : null;
|
||||
}
|
||||
|
||||
public boolean hasValidVCard() {
|
||||
return isBound() && mVCardResource != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(final String bindingId) {
|
||||
super.bind(bindingId);
|
||||
|
||||
// Bind and request the VCard from media resource manager.
|
||||
mBinding.bind(new VCardRequestDescriptor(mVCardUri).buildAsyncMediaRequest(mContext, this));
|
||||
MediaResourceManager.get().requestMediaResourceAsync(mBinding.getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbind(final String bindingId) {
|
||||
super.unbind(bindingId);
|
||||
mBinding.unbind();
|
||||
if (mVCardResource != null) {
|
||||
mVCardResource.release();
|
||||
mVCardResource = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(o instanceof VCardContactItemData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final VCardContactItemData lhs = (VCardContactItemData) o;
|
||||
return mVCardUri.equals(lhs.mVCardUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaResourceLoaded(final MediaRequest<VCardResource> request,
|
||||
final VCardResource resource, final boolean isCached) {
|
||||
Assert.isTrue(mVCardResource == null);
|
||||
mBinding.ensureBound();
|
||||
mDetails = mContext.getString(R.string.vcard_tap_hint);
|
||||
mVCardResource = resource;
|
||||
mVCardResource.addRef();
|
||||
notifyDataUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaResourceLoadError(final MediaRequest<VCardResource> request,
|
||||
final Exception exception) {
|
||||
mBinding.ensureBound();
|
||||
mDetails = mContext.getString(R.string.failed_loading_vcard);
|
||||
notifyDataFailed(exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.media;
|
||||
|
||||
import com.android.messaging.datamodel.media.MediaResourceManager.MediaResourceLoadListener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A mix-in style class that wraps around a normal, threading-agnostic MediaRequest object with
|
||||
* functionalities offered by {@link BindableMediaRequest} to allow for async processing.
|
||||
*/
|
||||
class AsyncMediaRequestWrapper<T extends RefCountedMediaResource> extends BindableMediaRequest<T> {
|
||||
|
||||
/**
|
||||
* Create a new async media request wrapper instance given the listener.
|
||||
*/
|
||||
public static <T extends RefCountedMediaResource> AsyncMediaRequestWrapper<T>
|
||||
createWith(final MediaRequest<T> wrappedRequest,
|
||||
final MediaResourceLoadListener<T> listener) {
|
||||
return new AsyncMediaRequestWrapper<>(listener, wrappedRequest);
|
||||
}
|
||||
|
||||
private final MediaRequest<T> mWrappedRequest;
|
||||
|
||||
private AsyncMediaRequestWrapper(final MediaResourceLoadListener<T> listener,
|
||||
final MediaRequest<T> wrappedRequest) {
|
||||
super(listener);
|
||||
mWrappedRequest = wrappedRequest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return mWrappedRequest.getKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaCache<T> getMediaCache() {
|
||||
return mWrappedRequest.getMediaCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRequestType() {
|
||||
return mWrappedRequest.getRequestType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T loadMediaBlocking(List<MediaRequest<T>> chainedTask) throws Exception {
|
||||
return mWrappedRequest.loadMediaBlocking(chainedTask);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCacheId() {
|
||||
return mWrappedRequest.getCacheId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaRequestDescriptor<T> getDescriptor() {
|
||||
return mWrappedRequest.getDescriptor();
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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.media;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.RectF;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.AvatarUriUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class AvatarGroupRequestDescriptor extends CompositeImageRequestDescriptor {
|
||||
private static final int MAX_GROUP_SIZE = 4;
|
||||
|
||||
public AvatarGroupRequestDescriptor(final Uri uri, final int desiredWidth,
|
||||
final int desiredHeight) {
|
||||
this(convertToDescriptor(uri, desiredWidth, desiredHeight), desiredWidth, desiredHeight);
|
||||
}
|
||||
|
||||
public AvatarGroupRequestDescriptor(final List<? extends ImageRequestDescriptor> descriptors,
|
||||
final int desiredWidth, final int desiredHeight) {
|
||||
super(descriptors, desiredWidth, desiredHeight);
|
||||
Assert.isTrue(descriptors.size() <= MAX_GROUP_SIZE);
|
||||
}
|
||||
|
||||
private static List<? extends ImageRequestDescriptor> convertToDescriptor(final Uri uri,
|
||||
final int desiredWidth, final int desiredHeight) {
|
||||
final List<String> participantUriStrings = AvatarUriUtil.getGroupParticipantUris(uri);
|
||||
final List<AvatarRequestDescriptor> avatarDescriptors =
|
||||
new ArrayList<>(participantUriStrings.size());
|
||||
for (final String uriString : participantUriStrings) {
|
||||
final AvatarRequestDescriptor descriptor = new AvatarRequestDescriptor(
|
||||
Uri.parse(uriString), desiredWidth, desiredHeight);
|
||||
avatarDescriptors.add(descriptor);
|
||||
}
|
||||
return avatarDescriptors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompositeImageRequest<?> buildBatchImageRequest(final Context context) {
|
||||
return new CompositeImageRequest<>(context, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RectF> getChildRequestTargetRects() {
|
||||
return Arrays.asList(generateDestRectArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of {@link RectF} which represents where each of the individual avatar
|
||||
* should be located in the final group avatar image. The location of each avatar depends on
|
||||
* the size of the group and the size of the overall group avatar size.
|
||||
*/
|
||||
private RectF[] generateDestRectArray() {
|
||||
final int groupSize = mDescriptors.size();
|
||||
final float width = desiredWidth;
|
||||
final float height = desiredHeight;
|
||||
final float halfWidth = width / 2F;
|
||||
final float halfHeight = height / 2F;
|
||||
final RectF[] destArray = new RectF[groupSize];
|
||||
switch (groupSize) {
|
||||
case 2:
|
||||
/**
|
||||
* +-------+
|
||||
* | 0 | |
|
||||
* +-------+
|
||||
* | | 1 |
|
||||
* +-------+
|
||||
*
|
||||
* We want two circles which touches in the center. To get this we know that the
|
||||
* diagonal of the overall group avatar is squareRoot(2) * w We also know that the
|
||||
* two circles touches the at the center of the overall group avatar and the
|
||||
* distance from the center of the circle to the corner of the group avatar is
|
||||
* radius * squareRoot(2). Therefore, the following emerges.
|
||||
*
|
||||
* w * squareRoot(2) = 2 (radius + radius * squareRoot(2))
|
||||
* Solving for radius we get:
|
||||
* d = 2 * radius = ( squareRoot(2) / (squareRoot(2) + 1)) * w
|
||||
* d = (2 - squareRoot(2)) * w
|
||||
*/
|
||||
final float diameter = (float) ((2 - Math.sqrt(2)) * width);
|
||||
destArray[0] = new RectF(0, 0, diameter, diameter);
|
||||
destArray[1] = new RectF(width - diameter, height - diameter, width, height);
|
||||
break;
|
||||
case 3:
|
||||
/**
|
||||
* +-------+
|
||||
* | | 0 | |
|
||||
* +-------+
|
||||
* | 1 | 2 |
|
||||
* +-------+
|
||||
* i0
|
||||
* |\
|
||||
* a | \ c
|
||||
* --- i2
|
||||
* b
|
||||
*
|
||||
* a = radius * squareRoot(3) due to the triangle being a 30-60-90 right triangle.
|
||||
* b = radius of circle
|
||||
* c = 2 * radius of circle
|
||||
*
|
||||
* All three of the images are circles and therefore image zero will not touch
|
||||
* image one or image two. Move image zero down so it touches image one and image
|
||||
* two. This can be done by keeping image zero in the center and moving it down
|
||||
* slightly. The amount to move down can be calculated by solving a right triangle.
|
||||
* We know that the center x of image two to the center x of image zero is the
|
||||
* radius of the circle, this is the length of edge b. Also we know that the
|
||||
* distance from image zero to image two's center is 2 * radius, edge c. From this
|
||||
* we know that the distance from center y of image two to center y of image one,
|
||||
* edge a, is equal to radius * squareRoot(3) due to this triangle being a 30-60-90
|
||||
* right triangle.
|
||||
*/
|
||||
final float quarterWidth = width / 4F;
|
||||
final float threeQuarterWidth = 3 * quarterWidth;
|
||||
final float radius = height / 4F;
|
||||
final float imageTwoCenterY = height - radius;
|
||||
final float lengthOfEdgeA = (float) (radius * Math.sqrt(3));
|
||||
final float imageZeroCenterY = imageTwoCenterY - lengthOfEdgeA;
|
||||
final float imageZeroTop = imageZeroCenterY - radius;
|
||||
final float imageZeroBottom = imageZeroCenterY + radius;
|
||||
destArray[0] = new RectF(
|
||||
quarterWidth, imageZeroTop, threeQuarterWidth, imageZeroBottom);
|
||||
destArray[1] = new RectF(0, halfHeight, halfWidth, height);
|
||||
destArray[2] = new RectF(halfWidth, halfHeight, width, height);
|
||||
break;
|
||||
default:
|
||||
/**
|
||||
* +-------+
|
||||
* | 0 | 1 |
|
||||
* +-------+
|
||||
* | 2 | 3 |
|
||||
* +-------+
|
||||
*/
|
||||
destArray[0] = new RectF(0, 0, halfWidth, halfHeight);
|
||||
destArray[1] = new RectF(halfWidth, 0, width, halfHeight);
|
||||
destArray[2] = new RectF(0, halfHeight, halfWidth, height);
|
||||
destArray[3] = new RectF(halfWidth, halfHeight, width, height);
|
||||
break;
|
||||
}
|
||||
return destArray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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.media;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.VectorDrawable;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.AvatarUriUtil;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.UriUtil;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
public class AvatarRequest extends UriImageRequest<AvatarRequestDescriptor> {
|
||||
private static final float SCALING_FACTOR = 1.33f;
|
||||
|
||||
private final TypedArray mColors;
|
||||
|
||||
public AvatarRequest(final Context context,
|
||||
final AvatarRequestDescriptor descriptor) {
|
||||
super(context, descriptor);
|
||||
mColors = mContext.getResources().obtainTypedArray(R.array.letter_tile_colors);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStreamForResource() throws FileNotFoundException {
|
||||
if (UriUtil.isLocalResourceUri(mDescriptor.uri)) {
|
||||
return super.getInputStreamForResource();
|
||||
} else {
|
||||
final Uri primaryUri = AvatarUriUtil.getPrimaryUri(mDescriptor.uri);
|
||||
Assert.isTrue(UriUtil.isLocalResourceUri(primaryUri));
|
||||
return mContext.getContentResolver().openInputStream(primaryUri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We can load multiple types of images for avatars depending on the uri. The uri should be
|
||||
* built by {@link com.android.messaging.util.AvatarUriUtil} which will decide on
|
||||
* what uri to build based on the available profile photo and name. Here we will check if the
|
||||
* image is a local resource (ie profile photo uri), if the resource isn't a local one we will
|
||||
* generate a tile with the first letter of the name.
|
||||
*/
|
||||
@Override
|
||||
protected ImageResource loadMediaInternal(List<MediaRequest<ImageResource>> chainedTasks)
|
||||
throws IOException {
|
||||
Assert.isNotMainThread();
|
||||
String avatarType = AvatarUriUtil.getAvatarType(mDescriptor.uri);
|
||||
Bitmap bitmap = null;
|
||||
int orientation = ExifInterface.ORIENTATION_NORMAL;
|
||||
final boolean isLocalResourceUri = UriUtil.isLocalResourceUri(mDescriptor.uri) ||
|
||||
AvatarUriUtil.TYPE_LOCAL_RESOURCE_URI.equals(avatarType);
|
||||
if (isLocalResourceUri) {
|
||||
try {
|
||||
ImageResource imageResource = super.loadMediaInternal(chainedTasks);
|
||||
bitmap = imageResource.getBitmap();
|
||||
orientation = imageResource.mOrientation;
|
||||
} catch (Exception ex) {
|
||||
// If we encountered any exceptions trying to load the local avatar resource,
|
||||
// fall back to generated avatar.
|
||||
LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, "AvatarRequest: failed to load local avatar " +
|
||||
"resource, switching to fallback rendering", ex);
|
||||
}
|
||||
}
|
||||
|
||||
final int width = mDescriptor.desiredWidth;
|
||||
final int height = mDescriptor.desiredHeight;
|
||||
// Check to see if we already got the bitmap. If not get a fallback avatar
|
||||
if (bitmap == null) {
|
||||
Uri generatedUri = mDescriptor.uri;
|
||||
if (isLocalResourceUri) {
|
||||
// If we are here, we just failed to load the local resource. Use the fallback Uri
|
||||
// if possible.
|
||||
generatedUri = AvatarUriUtil.getFallbackUri(mDescriptor.uri);
|
||||
if (generatedUri == null) {
|
||||
// No fallback Uri was provided, use the default avatar.
|
||||
generatedUri = AvatarUriUtil.DEFAULT_BACKGROUND_AVATAR;
|
||||
}
|
||||
}
|
||||
|
||||
avatarType = AvatarUriUtil.getAvatarType(generatedUri);
|
||||
if (AvatarUriUtil.TYPE_LETTER_TILE_URI.equals(avatarType)) {
|
||||
final String name = AvatarUriUtil.getName(generatedUri);
|
||||
bitmap = renderLetterTile(name, width, height);
|
||||
} else {
|
||||
bitmap = renderDefaultAvatar(width, height);
|
||||
}
|
||||
}
|
||||
return new DecodedImageResource(getKey(), bitmap, orientation);
|
||||
}
|
||||
|
||||
private Bitmap renderDefaultAvatar(final int width, final int height) {
|
||||
final Bitmap bitmap = getBitmapPool().createOrReuseBitmap(width, height,
|
||||
getBackgroundColor(AvatarUriUtil.getIdentifier(mDescriptor.uri)));
|
||||
final Canvas canvas = new Canvas(bitmap);
|
||||
final VectorDrawable defaultPerson = (VectorDrawable) ResourcesCompat.getDrawable(
|
||||
mContext.getResources(), R.drawable.ic_person_light, mContext.getTheme());
|
||||
float dstWidth = Math.min(defaultPerson.getIntrinsicWidth() * SCALING_FACTOR, width);
|
||||
float dstHeight = Math.min(defaultPerson.getIntrinsicHeight() * SCALING_FACTOR, height);
|
||||
|
||||
canvas.translate((width - dstWidth) / 2, (height - dstHeight) / 2);
|
||||
defaultPerson.setBounds(0, 0, (int)dstWidth, (int)dstHeight);
|
||||
defaultPerson.draw(canvas);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private Bitmap renderLetterTile(final String name, final int width, final int height) {
|
||||
final float halfWidth = width / 2;
|
||||
final float halfHeight = height / 2;
|
||||
final int minOfWidthAndHeight = Math.min(width, height);
|
||||
final Bitmap bitmap = getBitmapPool().createOrReuseBitmap(width, height,
|
||||
getBackgroundColor(AvatarUriUtil.getIdentifier(mDescriptor.uri)));
|
||||
final Resources resources = mContext.getResources();
|
||||
final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
paint.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
|
||||
paint.setColor(resources.getColor(R.color.letter_tile_font_color));
|
||||
final float letterToTileRatio = resources.getFraction(R.dimen.letter_to_tile_ratio, 1, 1);
|
||||
paint.setTextSize(letterToTileRatio * minOfWidthAndHeight);
|
||||
|
||||
final String firstCharString = name.substring(0, 1).toUpperCase();
|
||||
final Rect textBound = new Rect();
|
||||
paint.getTextBounds(firstCharString, 0, 1, textBound);
|
||||
|
||||
final Canvas canvas = new Canvas(bitmap);
|
||||
final float xOffset = halfWidth - textBound.centerX();
|
||||
final float yOffset = halfHeight - textBound.centerY();
|
||||
canvas.drawText(firstCharString, xOffset, yOffset, paint);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private int getBackgroundColor(final String identifier) {
|
||||
if (!TextUtils.isEmpty(identifier) &&
|
||||
mContext.getResources().getBoolean(R.bool.contact_colors)) {
|
||||
int idcolor = Math.abs(identifier.hashCode()) % mColors.length();
|
||||
return mColors.getColor(idcolor,
|
||||
mContext.getResources().getColor(R.color.primary_color));
|
||||
} else {
|
||||
return mContext.getResources().getColor(R.color.primary_color);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCacheId() {
|
||||
return BugleMediaCacheManager.AVATAR_IMAGE_CACHE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.media;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.AvatarUriUtil;
|
||||
import com.android.messaging.util.ImageUtils;
|
||||
import com.android.messaging.util.UriUtil;
|
||||
|
||||
public class AvatarRequestDescriptor extends UriImageRequestDescriptor {
|
||||
|
||||
public AvatarRequestDescriptor(final Uri uri, final int desiredWidth,
|
||||
final int desiredHeight) {
|
||||
this(uri, desiredWidth, desiredHeight, true /* cropToCircle */);
|
||||
}
|
||||
|
||||
public AvatarRequestDescriptor(final Uri uri, final int desiredWidth,
|
||||
final int desiredHeight, final boolean cropToCircle) {
|
||||
this(uri, desiredWidth, desiredHeight, cropToCircle, false /* isWearBackground */);
|
||||
}
|
||||
|
||||
public AvatarRequestDescriptor(final Uri uri, final int desiredWidth,
|
||||
final int desiredHeight, boolean cropToCircle, boolean isWearBackground) {
|
||||
super(uri, desiredWidth, desiredHeight, false /* allowCompression */, true /* isStatic */,
|
||||
cropToCircle,
|
||||
ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */,
|
||||
ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */);
|
||||
Assert.isTrue(uri == null || UriUtil.isLocalResourceUri(uri) ||
|
||||
AvatarUriUtil.isAvatarUri(uri));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaRequest<ImageResource> buildSyncMediaRequest(final Context context) {
|
||||
final String avatarType = uri == null ? null : AvatarUriUtil.getAvatarType(uri);
|
||||
if (AvatarUriUtil.TYPE_SIM_SELECTOR_URI.equals(avatarType)) {
|
||||
return new SimSelectorAvatarRequest(context, this);
|
||||
} else {
|
||||
return new AvatarRequest(context, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.media;
|
||||
|
||||
import com.android.messaging.datamodel.binding.BindableOnceData;
|
||||
import com.android.messaging.datamodel.media.MediaResourceManager.MediaResourceLoadListener;
|
||||
|
||||
/**
|
||||
* The {@link MediaRequest} interface is threading-model-oblivious, allowing the implementations to
|
||||
* be processed synchronously or asynchronously.
|
||||
* This is a {@link MediaRequest} implementation that includes functionalities such as binding and
|
||||
* event callbacks for multi-threaded media request processing.
|
||||
*/
|
||||
public abstract class BindableMediaRequest<T extends RefCountedMediaResource>
|
||||
extends BindableOnceData
|
||||
implements MediaRequest<T>, MediaResourceLoadListener<T> {
|
||||
private MediaResourceLoadListener<T> mListener;
|
||||
|
||||
public BindableMediaRequest(final MediaResourceLoadListener<T> listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates the media resource callback to the listener. Performs binding check to ensure
|
||||
* the listener is still bound to this request.
|
||||
*/
|
||||
@Override
|
||||
public void onMediaResourceLoaded(final MediaRequest<T> request, final T resource,
|
||||
final boolean cached) {
|
||||
if (isBound() && mListener != null) {
|
||||
mListener.onMediaResourceLoaded(request, resource, cached);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates the media resource callback to the listener. Performs binding check to ensure
|
||||
* the listener is still bound to this request.
|
||||
*/
|
||||
@Override
|
||||
public void onMediaResourceLoadError(final MediaRequest<T> request, final Exception exception) {
|
||||
if (isBound() && mListener != null) {
|
||||
mListener.onMediaResourceLoadError(request, exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unregisterListeners() {
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.media;
|
||||
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
/**
|
||||
* An implementation of {@link MediaCacheManager} that creates caches specific to Bugle's needs.
|
||||
*
|
||||
* To create a new type of cache, add to the list of cache ids and create a new MediaCache<>
|
||||
* for your cache id / media resource type in createMediaCacheById().
|
||||
*/
|
||||
public class BugleMediaCacheManager extends MediaCacheManager {
|
||||
// List of available cache ids.
|
||||
public static final int DEFAULT_IMAGE_CACHE = 1;
|
||||
public static final int AVATAR_IMAGE_CACHE = 2;
|
||||
public static final int VCARD_CACHE = 3;
|
||||
|
||||
// VCard cache size - we compute the size by count, not by bytes.
|
||||
private static final int VCARD_CACHE_SIZE = 5;
|
||||
private static final int SHARED_IMAGE_CACHE_SIZE = 1024 * 10; // 10MB
|
||||
|
||||
@Override
|
||||
protected MediaCache<?> createMediaCacheById(final int id) {
|
||||
switch (id) {
|
||||
case DEFAULT_IMAGE_CACHE:
|
||||
return new PoolableImageCache(SHARED_IMAGE_CACHE_SIZE, id, "DefaultImageCache");
|
||||
|
||||
case AVATAR_IMAGE_CACHE:
|
||||
return new PoolableImageCache(id, "AvatarImageCache");
|
||||
|
||||
case VCARD_CACHE:
|
||||
return new MediaCache<VCardResource>(VCARD_CACHE_SIZE, id, "VCardCache");
|
||||
|
||||
default:
|
||||
Assert.fail("BugleMediaCacheManager: unsupported cache id " + id);
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.media;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.ImageUtils;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Requests a composite image resource. The composite image resource is constructed by first
|
||||
* sequentially requesting a number of sub image resources specified by
|
||||
* {@link CompositeImageRequestDescriptor#getChildRequestDescriptors()}. After this, the
|
||||
* individual sub images are composed into the final image onto their respective target rects
|
||||
* returned by {@link CompositeImageRequestDescriptor#getChildRequestTargetRects()}.
|
||||
*/
|
||||
public class CompositeImageRequest<D extends CompositeImageRequestDescriptor>
|
||||
extends ImageRequest<D> {
|
||||
private final Bitmap mBitmap;
|
||||
private final Canvas mCanvas;
|
||||
private final Paint mPaint;
|
||||
|
||||
public CompositeImageRequest(final Context context, final D descriptor) {
|
||||
super(context, descriptor);
|
||||
mBitmap = getBitmapPool().createOrReuseBitmap(
|
||||
mDescriptor.desiredWidth, mDescriptor.desiredHeight);
|
||||
mCanvas = new Canvas(mBitmap);
|
||||
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ImageResource loadMediaInternal(List<MediaRequest<ImageResource>> chainedTask) {
|
||||
final List<? extends ImageRequestDescriptor> descriptors =
|
||||
mDescriptor.getChildRequestDescriptors();
|
||||
final List<RectF> targetRects = mDescriptor.getChildRequestTargetRects();
|
||||
Assert.equals(descriptors.size(), targetRects.size());
|
||||
Assert.isTrue(descriptors.size() > 1);
|
||||
|
||||
for (int i = 0; i < descriptors.size(); i++) {
|
||||
final MediaRequest<ImageResource> request =
|
||||
descriptors.get(i).buildSyncMediaRequest(mContext);
|
||||
// Synchronously request the child image.
|
||||
final ImageResource resource =
|
||||
MediaResourceManager.get().requestMediaResourceSync(request);
|
||||
if (resource != null) {
|
||||
try {
|
||||
final RectF avatarDestOnGroup = targetRects.get(i);
|
||||
|
||||
// Draw the bitmap into a smaller size with a circle mask.
|
||||
final Bitmap resourceBitmap = resource.getBitmap();
|
||||
final RectF resourceRect = new RectF(
|
||||
0, 0, resourceBitmap.getWidth(), resourceBitmap.getHeight());
|
||||
final Bitmap smallCircleBitmap = getBitmapPool().createOrReuseBitmap(
|
||||
Math.round(avatarDestOnGroup.width()),
|
||||
Math.round(avatarDestOnGroup.height()));
|
||||
final RectF smallCircleRect = new RectF(
|
||||
0, 0, smallCircleBitmap.getWidth(), smallCircleBitmap.getHeight());
|
||||
final Canvas smallCircleCanvas = new Canvas(smallCircleBitmap);
|
||||
ImageUtils.drawBitmapWithCircleOnCanvas(resource.getBitmap(), smallCircleCanvas,
|
||||
resourceRect, smallCircleRect, null /* bitmapPaint */,
|
||||
false /* fillBackground */,
|
||||
ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */,
|
||||
ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */);
|
||||
final Matrix matrix = new Matrix();
|
||||
matrix.setRectToRect(smallCircleRect, avatarDestOnGroup,
|
||||
Matrix.ScaleToFit.FILL);
|
||||
mCanvas.drawBitmap(smallCircleBitmap, matrix, mPaint);
|
||||
} finally {
|
||||
resource.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new DecodedImageResource(getKey(), mBitmap, ExifInterface.ORIENTATION_NORMAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCacheId() {
|
||||
return BugleMediaCacheManager.AVATAR_IMAGE_CACHE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStreamForResource() throws FileNotFoundException {
|
||||
throw new IllegalStateException("Composite image request doesn't support input stream!");
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.media;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.RectF;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class CompositeImageRequestDescriptor extends ImageRequestDescriptor {
|
||||
protected final List<? extends ImageRequestDescriptor> mDescriptors;
|
||||
private final String mKey;
|
||||
|
||||
public CompositeImageRequestDescriptor(final List<? extends ImageRequestDescriptor> descriptors,
|
||||
final int desiredWidth, final int desiredHeight) {
|
||||
super(desiredWidth, desiredHeight);
|
||||
mDescriptors = descriptors;
|
||||
|
||||
final String[] keyParts = new String[descriptors.size()];
|
||||
for (int i = 0; i < descriptors.size(); i++) {
|
||||
keyParts[i] = descriptors.get(i).getKey();
|
||||
}
|
||||
mKey = Joiner.on(",").skipNulls().join(keyParts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a key that uniquely identify all the underlying image resource to be loaded (e.g. Uri or
|
||||
* file path).
|
||||
*/
|
||||
@Override
|
||||
public String getKey() {
|
||||
return mKey;
|
||||
}
|
||||
|
||||
public List<? extends ImageRequestDescriptor> getChildRequestDescriptors(){
|
||||
return mDescriptors;
|
||||
}
|
||||
|
||||
public abstract List<RectF> getChildRequestTargetRects();
|
||||
public abstract CompositeImageRequest<?> buildBatchImageRequest(final Context context);
|
||||
|
||||
@Override
|
||||
public MediaRequest<ImageResource> buildSyncMediaRequest(final Context context) {
|
||||
return buildBatchImageRequest(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.media;
|
||||
|
||||
import android.accounts.Account;
|
||||
import androidx.collection.ArrayMap;
|
||||
|
||||
import com.android.vcard.VCardEntry;
|
||||
import com.android.vcard.VCardProperty;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Class which extends VCardEntry to add support for unknown properties. Currently there is a TODO
|
||||
* to add this in the VCardEntry code, but we have to extend it to add the needed support
|
||||
*/
|
||||
public class CustomVCardEntry extends VCardEntry {
|
||||
// List of properties keyed by their name for easy lookup
|
||||
private final Map<String, VCardProperty> mAllProperties;
|
||||
|
||||
public CustomVCardEntry(int vCardType, Account account) {
|
||||
super(vCardType, account);
|
||||
mAllProperties = new ArrayMap<String, VCardProperty>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addProperty(VCardProperty property) {
|
||||
super.addProperty(property);
|
||||
mAllProperties.put(property.getName(), property);
|
||||
}
|
||||
|
||||
public VCardProperty getProperty(String name) {
|
||||
return mAllProperties.get(name);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user