summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/datamodel/data/ConversationData.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/messaging/datamodel/data/ConversationData.java')
-rw-r--r--src/com/android/messaging/datamodel/data/ConversationData.java849
1 files changed, 849 insertions, 0 deletions
diff --git a/src/com/android/messaging/datamodel/data/ConversationData.java b/src/com/android/messaging/datamodel/data/ConversationData.java
new file mode 100644
index 0000000..d504928
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/ConversationData.java
@@ -0,0 +1,849 @@
+/*
+ * 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.app.LoaderManager;
+import android.content.Context;
+import android.content.Loader;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.database.sqlite.SQLiteFullException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.android.common.contacts.DataUsageStatUpdater;
+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.ContactUtil;
+import com.android.messaging.util.LogUtil;
+import com.android.messaging.util.OsUtil;
+import com.android.messaging.util.PhoneUtils;
+import com.android.messaging.util.SafeAsyncTask;
+import com.android.messaging.widget.WidgetConversationProvider;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+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;
+
+ /**
+ * Takes a conversation id and a list of message ids and computes the positions
+ * for each message.
+ */
+ public List<Integer> getPositions(final String conversationId, final List<Long> ids) {
+ final ArrayList<Integer> result = new ArrayList<Integer>();
+
+ if (ids.isEmpty()) {
+ return result;
+ }
+
+ final Cursor c = new ConversationData.ReversedCursor(
+ DataModel.get().getDatabase().rawQuery(
+ ConversationMessageData.getConversationMessageIdsQuerySql(),
+ new String [] { conversationId }));
+ if (c != null) {
+ try {
+ final Set<Long> idsSet = new HashSet<Long>(ids);
+ if (c.moveToLast()) {
+ do {
+ final long messageId = c.getLong(0);
+ if (idsSet.contains(messageId)) {
+ result.add(c.getPosition());
+ }
+ } while (c.moveToPrevious());
+ }
+ } finally {
+ c.close();
+ }
+ }
+ Collections.sort(result);
+ return result;
+ }
+
+ public interface ConversationDataListener {
+ public void onConversationMessagesCursorUpdated(ConversationData data, Cursor cursor,
+ @Nullable ConversationMessageData newestMessage, boolean isSync);
+ public void onConversationMetadataUpdated(ConversationData data);
+ public void closeConversation(String conversationId);
+ public void onConversationParticipantDataLoaded(ConversationData data);
+ public 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> {
+ @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(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(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> {
+ @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(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(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> {
+ @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(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(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> {
+ @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(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(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);
+ }
+
+ @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 (!OsUtil.isAtLeastL_MR1() || 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());
+ }
+ }
+ }
+
+ if (ContactUtil.hasReadContactsPermission()) {
+ SafeAsyncTask.executeOnThreadPool(new Runnable() {
+ @Override
+ public void run() {
+ final DataUsageStatUpdater updater = new DataUsageStatUpdater(
+ Factory.get().getApplicationContext());
+ try {
+ if (!phones.isEmpty()) {
+ updater.updateWithPhoneNumber(phones);
+ }
+ if (!emails.isEmpty()) {
+ updater.updateWithAddress(emails);
+ }
+ } catch (final SQLiteFullException ex) {
+ LogUtil.w(TAG, "Unable to update contact", ex);
+ }
+ }
+ });
+ }
+ }
+
+ 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 (OsUtil.isAtLeastL_MR1() &&
+ selfParticipantsData.getSelfParticipantsCountExcludingDefault(true) > 1) {
+ return subscriptionListData.getActiveSubscriptionEntryBySelfId(selfParticipantId,
+ excludeDefault);
+ }
+ return null;
+ }
+
+ public SubscriptionListData getSubscriptionListData() {
+ return mSubscriptionListData;
+ }
+
+ /**
+ * A dummy 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);
+ }
+ }
+ }
+}