| /* |
| * 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 androidx.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); |
| } |
| } |
| } |
| } |