Initial checkin of AOSP Messaging app.
b/23110861 Change-Id: I11db999bd10656801e618f78ab2b2ef74136fff1
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user