Revert "Initial checkin of AOSP Messaging app."

This reverts commit 461a34b466.

Change-Id: Iac4ca77eeaa94989e91dead49a7959c905bd3078
This commit is contained in:
Mike Dodd
2015-08-12 18:48:39 +00:00
parent 461a34b466
commit ef8c7abbcf
1645 changed files with 0 additions and 186271 deletions
@@ -1,103 +0,0 @@
/*
* 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.net.Uri;
import android.os.Bundle;
import com.android.messaging.datamodel.BoundCursorLoader;
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.binding.BindableData;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.util.Assert;
/**
* Services data needs for BlockedParticipantsFragment
*/
public class BlockedParticipantsData extends BindableData implements
LoaderManager.LoaderCallbacks<Cursor> {
public interface BlockedParticipantsDataListener {
public void onBlockedParticipantsCursorUpdated(final Cursor cursor);
}
private static final String BINDING_ID = "bindingId";
private static final int BLOCKED_PARTICIPANTS_LOADER = 1;
private final Context mContext;
private LoaderManager mLoaderManager;
private BlockedParticipantsDataListener mListener;
public BlockedParticipantsData(final Context context,
final BlockedParticipantsDataListener listener) {
mContext = context;
mListener = listener;
}
@Override
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
Assert.isTrue(id == BLOCKED_PARTICIPANTS_LOADER);
final String bindingId = args.getString(BINDING_ID);
// Check if data still bound to the requesting ui element
if (isBound(bindingId)) {
final Uri uri = MessagingContentProvider.PARTICIPANTS_URI;
return new BoundCursorLoader(bindingId, mContext, uri,
ParticipantData.ParticipantsQuery.PROJECTION,
ParticipantColumns.BLOCKED + "=1", null, null);
}
return null;
}
@Override
public void onLoadFinished(final Loader<Cursor> loader, final Cursor cursor) {
Assert.isTrue(loader.getId() == BLOCKED_PARTICIPANTS_LOADER);
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
Assert.isTrue(isBound(cursorLoader.getBindingId()));
mListener.onBlockedParticipantsCursorUpdated(cursor);
}
@Override
public void onLoaderReset(final Loader<Cursor> loader) {
Assert.isTrue(loader.getId() == BLOCKED_PARTICIPANTS_LOADER);
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
Assert.isTrue(isBound(cursorLoader.getBindingId()));
mListener.onBlockedParticipantsCursorUpdated(null);
}
public void init(final LoaderManager loaderManager,
final BindingBase<BlockedParticipantsData> binding) {
final Bundle args = new Bundle();
args.putString(BINDING_ID, binding.getBindingId());
mLoaderManager = loaderManager;
mLoaderManager.initLoader(BLOCKED_PARTICIPANTS_LOADER, args, this);
}
@Override
protected void unregisterListeners() {
mListener = null;
if (mLoaderManager != null) {
mLoaderManager.destroyLoader(BLOCKED_PARTICIPANTS_LOADER);
mLoaderManager = null;
}
}
public ParticipantListItemData createParticipantListItemData(Cursor cursor) {
return new ParticipantListItemData(ParticipantData.getFromCursor(cursor));
}
}
@@ -1,160 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract.DisplayNameSources;
import com.android.ex.chips.RecipientEntry;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ContactRecipientEntryUtils;
import com.android.messaging.util.ContactUtil;
/**
* Data model object used to power ContactListItemViews, which may be displayed either in
* our contact list, or in the chips UI search drop down presented by ContactDropdownLayouter.
*/
public class ContactListItemData {
// Keeps the contact data in the form of RecipientEntry that RecipientEditTextView can
// directly use.
private RecipientEntry mRecipientEntry;
private CharSequence mStyledName;
private CharSequence mStyledDestination;
// If this contact is the first in the list for its first letter, then this will be the
// first letter, otherwise this is null.
private String mAlphabetHeader;
// Is the contact the only item in the list (happens when the user clicks on an
// existing chip for which we show full contact detail for the selected contact).
private boolean mSingleRecipient;
/**
* Bind to a contact cursor in the contact list.
*/
public void bind(final Cursor cursor, final String alphabetHeader) {
final long dataId = cursor.getLong(ContactUtil.INDEX_DATA_ID);
final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID);
final String lookupKey = cursor.getString(ContactUtil.INDEX_LOOKUP_KEY);
final String displayName = cursor.getString(ContactUtil.INDEX_DISPLAY_NAME);
final String photoThumbnailUri = cursor.getString(ContactUtil.INDEX_PHOTO_URI);
final String destination = cursor.getString(ContactUtil.INDEX_PHONE_EMAIL);
final int destinationType = cursor.getInt(ContactUtil.INDEX_PHONE_EMAIL_TYPE);
final String destinationLabel = cursor.getString(ContactUtil.INDEX_PHONE_EMAIL_LABEL);
mStyledName = null;
mStyledDestination = null;
mAlphabetHeader = alphabetHeader;
mSingleRecipient = false;
// Check whether this contact is first level (i.e. whether it's the first entry of this
// contact in the contact list).
boolean isFirstLevel = true;
if (!cursor.isFirst() && cursor.moveToPrevious()) {
final long contactIdPrevious = cursor.getLong(ContactUtil.INDEX_CONTACT_ID);
if (contactId == contactIdPrevious) {
isFirstLevel = false;
}
cursor.moveToNext();
}
mRecipientEntry = ContactUtil.createRecipientEntry(displayName,
DisplayNameSources.STRUCTURED_NAME, destination, destinationType, destinationLabel,
contactId, lookupKey, dataId, photoThumbnailUri, isFirstLevel);
}
/**
* Bind to a RecipientEntry produced by the chips text view in the search drop down, plus
* optional styled name & destination for showing bold search match.
*/
public void bind(final RecipientEntry entry, final CharSequence styledName,
final CharSequence styledDestination, final boolean singleRecipient) {
Assert.isTrue(entry.isValid());
mRecipientEntry = entry;
mStyledName = styledName;
mStyledDestination = styledDestination;
mAlphabetHeader = null;
mSingleRecipient = singleRecipient;
}
public CharSequence getDisplayName() {
final CharSequence displayName = mStyledName != null ? mStyledName :
ContactRecipientEntryUtils.getDisplayNameForContactList(mRecipientEntry);
return displayName == null ? "" : displayName;
}
public Uri getPhotoThumbnailUri() {
return mRecipientEntry.getPhotoThumbnailUri() == null ? null :
mRecipientEntry.getPhotoThumbnailUri();
}
public CharSequence getDestination() {
final CharSequence destination = mStyledDestination != null ?
mStyledDestination : ContactRecipientEntryUtils.formatDestination(mRecipientEntry);
return destination == null ? "" : destination;
}
public int getDestinationType() {
return mRecipientEntry.getDestinationType();
}
public String getDestinationLabel() {
return mRecipientEntry.getDestinationLabel();
}
public long getContactId() {
return mRecipientEntry.getContactId();
}
public String getLookupKey() {
return mRecipientEntry.getLookupKey();
}
/**
* Returns if this item is "first-level," i.e. whether it's the first entry of the contact
* that it represents in the list. For example, if John Smith has 3 different phone numbers,
* then the first number is considered first-level, while the other two are considered
* second-level.
*/
public boolean getIsFirstLevel() {
// Treat the item as first level if it's a top-level recipient entry, or if it's the only
// item in the list.
return mRecipientEntry.isFirstLevel() || mSingleRecipient;
}
/**
* Returns if this item is simple, i.e. it has only avatar and a display name with phone number
* embedded so we can hide everything else.
*/
public boolean getIsSimpleContactItem() {
return ContactRecipientEntryUtils.isAvatarAndNumberOnlyContact(mRecipientEntry) ||
ContactRecipientEntryUtils.isSendToDestinationContact(mRecipientEntry);
}
public String getAlphabetHeader() {
return mAlphabetHeader;
}
/**
* Returns a RecipientEntry instance readily usable by the RecipientEditTextView.
*/
public RecipientEntry getRecipientEntry() {
return mRecipientEntry;
}
}
@@ -1,194 +0,0 @@
/*
* 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.os.Bundle;
import com.android.messaging.datamodel.BoundCursorLoader;
import com.android.messaging.datamodel.FrequentContactsCursorBuilder;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.binding.BindableData;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.sms.MmsConfig;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ContactUtil;
import com.android.messaging.util.LogUtil;
/**
* Class to access phone contacts.
* The caller is responsible for ensuring that the app has READ_CONTACTS permission (see
* {@link ContactUtil#hasReadContactsPermission()}) before instantiating this class.
*/
public class ContactPickerData extends BindableData implements
LoaderManager.LoaderCallbacks<Cursor> {
public interface ContactPickerDataListener {
void onAllContactsCursorUpdated(Cursor data);
void onFrequentContactsCursorUpdated(Cursor data);
void onContactCustomColorLoaded(ContactPickerData data);
}
private static final String BINDING_ID = "bindingId";
private final Context mContext;
private LoaderManager mLoaderManager;
private ContactPickerDataListener mListener;
private final FrequentContactsCursorBuilder mFrequentContactsCursorBuilder;
public ContactPickerData(final Context context, final ContactPickerDataListener listener) {
mListener = listener;
mContext = context;
mFrequentContactsCursorBuilder = new FrequentContactsCursorBuilder();
}
private static final int ALL_CONTACTS_LOADER = 1;
private static final int FREQUENT_CONTACTS_LOADER = 2;
private static final int PARTICIPANT_LOADER = 3;
@Override
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
final String bindingId = args.getString(BINDING_ID);
// Check if data still bound to the requesting ui element
if (isBound(bindingId)) {
switch (id) {
case ALL_CONTACTS_LOADER:
return ContactUtil.getPhones(mContext)
.createBoundCursorLoader(bindingId);
case FREQUENT_CONTACTS_LOADER:
return ContactUtil.getFrequentContacts(mContext)
.createBoundCursorLoader(bindingId);
case PARTICIPANT_LOADER:
return new BoundCursorLoader(bindingId, mContext,
MessagingContentProvider.PARTICIPANTS_URI,
ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
default:
Assert.fail("Unknown loader id for contact picker!");
break;
}
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "Loader created after unbinding the contacts list");
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) {
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
if (isBound(cursorLoader.getBindingId())) {
switch (loader.getId()) {
case ALL_CONTACTS_LOADER:
mListener.onAllContactsCursorUpdated(data);
mFrequentContactsCursorBuilder.setAllContacts(data);
break;
case FREQUENT_CONTACTS_LOADER:
mFrequentContactsCursorBuilder.setFrequents(data);
break;
case PARTICIPANT_LOADER:
mListener.onContactCustomColorLoaded(this);
break;
default:
Assert.fail("Unknown loader id for contact picker!");
break;
}
if (loader.getId() != PARTICIPANT_LOADER) {
// The frequent contacts cursor to be used in the UI depends on results from both
// all contacts and frequent contacts loader, and we don't know which will finish
// first. Therefore, try to build the cursor and notify the listener if it's
// successfully built.
final Cursor frequentContactsCursor = mFrequentContactsCursorBuilder.build();
if (frequentContactsCursor != null) {
mListener.onFrequentContactsCursorUpdated(frequentContactsCursor);
}
}
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "Loader finished after unbinding the contacts list");
}
}
/**
* {@inheritDoc}
*/
@Override
public void onLoaderReset(final Loader<Cursor> loader) {
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
if (isBound(cursorLoader.getBindingId())) {
switch (loader.getId()) {
case ALL_CONTACTS_LOADER:
mListener.onAllContactsCursorUpdated(null);
mFrequentContactsCursorBuilder.setAllContacts(null);
break;
case FREQUENT_CONTACTS_LOADER:
mListener.onFrequentContactsCursorUpdated(null);
mFrequentContactsCursorBuilder.setFrequents(null);
break;
case PARTICIPANT_LOADER:
mListener.onContactCustomColorLoaded(this);
break;
default:
Assert.fail("Unknown loader id for contact picker!");
break;
}
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "Loader reset after unbinding the contacts list");
}
}
public void init(final LoaderManager loaderManager,
final BindingBase<ContactPickerData> binding) {
final Bundle args = new Bundle();
args.putString(BINDING_ID, binding.getBindingId());
mLoaderManager = loaderManager;
mLoaderManager.initLoader(ALL_CONTACTS_LOADER, args, this);
mLoaderManager.initLoader(FREQUENT_CONTACTS_LOADER, args, this);
mLoaderManager.initLoader(PARTICIPANT_LOADER, args, this);
}
@Override
protected void unregisterListeners() {
mListener = null;
// This could be null if we bind but the caller doesn't init the BindableData
if (mLoaderManager != null) {
mLoaderManager.destroyLoader(ALL_CONTACTS_LOADER);
mLoaderManager.destroyLoader(FREQUENT_CONTACTS_LOADER);
mLoaderManager.destroyLoader(PARTICIPANT_LOADER);
mLoaderManager = null;
}
mFrequentContactsCursorBuilder.resetBuilder();
}
public static boolean isTooManyParticipants(final int participantCount) {
// When creating a conversation, the conversation will be created using the system's
// default SIM, so use the default MmsConfig's recipient limit.
return (participantCount > MmsConfig.get(ParticipantData.DEFAULT_SELF_SUB_ID)
.getRecipientLimit());
}
public static boolean getCanAddMoreParticipants(final int participantCount) {
// When creating a conversation, the conversation will be created using the system's
// default SIM, so use the default MmsConfig's recipient limit.
return (participantCount < MmsConfig.get(ParticipantData.DEFAULT_SELF_SUB_ID)
.getRecipientLimit());
}
}
@@ -1,849 +0,0 @@
/*
* 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);
}
}
}
}
@@ -1,211 +0,0 @@
/*
* 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.os.Bundle;
import com.android.messaging.datamodel.BoundCursorLoader;
import com.android.messaging.datamodel.BugleNotifications;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.SyncManager;
import com.android.messaging.datamodel.binding.BindableData;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.datamodel.data.ConversationListItemData.ConversationListViewColumns;
import com.android.messaging.receiver.SmsReceiver;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import java.util.HashSet;
public class ConversationListData extends BindableData
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
private static final String BINDING_ID = "bindingId";
public static final String SORT_ORDER =
ConversationListViewColumns.SORT_TIMESTAMP + " DESC";
private static final String WHERE_ARCHIVED =
"(" + ConversationListViewColumns.ARCHIVE_STATUS + " = 1)";
public static final String WHERE_NOT_ARCHIVED =
"(" + ConversationListViewColumns.ARCHIVE_STATUS + " = 0)";
public interface ConversationListDataListener {
public void onConversationListCursorUpdated(ConversationListData data, Cursor cursor);
public void setBlockedParticipantsAvailable(boolean blockedAvailable);
}
private ConversationListDataListener mListener;
private final Context mContext;
private final boolean mArchivedMode;
private LoaderManager mLoaderManager;
public ConversationListData(final Context context, final ConversationListDataListener listener,
final boolean archivedMode) {
mListener = listener;
mContext = context;
mArchivedMode = archivedMode;
}
private static final int CONVERSATION_LIST_LOADER = 1;
private static final int BLOCKED_PARTICIPANTS_AVAILABLE_LOADER = 2;
private static final String[] BLOCKED_PARTICIPANTS_PROJECTION = new String[] {
ParticipantColumns._ID,
ParticipantColumns.NORMALIZED_DESTINATION,
};
private static final int INDEX_BLOCKED_PARTICIPANTS_ID = 0;
private static final int INDEX_BLOCKED_PARTICIPANTS_NORMALIZED_DESTINATION = 1;
// all blocked participants
private final HashSet<String> mBlockedParticipants = new HashSet<String>();
@Override
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
final String bindingId = args.getString(BINDING_ID);
Loader<Cursor> loader = null;
// Check if data still bound to the requesting ui element
if (isBound(bindingId)) {
switch (id) {
case BLOCKED_PARTICIPANTS_AVAILABLE_LOADER:
loader = new BoundCursorLoader(bindingId, mContext,
MessagingContentProvider.PARTICIPANTS_URI,
BLOCKED_PARTICIPANTS_PROJECTION,
ParticipantColumns.BLOCKED + "=1", null, null);
break;
case CONVERSATION_LIST_LOADER:
loader = new BoundCursorLoader(bindingId, mContext,
MessagingContentProvider.CONVERSATIONS_URI,
ConversationListItemData.PROJECTION,
mArchivedMode ? WHERE_ARCHIVED : WHERE_NOT_ARCHIVED,
null, // selection args
SORT_ORDER);
break;
default:
Assert.fail("Unknown loader id");
break;
}
} else {
LogUtil.w(TAG, "Creating loader after unbinding list");
}
return loader;
}
/**
* {@inheritDoc}
*/
@Override
public void onLoadFinished(final Loader<Cursor> generic, final Cursor data) {
final BoundCursorLoader loader = (BoundCursorLoader) generic;
if (isBound(loader.getBindingId())) {
switch (loader.getId()) {
case BLOCKED_PARTICIPANTS_AVAILABLE_LOADER:
mBlockedParticipants.clear();
for (int i = 0; i < data.getCount(); i++) {
data.moveToPosition(i);
mBlockedParticipants.add(data.getString(
INDEX_BLOCKED_PARTICIPANTS_NORMALIZED_DESTINATION));
}
mListener.setBlockedParticipantsAvailable(data != null && data.getCount() > 0);
break;
case CONVERSATION_LIST_LOADER:
mListener.onConversationListCursorUpdated(this, data);
break;
default:
Assert.fail("Unknown loader id");
break;
}
} else {
LogUtil.w(TAG, "Loader finished after unbinding list");
}
}
/**
* {@inheritDoc}
*/
@Override
public void onLoaderReset(final Loader<Cursor> generic) {
final BoundCursorLoader loader = (BoundCursorLoader) generic;
if (isBound(loader.getBindingId())) {
switch (loader.getId()) {
case BLOCKED_PARTICIPANTS_AVAILABLE_LOADER:
mListener.setBlockedParticipantsAvailable(false);
break;
case CONVERSATION_LIST_LOADER:
mListener.onConversationListCursorUpdated(this, null);
break;
default:
Assert.fail("Unknown loader id");
break;
}
} else {
LogUtil.w(TAG, "Loader reset after unbinding list");
}
}
private Bundle mArgs;
public void init(final LoaderManager loaderManager,
final BindingBase<ConversationListData> binding) {
mArgs = new Bundle();
mArgs.putString(BINDING_ID, binding.getBindingId());
mLoaderManager = loaderManager;
mLoaderManager.initLoader(CONVERSATION_LIST_LOADER, mArgs, this);
mLoaderManager.initLoader(BLOCKED_PARTICIPANTS_AVAILABLE_LOADER, mArgs, this);
}
public void handleMessagesSeen() {
BugleNotifications.markAllMessagesAsSeen();
SmsReceiver.cancelSecondaryUserNotification();
}
@Override
protected void unregisterListeners() {
mListener = null;
// This could be null if we bind but the caller doesn't init the BindableData
if (mLoaderManager != null) {
mLoaderManager.destroyLoader(CONVERSATION_LIST_LOADER);
mLoaderManager.destroyLoader(BLOCKED_PARTICIPANTS_AVAILABLE_LOADER);
mLoaderManager = null;
}
}
public boolean getHasFirstSyncCompleted() {
final SyncManager syncManager = DataModel.get().getSyncManager();
return syncManager.getHasFirstSyncCompleted();
}
public void setScrolledToNewestConversation(boolean scrolledToNewestConversation) {
DataModel.get().setConversationListScrolledToNewestConversation(
scrolledToNewestConversation);
if (scrolledToNewestConversation) {
handleMessagesSeen();
}
}
public HashSet<String> getBlockedParticipants() {
return mBlockedParticipants;
}
}
@@ -1,510 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.text.TextUtils;
import com.android.messaging.datamodel.DatabaseHelper;
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.action.DeleteConversationAction;
import com.android.messaging.util.Assert;
import com.android.messaging.util.Dates;
import com.google.common.base.Joiner;
import java.util.ArrayList;
import java.util.List;
/**
* Class wrapping the conversation list view used to display each item in conversation list
*/
public class ConversationListItemData {
private String mConversationId;
private String mName;
private String mIcon;
private boolean mIsRead;
private long mTimestamp;
private String mSnippetText;
private Uri mPreviewUri;
private String mPreviewContentType;
private long mParticipantContactId;
private String mParticipantLookupKey;
private String mOtherParticipantNormalizedDestination;
private String mSelfId;
private int mParticipantCount;
private boolean mNotificationEnabled;
private String mNotificationSoundUri;
private boolean mNotificationVibrate;
private boolean mIncludeEmailAddress;
private int mMessageStatus;
private int mMessageRawTelephonyStatus;
private boolean mShowDraft;
private Uri mDraftPreviewUri;
private String mDraftPreviewContentType;
private String mDraftSnippetText;
private boolean mIsArchived;
private String mSubject;
private String mDraftSubject;
private String mSnippetSenderFirstName;
private String mSnippetSenderDisplayDestination;
public ConversationListItemData() {
}
public void bind(final Cursor cursor) {
bind(cursor, false);
}
public void bind(final Cursor cursor, final boolean ignoreDraft) {
mConversationId = cursor.getString(INDEX_ID);
mName = cursor.getString(INDEX_CONVERSATION_NAME);
mIcon = cursor.getString(INDEX_CONVERSATION_ICON);
mSnippetText = cursor.getString(INDEX_SNIPPET_TEXT);
mTimestamp = cursor.getLong(INDEX_SORT_TIMESTAMP);
mIsRead = cursor.getInt(INDEX_READ) == 1;
final String previewUriString = cursor.getString(INDEX_PREVIEW_URI);
mPreviewUri = TextUtils.isEmpty(previewUriString) ? null : Uri.parse(previewUriString);
mPreviewContentType = cursor.getString(INDEX_PREVIEW_CONTENT_TYPE);
mParticipantContactId = cursor.getLong(INDEX_PARTICIPANT_CONTACT_ID);
mParticipantLookupKey = cursor.getString(INDEX_PARTICIPANT_LOOKUP_KEY);
mOtherParticipantNormalizedDestination = cursor.getString(
INDEX_OTHER_PARTICIPANT_NORMALIZED_DESTINATION);
mSelfId = cursor.getString(INDEX_SELF_ID);
mParticipantCount = cursor.getInt(INDEX_PARTICIPANT_COUNT);
mNotificationEnabled = cursor.getInt(INDEX_NOTIFICATION_ENABLED) == 1;
mNotificationSoundUri = cursor.getString(INDEX_NOTIFICATION_SOUND_URI);
mNotificationVibrate = cursor.getInt(INDEX_NOTIFICATION_VIBRATION) == 1;
mIncludeEmailAddress = cursor.getInt(INDEX_INCLUDE_EMAIL_ADDRESS) == 1;
mMessageStatus = cursor.getInt(INDEX_MESSAGE_STATUS);
mMessageRawTelephonyStatus = cursor.getInt(INDEX_MESSAGE_RAW_TELEPHONY_STATUS);
if (!ignoreDraft) {
mShowDraft = cursor.getInt(INDEX_SHOW_DRAFT) == 1;
final String draftPreviewUriString = cursor.getString(INDEX_DRAFT_PREVIEW_URI);
mDraftPreviewUri = TextUtils.isEmpty(draftPreviewUriString) ?
null : Uri.parse(draftPreviewUriString);
mDraftPreviewContentType = cursor.getString(INDEX_DRAFT_PREVIEW_CONTENT_TYPE);
mDraftSnippetText = cursor.getString(INDEX_DRAFT_SNIPPET_TEXT);
mDraftSubject = cursor.getString(INDEX_DRAFT_SUBJECT_TEXT);
} else {
mShowDraft = false;
mDraftPreviewUri = null;
mDraftPreviewContentType = null;
mDraftSnippetText = null;
mDraftSubject = null;
}
mIsArchived = cursor.getInt(INDEX_ARCHIVE_STATUS) == 1;
mSubject = cursor.getString(INDEX_SUBJECT_TEXT);
mSnippetSenderFirstName = cursor.getString(INDEX_SNIPPET_SENDER_FIRST_NAME);
mSnippetSenderDisplayDestination =
cursor.getString(INDEX_SNIPPET_SENDER_DISPLAY_DESTINATION);
}
public String getConversationId() {
return mConversationId;
}
public String getName() {
return mName;
}
public String getIcon() {
return mIcon;
}
public boolean getIsRead() {
return mIsRead;
}
public String getFormattedTimestamp() {
return Dates.getConversationTimeString(mTimestamp).toString();
}
public long getTimestamp() {
return mTimestamp;
}
public String getSnippetText() {
return mSnippetText;
}
public Uri getPreviewUri() {
return mPreviewUri;
}
public String getPreviewContentType() {
return mPreviewContentType;
}
public long getParticipantContactId() {
return mParticipantContactId;
}
public String getParticipantLookupKey() {
return mParticipantLookupKey;
}
public String getOtherParticipantNormalizedDestination() {
return mOtherParticipantNormalizedDestination;
}
public String getSelfId() {
return mSelfId;
}
public int getParticipantCount() {
return mParticipantCount;
}
public boolean getIsGroup() {
// Participant count excludes self
return (mParticipantCount > 1);
}
public boolean getIncludeEmailAddress() {
return mIncludeEmailAddress;
}
public boolean getNotificationEnabled() {
return mNotificationEnabled;
}
public String getNotificationSoundUri() {
return mNotificationSoundUri;
}
public boolean getNotifiationVibrate() {
return mNotificationVibrate;
}
public final boolean getIsFailedStatus() {
return (mMessageStatus == MessageData.BUGLE_STATUS_OUTGOING_FAILED ||
mMessageStatus == MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER ||
mMessageStatus == MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED ||
mMessageStatus == MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE);
}
public final boolean getIsSendRequested() {
return (mMessageStatus == MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND ||
mMessageStatus == MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY ||
mMessageStatus == MessageData.BUGLE_STATUS_OUTGOING_SENDING ||
mMessageStatus == MessageData.BUGLE_STATUS_OUTGOING_RESENDING);
}
public boolean getIsMessageTypeOutgoing() {
return !MessageData.getIsIncoming(mMessageStatus);
}
public int getMessageRawTelephonyStatus() {
return mMessageRawTelephonyStatus;
}
public int getMessageStatus() {
return mMessageStatus;
}
public boolean getShowDraft() {
return mShowDraft;
}
public String getDraftSnippetText() {
return mDraftSnippetText;
}
public Uri getDraftPreviewUri() {
return mDraftPreviewUri;
}
public String getDraftPreviewContentType() {
return mDraftPreviewContentType;
}
public boolean getIsArchived() {
return mIsArchived;
}
public String getSubject() {
return mSubject;
}
public String getDraftSubject() {
return mDraftSubject;
}
public String getSnippetSenderName() {
if (!TextUtils.isEmpty(mSnippetSenderFirstName)) {
return mSnippetSenderFirstName;
}
return mSnippetSenderDisplayDestination;
}
public void deleteConversation() {
DeleteConversationAction.deleteConversation(mConversationId, mTimestamp);
}
/**
* Get the name of the view for this data item
*/
public static final String getConversationListView() {
return CONVERSATION_LIST_VIEW;
}
public static final String getConversationListViewSql() {
return CONVERSATION_LIST_VIEW_SQL;
}
private static final String CONVERSATION_LIST_VIEW = "conversation_list_view";
private static final String CONVERSATION_LIST_VIEW_PROJECTION =
DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns._ID
+ " as " + ConversationListViewColumns._ID + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.NAME
+ " as " + ConversationListViewColumns.NAME + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.CURRENT_SELF_ID
+ " as " + ConversationListViewColumns.CURRENT_SELF_ID + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.ARCHIVE_STATUS
+ " as " + ConversationListViewColumns.ARCHIVE_STATUS + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.READ
+ " as " + ConversationListViewColumns.READ + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.ICON
+ " as " + ConversationListViewColumns.ICON + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.PARTICIPANT_CONTACT_ID
+ " as " + ConversationListViewColumns.PARTICIPANT_CONTACT_ID + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.PARTICIPANT_LOOKUP_KEY
+ " as " + ConversationListViewColumns.PARTICIPANT_LOOKUP_KEY + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.'
+ ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION
+ " as " + ConversationListViewColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.SORT_TIMESTAMP
+ " as " + ConversationListViewColumns.SORT_TIMESTAMP + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.SHOW_DRAFT
+ " as " + ConversationListViewColumns.SHOW_DRAFT + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.DRAFT_SNIPPET_TEXT
+ " as " + ConversationListViewColumns.DRAFT_SNIPPET_TEXT + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.DRAFT_PREVIEW_URI
+ " as " + ConversationListViewColumns.DRAFT_PREVIEW_URI + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.DRAFT_SUBJECT_TEXT
+ " as " + ConversationListViewColumns.DRAFT_SUBJECT_TEXT + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.'
+ ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE
+ " as " + ConversationListViewColumns.DRAFT_PREVIEW_CONTENT_TYPE + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.PREVIEW_URI
+ " as " + ConversationListViewColumns.PREVIEW_URI + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.PREVIEW_CONTENT_TYPE
+ " as " + ConversationListViewColumns.PREVIEW_CONTENT_TYPE + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.PARTICIPANT_COUNT
+ " as " + ConversationListViewColumns.PARTICIPANT_COUNT + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.NOTIFICATION_ENABLED
+ " as " + ConversationListViewColumns.NOTIFICATION_ENABLED + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.NOTIFICATION_SOUND_URI
+ " as " + ConversationListViewColumns.NOTIFICATION_SOUND_URI + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.NOTIFICATION_VIBRATION
+ " as " + ConversationListViewColumns.NOTIFICATION_VIBRATION + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' +
ConversationColumns.INCLUDE_EMAIL_ADDRESS
+ " as " + ConversationListViewColumns.INCLUDE_EMAIL_ADDRESS + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.STATUS
+ " as " + ConversationListViewColumns.MESSAGE_STATUS + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RAW_TELEPHONY_STATUS
+ " as " + ConversationListViewColumns.MESSAGE_RAW_TELEPHONY_STATUS + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID
+ " as " + ConversationListViewColumns.MESSAGE_ID + ", "
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FIRST_NAME
+ " as " + ConversationListViewColumns.SNIPPET_SENDER_FIRST_NAME + ", "
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.DISPLAY_DESTINATION
+ " as " + ConversationListViewColumns.SNIPPET_SENDER_DISPLAY_DESTINATION;
private static final String JOIN_PARTICIPANTS =
" LEFT JOIN " + DatabaseHelper.PARTICIPANTS_TABLE + " ON ("
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID
+ '=' + DatabaseHelper.PARTICIPANTS_TABLE + '.' + DatabaseHelper.ParticipantColumns._ID
+ ") ";
// View that makes latest message read flag available with rest of conversation data.
private static final String CONVERSATION_LIST_VIEW_SQL = "CREATE VIEW " +
CONVERSATION_LIST_VIEW + " AS SELECT "
+ CONVERSATION_LIST_VIEW_PROJECTION + ", "
// Snippet not part of the base projection shared with search view
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.SNIPPET_TEXT
+ " as " + ConversationListViewColumns.SNIPPET_TEXT + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.SUBJECT_TEXT
+ " as " + ConversationListViewColumns.SUBJECT_TEXT + " "
+ " FROM " + DatabaseHelper.CONVERSATIONS_TABLE
+ " LEFT JOIN " + DatabaseHelper.MESSAGES_TABLE + " ON ("
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.LATEST_MESSAGE_ID
+ '=' + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID + ") "
+ JOIN_PARTICIPANTS
+ "ORDER BY " + DatabaseHelper.CONVERSATIONS_TABLE + '.'
+ ConversationColumns.SORT_TIMESTAMP + " DESC";
public static class ConversationListViewColumns implements BaseColumns {
public static final String _ID = ConversationColumns._ID;
static final String NAME = ConversationColumns.NAME;
static final String ARCHIVE_STATUS = ConversationColumns.ARCHIVE_STATUS;
static final String READ = MessageColumns.READ;
static final String SORT_TIMESTAMP = ConversationColumns.SORT_TIMESTAMP;
static final String PREVIEW_URI = ConversationColumns.PREVIEW_URI;
static final String PREVIEW_CONTENT_TYPE = ConversationColumns.PREVIEW_CONTENT_TYPE;
static final String SNIPPET_TEXT = ConversationColumns.SNIPPET_TEXT;
static final String SUBJECT_TEXT = ConversationColumns.SUBJECT_TEXT;
static final String ICON = ConversationColumns.ICON;
static final String SHOW_DRAFT = ConversationColumns.SHOW_DRAFT;
static final String DRAFT_SUBJECT_TEXT = ConversationColumns.DRAFT_SUBJECT_TEXT;
static final String DRAFT_PREVIEW_URI = ConversationColumns.DRAFT_PREVIEW_URI;
static final String DRAFT_PREVIEW_CONTENT_TYPE =
ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE;
static final String DRAFT_SNIPPET_TEXT = ConversationColumns.DRAFT_SNIPPET_TEXT;
static final String PARTICIPANT_CONTACT_ID = ConversationColumns.PARTICIPANT_CONTACT_ID;
static final String PARTICIPANT_LOOKUP_KEY = ConversationColumns.PARTICIPANT_LOOKUP_KEY;
static final String OTHER_PARTICIPANT_NORMALIZED_DESTINATION =
ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION;
static final String CURRENT_SELF_ID = ConversationColumns.CURRENT_SELF_ID;
static final String PARTICIPANT_COUNT = ConversationColumns.PARTICIPANT_COUNT;
static final String NOTIFICATION_ENABLED = ConversationColumns.NOTIFICATION_ENABLED;
static final String NOTIFICATION_SOUND_URI = ConversationColumns.NOTIFICATION_SOUND_URI;
static final String NOTIFICATION_VIBRATION = ConversationColumns.NOTIFICATION_VIBRATION;
static final String INCLUDE_EMAIL_ADDRESS =
ConversationColumns.INCLUDE_EMAIL_ADDRESS;
static final String MESSAGE_STATUS = MessageColumns.STATUS;
static final String MESSAGE_RAW_TELEPHONY_STATUS = MessageColumns.RAW_TELEPHONY_STATUS;
static final String MESSAGE_ID = "message_id";
static final String SNIPPET_SENDER_FIRST_NAME = "snippet_sender_first_name";
static final String SNIPPET_SENDER_DISPLAY_DESTINATION =
"snippet_sender_display_destination";
}
public static final String[] PROJECTION = {
ConversationListViewColumns._ID,
ConversationListViewColumns.NAME,
ConversationListViewColumns.ICON,
ConversationListViewColumns.SNIPPET_TEXT,
ConversationListViewColumns.SORT_TIMESTAMP,
ConversationListViewColumns.READ,
ConversationListViewColumns.PREVIEW_URI,
ConversationListViewColumns.PREVIEW_CONTENT_TYPE,
ConversationListViewColumns.PARTICIPANT_CONTACT_ID,
ConversationListViewColumns.PARTICIPANT_LOOKUP_KEY,
ConversationListViewColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION,
ConversationListViewColumns.PARTICIPANT_COUNT,
ConversationListViewColumns.CURRENT_SELF_ID,
ConversationListViewColumns.NOTIFICATION_ENABLED,
ConversationListViewColumns.NOTIFICATION_SOUND_URI,
ConversationListViewColumns.NOTIFICATION_VIBRATION,
ConversationListViewColumns.INCLUDE_EMAIL_ADDRESS,
ConversationListViewColumns.MESSAGE_STATUS,
ConversationListViewColumns.SHOW_DRAFT,
ConversationListViewColumns.DRAFT_PREVIEW_URI,
ConversationListViewColumns.DRAFT_PREVIEW_CONTENT_TYPE,
ConversationListViewColumns.DRAFT_SNIPPET_TEXT,
ConversationListViewColumns.ARCHIVE_STATUS,
ConversationListViewColumns.MESSAGE_ID,
ConversationListViewColumns.SUBJECT_TEXT,
ConversationListViewColumns.DRAFT_SUBJECT_TEXT,
ConversationListViewColumns.MESSAGE_RAW_TELEPHONY_STATUS,
ConversationListViewColumns.SNIPPET_SENDER_FIRST_NAME,
ConversationListViewColumns.SNIPPET_SENDER_DISPLAY_DESTINATION,
};
private static final int INDEX_ID = 0;
private static final int INDEX_CONVERSATION_NAME = 1;
private static final int INDEX_CONVERSATION_ICON = 2;
private static final int INDEX_SNIPPET_TEXT = 3;
private static final int INDEX_SORT_TIMESTAMP = 4;
private static final int INDEX_READ = 5;
private static final int INDEX_PREVIEW_URI = 6;
private static final int INDEX_PREVIEW_CONTENT_TYPE = 7;
private static final int INDEX_PARTICIPANT_CONTACT_ID = 8;
private static final int INDEX_PARTICIPANT_LOOKUP_KEY = 9;
private static final int INDEX_OTHER_PARTICIPANT_NORMALIZED_DESTINATION = 10;
private static final int INDEX_PARTICIPANT_COUNT = 11;
private static final int INDEX_SELF_ID = 12;
private static final int INDEX_NOTIFICATION_ENABLED = 13;
private static final int INDEX_NOTIFICATION_SOUND_URI = 14;
private static final int INDEX_NOTIFICATION_VIBRATION = 15;
private static final int INDEX_INCLUDE_EMAIL_ADDRESS = 16;
private static final int INDEX_MESSAGE_STATUS = 17;
private static final int INDEX_SHOW_DRAFT = 18;
private static final int INDEX_DRAFT_PREVIEW_URI = 19;
private static final int INDEX_DRAFT_PREVIEW_CONTENT_TYPE = 20;
private static final int INDEX_DRAFT_SNIPPET_TEXT = 21;
private static final int INDEX_ARCHIVE_STATUS = 22;
private static final int INDEX_MESSAGE_ID = 23;
private static final int INDEX_SUBJECT_TEXT = 24;
private static final int INDEX_DRAFT_SUBJECT_TEXT = 25;
private static final int INDEX_MESSAGE_RAW_TELEPHONY_STATUS = 26;
private static final int INDEX_SNIPPET_SENDER_FIRST_NAME = 27;
private static final int INDEX_SNIPPET_SENDER_DISPLAY_DESTINATION = 28;
private static final String DIVIDER_TEXT = ", ";
/**
* Get a conversation from the local DB based on the conversation id.
*
* @param dbWrapper The database
* @param conversationId The conversation Id to read
* @return The existing conversation or null
*/
public static ConversationListItemData getExistingConversation(final DatabaseWrapper dbWrapper,
final String conversationId) {
ConversationListItemData conversation = null;
// Look for an existing conversation in the db with this conversation id
Cursor cursor = null;
try {
// TODO: Should we be able to read a row from just the conversation table?
cursor = dbWrapper.query(getConversationListView(),
PROJECTION,
ConversationColumns._ID + "=?",
new String[] { conversationId },
null, null, null);
Assert.inRange(cursor.getCount(), 0, 1);
if (cursor.moveToFirst()) {
conversation = new ConversationListItemData();
conversation.bind(cursor);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return conversation;
}
public static String generateConversationName(final List<ParticipantData>
participants) {
if (participants.size() == 1) {
// Prefer full name over first name for 1:1 conversation
return participants.get(0).getDisplayName(true);
}
final ArrayList<String> participantNames = new ArrayList<String>();
for (final ParticipantData participant : participants) {
// Prefer first name over full name for group conversation
participantNames.add(participant.getDisplayName(false));
}
final Joiner joiner = Joiner.on(DIVIDER_TEXT).skipNulls();
return joiner.join(participantNames);
}
}
@@ -1,37 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.text.TextUtils;
/**
* Holds data for conversation message bubble which keeps track of whether it's been bound to
* a new message.
*/
public class ConversationMessageBubbleData {
private String mMessageId;
/**
* Binds to ConversationMessageData instance.
* @return true if we are binding to a different message, false if we are binding to the
* same message (e.g. in order to update the status text)
*/
public boolean bind(final ConversationMessageData data) {
final boolean changed = !TextUtils.equals(mMessageId, data.getMessageId());
mMessageId = data.getMessageId();
return changed;
}
}
@@ -1,917 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
import android.text.TextUtils;
import android.text.format.DateUtils;
import com.android.messaging.datamodel.DatabaseHelper;
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
import com.android.messaging.util.Assert;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.Dates;
import com.android.messaging.util.LogUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* Class representing a message within a conversation sequence. The message parts
* are available via the getParts() method.
*
* TODO: See if we can delegate to MessageData for the logic that this class duplicates
* (e.g. getIsMms).
*/
public class ConversationMessageData {
private static final String TAG = LogUtil.BUGLE_TAG;
private String mMessageId;
private String mConversationId;
private String mParticipantId;
private int mPartsCount;
private List<MessagePartData> mParts;
private long mSentTimestamp;
private long mReceivedTimestamp;
private boolean mSeen;
private boolean mRead;
private int mProtocol;
private int mStatus;
private String mSmsMessageUri;
private int mSmsPriority;
private int mSmsMessageSize;
private String mMmsSubject;
private long mMmsExpiry;
private int mRawTelephonyStatus;
private String mSenderFullName;
private String mSenderFirstName;
private String mSenderDisplayDestination;
private String mSenderNormalizedDestination;
private String mSenderProfilePhotoUri;
private long mSenderContactId;
private String mSenderContactLookupKey;
private String mSelfParticipantId;
/** Are we similar enough to the previous/next messages that we can cluster them? */
private boolean mCanClusterWithPreviousMessage;
private boolean mCanClusterWithNextMessage;
public ConversationMessageData() {
}
public void bind(final Cursor cursor) {
mMessageId = cursor.getString(INDEX_MESSAGE_ID);
mConversationId = cursor.getString(INDEX_CONVERSATION_ID);
mParticipantId = cursor.getString(INDEX_PARTICIPANT_ID);
mPartsCount = cursor.getInt(INDEX_PARTS_COUNT);
mParts = makeParts(
cursor.getString(INDEX_PARTS_IDS),
cursor.getString(INDEX_PARTS_CONTENT_TYPES),
cursor.getString(INDEX_PARTS_CONTENT_URIS),
cursor.getString(INDEX_PARTS_WIDTHS),
cursor.getString(INDEX_PARTS_HEIGHTS),
cursor.getString(INDEX_PARTS_TEXTS),
mPartsCount,
mMessageId);
mSentTimestamp = cursor.getLong(INDEX_SENT_TIMESTAMP);
mReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP);
mSeen = (cursor.getInt(INDEX_SEEN) != 0);
mRead = (cursor.getInt(INDEX_READ) != 0);
mProtocol = cursor.getInt(INDEX_PROTOCOL);
mStatus = cursor.getInt(INDEX_STATUS);
mSmsMessageUri = cursor.getString(INDEX_SMS_MESSAGE_URI);
mSmsPriority = cursor.getInt(INDEX_SMS_PRIORITY);
mSmsMessageSize = cursor.getInt(INDEX_SMS_MESSAGE_SIZE);
mMmsSubject = cursor.getString(INDEX_MMS_SUBJECT);
mMmsExpiry = cursor.getLong(INDEX_MMS_EXPIRY);
mRawTelephonyStatus = cursor.getInt(INDEX_RAW_TELEPHONY_STATUS);
mSenderFullName = cursor.getString(INDEX_SENDER_FULL_NAME);
mSenderFirstName = cursor.getString(INDEX_SENDER_FIRST_NAME);
mSenderDisplayDestination = cursor.getString(INDEX_SENDER_DISPLAY_DESTINATION);
mSenderNormalizedDestination = cursor.getString(INDEX_SENDER_NORMALIZED_DESTINATION);
mSenderProfilePhotoUri = cursor.getString(INDEX_SENDER_PROFILE_PHOTO_URI);
mSenderContactId = cursor.getLong(INDEX_SENDER_CONTACT_ID);
mSenderContactLookupKey = cursor.getString(INDEX_SENDER_CONTACT_LOOKUP_KEY);
mSelfParticipantId = cursor.getString(INDEX_SELF_PARTICIPIANT_ID);
if (!cursor.isFirst() && cursor.moveToPrevious()) {
mCanClusterWithPreviousMessage = canClusterWithMessage(cursor);
cursor.moveToNext();
} else {
mCanClusterWithPreviousMessage = false;
}
if (!cursor.isLast() && cursor.moveToNext()) {
mCanClusterWithNextMessage = canClusterWithMessage(cursor);
cursor.moveToPrevious();
} else {
mCanClusterWithNextMessage = false;
}
}
private boolean canClusterWithMessage(final Cursor cursor) {
final String otherParticipantId = cursor.getString(INDEX_PARTICIPANT_ID);
if (!TextUtils.equals(getParticipantId(), otherParticipantId)) {
return false;
}
final int otherStatus = cursor.getInt(INDEX_STATUS);
final boolean otherIsIncoming = (otherStatus >= MessageData.BUGLE_STATUS_FIRST_INCOMING);
if (getIsIncoming() != otherIsIncoming) {
return false;
}
final long otherReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP);
final long timestampDeltaMillis = Math.abs(mReceivedTimestamp - otherReceivedTimestamp);
if (timestampDeltaMillis > DateUtils.MINUTE_IN_MILLIS) {
return false;
}
final String otherSelfId = cursor.getString(INDEX_SELF_PARTICIPIANT_ID);
if (!TextUtils.equals(getSelfParticipantId(), otherSelfId)) {
return false;
}
return true;
}
private static final Character QUOTE_CHAR = '\'';
private static final char DIVIDER = '|';
// statics to avoid unnecessary object allocation
private static final StringBuilder sUnquoteStringBuilder = new StringBuilder();
private static final ArrayList<String> sUnquoteResults = new ArrayList<String>();
// this lock is used to guard access to the above statics
private static final Object sUnquoteLock = new Object();
private static void addResult(final ArrayList<String> results, final StringBuilder value) {
if (value.length() > 0) {
results.add(value.toString());
} else {
results.add(EMPTY_STRING);
}
}
@VisibleForTesting
static String[] splitUnquotedString(final String inputString) {
if (TextUtils.isEmpty(inputString)) {
return new String[0];
}
return inputString.split("\\" + DIVIDER);
}
/**
* Takes a group-concated and quoted string and decomposes it into its constituent
* parts. A quoted string starts and ends with a single quote. Actual single quotes
* within the string are escaped using a second single quote. So, for example, an
* input string with 3 constituent parts might look like this:
*
* 'now is the time'|'I can''t do it'|'foo'
*
* This would be returned as an array of 3 strings as follows:
* now is the time
* I can't do it
* foo
*
* This is achieved by walking through the inputString, character by character,
* ignoring the outer quotes and the divider and replacing any pair of consecutive
* single quotes with a single single quote.
*
* @param inputString
* @return array of constituent strings
*/
@VisibleForTesting
static String[] splitQuotedString(final String inputString) {
if (TextUtils.isEmpty(inputString)) {
return new String[0];
}
// this method can be called from multiple threads but it uses a static
// string builder
synchronized (sUnquoteLock) {
final int length = inputString.length();
final ArrayList<String> results = sUnquoteResults;
results.clear();
int characterPos = -1;
while (++characterPos < length) {
final char mustBeQuote = inputString.charAt(characterPos);
Assert.isTrue(QUOTE_CHAR == mustBeQuote);
while (++characterPos < length) {
final char currentChar = inputString.charAt(characterPos);
if (currentChar == QUOTE_CHAR) {
final char peekAhead = characterPos < length - 1
? inputString.charAt(characterPos + 1) : 0;
if (peekAhead == QUOTE_CHAR) {
characterPos += 1; // skip the second quote
} else {
addResult(results, sUnquoteStringBuilder);
sUnquoteStringBuilder.setLength(0);
Assert.isTrue((peekAhead == DIVIDER) || (peekAhead == (char) 0));
characterPos += 1; // skip the divider
break;
}
}
sUnquoteStringBuilder.append(currentChar);
}
}
return results.toArray(new String[results.size()]);
}
}
static MessagePartData makePartData(
final String partId,
final String contentType,
final String contentUriString,
final String contentWidth,
final String contentHeight,
final String text,
final String messageId) {
if (ContentType.isTextType(contentType)) {
final MessagePartData textPart = MessagePartData.createTextMessagePart(text);
textPart.updatePartId(partId);
textPart.updateMessageId(messageId);
return textPart;
} else {
final Uri contentUri = Uri.parse(contentUriString);
final int width = Integer.parseInt(contentWidth);
final int height = Integer.parseInt(contentHeight);
final MessagePartData attachmentPart = MessagePartData.createMediaMessagePart(
contentType, contentUri, width, height);
attachmentPart.updatePartId(partId);
attachmentPart.updateMessageId(messageId);
return attachmentPart;
}
}
@VisibleForTesting
static List<MessagePartData> makeParts(
final String rawIds,
final String rawContentTypes,
final String rawContentUris,
final String rawWidths,
final String rawHeights,
final String rawTexts,
final int partsCount,
final String messageId) {
final List<MessagePartData> parts = new LinkedList<MessagePartData>();
if (partsCount == 1) {
parts.add(makePartData(
rawIds,
rawContentTypes,
rawContentUris,
rawWidths,
rawHeights,
rawTexts,
messageId));
} else {
unpackMessageParts(
parts,
splitUnquotedString(rawIds),
splitQuotedString(rawContentTypes),
splitQuotedString(rawContentUris),
splitUnquotedString(rawWidths),
splitUnquotedString(rawHeights),
splitQuotedString(rawTexts),
partsCount,
messageId);
}
return parts;
}
@VisibleForTesting
static void unpackMessageParts(
final List<MessagePartData> parts,
final String[] ids,
final String[] contentTypes,
final String[] contentUris,
final String[] contentWidths,
final String[] contentHeights,
final String[] texts,
final int partsCount,
final String messageId) {
Assert.equals(partsCount, ids.length);
Assert.equals(partsCount, contentTypes.length);
Assert.equals(partsCount, contentUris.length);
Assert.equals(partsCount, contentWidths.length);
Assert.equals(partsCount, contentHeights.length);
Assert.equals(partsCount, texts.length);
for (int i = 0; i < partsCount; i++) {
parts.add(makePartData(
ids[i],
contentTypes[i],
contentUris[i],
contentWidths[i],
contentHeights[i],
texts[i],
messageId));
}
if (parts.size() != partsCount) {
LogUtil.wtf(TAG, "Only unpacked " + parts.size() + " parts from message (id="
+ messageId + "), expected " + partsCount + " parts");
}
}
public final String getMessageId() {
return mMessageId;
}
public final String getConversationId() {
return mConversationId;
}
public final String getParticipantId() {
return mParticipantId;
}
public List<MessagePartData> getParts() {
return mParts;
}
public boolean hasText() {
for (final MessagePartData part : mParts) {
if (part.isText()) {
return true;
}
}
return false;
}
/**
* Get a concatenation of all text parts
*
* @return the text that is a concatenation of all text parts
*/
public String getText() {
// This is optimized for single text part case, which is the majority
// For single text part, we just return the part without creating the StringBuilder
String firstTextPart = null;
boolean foundText = false;
// For multiple text parts, we need the StringBuilder and the separator for concatenation
StringBuilder sb = null;
String separator = null;
for (final MessagePartData part : mParts) {
if (part.isText()) {
if (!foundText) {
// First text part
firstTextPart = part.getText();
foundText = true;
} else {
// Second and beyond
if (sb == null) {
// Need the StringBuilder and the separator starting from 2nd text part
sb = new StringBuilder();
if (!TextUtils.isEmpty(firstTextPart)) {
sb.append(firstTextPart);
}
separator = BugleGservices.get().getString(
BugleGservicesKeys.MMS_TEXT_CONCAT_SEPARATOR,
BugleGservicesKeys.MMS_TEXT_CONCAT_SEPARATOR_DEFAULT);
}
final String partText = part.getText();
if (!TextUtils.isEmpty(partText)) {
if (!TextUtils.isEmpty(separator) && sb.length() > 0) {
sb.append(separator);
}
sb.append(partText);
}
}
}
}
if (sb == null) {
// Only one text part
return firstTextPart;
} else {
// More than one
return sb.toString();
}
}
public boolean hasAttachments() {
for (final MessagePartData part : mParts) {
if (part.isAttachment()) {
return true;
}
}
return false;
}
public List<MessagePartData> getAttachments() {
return getAttachments(null);
}
public List<MessagePartData> getAttachments(final Predicate<MessagePartData> filter) {
if (mParts.isEmpty()) {
return Collections.emptyList();
}
final List<MessagePartData> attachmentParts = new LinkedList<>();
for (final MessagePartData part : mParts) {
if (part.isAttachment()) {
if (filter == null || filter.apply(part)) {
attachmentParts.add(part);
}
}
}
return attachmentParts;
}
public final long getSentTimeStamp() {
return mSentTimestamp;
}
public final long getReceivedTimeStamp() {
return mReceivedTimestamp;
}
public final String getFormattedReceivedTimeStamp() {
return Dates.getMessageTimeString(mReceivedTimestamp).toString();
}
public final boolean getIsSeen() {
return mSeen;
}
public final boolean getIsRead() {
return mRead;
}
public final boolean getIsMms() {
return (mProtocol == MessageData.PROTOCOL_MMS ||
mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION);
}
public final boolean getIsMmsNotification() {
return (mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION);
}
public final boolean getIsSms() {
return mProtocol == (MessageData.PROTOCOL_SMS);
}
final int getProtocol() {
return mProtocol;
}
public final int getStatus() {
return mStatus;
}
public final String getSmsMessageUri() {
return mSmsMessageUri;
}
public final int getSmsPriority() {
return mSmsPriority;
}
public final int getSmsMessageSize() {
return mSmsMessageSize;
}
public final String getMmsSubject() {
return mMmsSubject;
}
public final long getMmsExpiry() {
return mMmsExpiry;
}
public final int getRawTelephonyStatus() {
return mRawTelephonyStatus;
}
public final String getSelfParticipantId() {
return mSelfParticipantId;
}
public boolean getIsIncoming() {
return (mStatus >= MessageData.BUGLE_STATUS_FIRST_INCOMING);
}
public boolean hasIncomingErrorStatus() {
return (mStatus == MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE ||
mStatus == MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED);
}
public boolean getIsSendComplete() {
return mStatus == MessageData.BUGLE_STATUS_OUTGOING_COMPLETE;
}
public String getSenderFullName() {
return mSenderFullName;
}
public String getSenderFirstName() {
return mSenderFirstName;
}
public String getSenderDisplayDestination() {
return mSenderDisplayDestination;
}
public String getSenderNormalizedDestination() {
return mSenderNormalizedDestination;
}
public Uri getSenderProfilePhotoUri() {
return mSenderProfilePhotoUri == null ? null : Uri.parse(mSenderProfilePhotoUri);
}
public long getSenderContactId() {
return mSenderContactId;
}
public String getSenderDisplayName() {
if (!TextUtils.isEmpty(mSenderFullName)) {
return mSenderFullName;
}
if (!TextUtils.isEmpty(mSenderFirstName)) {
return mSenderFirstName;
}
return mSenderDisplayDestination;
}
public String getSenderContactLookupKey() {
return mSenderContactLookupKey;
}
public boolean getShowDownloadMessage() {
return MessageData.getShowDownloadMessage(mStatus);
}
public boolean getShowResendMessage() {
return MessageData.getShowResendMessage(mStatus);
}
public boolean getCanForwardMessage() {
// Even for outgoing messages, we only allow forwarding if the message has finished sending
// as media often has issues when send isn't complete
return (mStatus == MessageData.BUGLE_STATUS_OUTGOING_COMPLETE ||
mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE);
}
public boolean getCanCopyMessageToClipboard() {
return (hasText() &&
(!getIsIncoming() || mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE));
}
public boolean getOneClickResendMessage() {
return MessageData.getOneClickResendMessage(mStatus, mRawTelephonyStatus);
}
/**
* Get sender's lookup uri.
* This method doesn't support corp contacts.
*
* @return Lookup uri of sender's contact
*/
public Uri getSenderContactLookupUri() {
if (mSenderContactId > ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED
&& !TextUtils.isEmpty(mSenderContactLookupKey)) {
return ContactsContract.Contacts.getLookupUri(mSenderContactId,
mSenderContactLookupKey);
}
return null;
}
public boolean getCanClusterWithPreviousMessage() {
return mCanClusterWithPreviousMessage;
}
public boolean getCanClusterWithNextMessage() {
return mCanClusterWithNextMessage;
}
@Override
public String toString() {
return MessageData.toString(mMessageId, mParts);
}
// Data definitions
public static final String getConversationMessagesQuerySql() {
return CONVERSATION_MESSAGES_QUERY_SQL
+ " AND "
// Inject the conversation id
+ DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)"
+ CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY;
}
static final String getConversationMessageIdsQuerySql() {
return CONVERSATION_MESSAGES_IDS_QUERY_SQL
+ " AND "
// Inject the conversation id
+ DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)"
+ CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY;
}
public static final String getNotificationQuerySql() {
return CONVERSATION_MESSAGES_QUERY_SQL
+ " AND "
+ "(" + DatabaseHelper.MessageColumns.STATUS + " in ("
+ MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", "
+ MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")"
+ " AND "
+ DatabaseHelper.MessageColumns.SEEN + " = 0)"
+ ")"
+ NOTIFICATION_QUERY_SQL_GROUP_BY;
}
public static final String getWearableQuerySql() {
return CONVERSATION_MESSAGES_QUERY_SQL
+ " AND "
+ DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?"
+ " AND "
+ DatabaseHelper.MessageColumns.STATUS + " IN ("
+ MessageData.BUGLE_STATUS_OUTGOING_DELIVERED + ", "
+ MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + ", "
+ MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND + ", "
+ MessageData.BUGLE_STATUS_OUTGOING_SENDING + ", "
+ MessageData.BUGLE_STATUS_OUTGOING_RESENDING + ", "
+ MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY + ", "
+ MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", "
+ MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")"
+ ")"
+ NOTIFICATION_QUERY_SQL_GROUP_BY;
}
/*
* Generate a sqlite snippet to call the quote function on the columnName argument.
* The columnName doesn't strictly have to be a column name (e.g. it could be an
* expression).
*/
private static String quote(final String columnName) {
return "quote(" + columnName + ")";
}
private static String makeGroupConcatString(final String column) {
return "group_concat(" + column + ", '" + DIVIDER + "')";
}
private static String makeIfNullString(final String column) {
return "ifnull(" + column + "," + "''" + ")";
}
private static String makePartsTableColumnString(final String column) {
return DatabaseHelper.PARTS_TABLE + '.' + column;
}
private static String makeCaseWhenString(final String column,
final boolean quote,
final String asColumn) {
final String fullColumn = makeIfNullString(makePartsTableColumnString(column));
final String groupConcatTerm = quote
? makeGroupConcatString(quote(fullColumn))
: makeGroupConcatString(fullColumn);
return "CASE WHEN (" + CONVERSATION_MESSAGE_VIEW_PARTS_COUNT + ">1) THEN " + groupConcatTerm
+ " ELSE " + makePartsTableColumnString(column) + " END AS " + asColumn;
}
private static final String CONVERSATION_MESSAGE_VIEW_PARTS_COUNT =
"count(" + DatabaseHelper.PARTS_TABLE + '.' + PartColumns._ID + ")";
private static final String EMPTY_STRING = "";
private static final String CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL =
DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID
+ " as " + ConversationMessageViewColumns._ID + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.CONVERSATION_ID
+ " as " + ConversationMessageViewColumns.CONVERSATION_ID + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID
+ " as " + ConversationMessageViewColumns.PARTICIPANT_ID + ", "
+ makeCaseWhenString(PartColumns._ID, false,
ConversationMessageViewColumns.PARTS_IDS) + ", "
+ makeCaseWhenString(PartColumns.CONTENT_TYPE, true,
ConversationMessageViewColumns.PARTS_CONTENT_TYPES) + ", "
+ makeCaseWhenString(PartColumns.CONTENT_URI, true,
ConversationMessageViewColumns.PARTS_CONTENT_URIS) + ", "
+ makeCaseWhenString(PartColumns.WIDTH, false,
ConversationMessageViewColumns.PARTS_WIDTHS) + ", "
+ makeCaseWhenString(PartColumns.HEIGHT, false,
ConversationMessageViewColumns.PARTS_HEIGHTS) + ", "
+ makeCaseWhenString(PartColumns.TEXT, true,
ConversationMessageViewColumns.PARTS_TEXTS) + ", "
+ CONVERSATION_MESSAGE_VIEW_PARTS_COUNT
+ " as " + ConversationMessageViewColumns.PARTS_COUNT + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENT_TIMESTAMP
+ " as " + ConversationMessageViewColumns.SENT_TIMESTAMP + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP
+ " as " + ConversationMessageViewColumns.RECEIVED_TIMESTAMP + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SEEN
+ " as " + ConversationMessageViewColumns.SEEN + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.READ
+ " as " + ConversationMessageViewColumns.READ + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.PROTOCOL
+ " as " + ConversationMessageViewColumns.PROTOCOL + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.STATUS
+ " as " + ConversationMessageViewColumns.STATUS + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_URI
+ " as " + ConversationMessageViewColumns.SMS_MESSAGE_URI + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_PRIORITY
+ " as " + ConversationMessageViewColumns.SMS_PRIORITY + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_SIZE
+ " as " + ConversationMessageViewColumns.SMS_MESSAGE_SIZE + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_SUBJECT
+ " as " + ConversationMessageViewColumns.MMS_SUBJECT + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_EXPIRY
+ " as " + ConversationMessageViewColumns.MMS_EXPIRY + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RAW_TELEPHONY_STATUS
+ " as " + ConversationMessageViewColumns.RAW_TELEPHONY_STATUS + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SELF_PARTICIPANT_ID
+ " as " + ConversationMessageViewColumns.SELF_PARTICIPANT_ID + ", "
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FULL_NAME
+ " as " + ConversationMessageViewColumns.SENDER_FULL_NAME + ", "
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FIRST_NAME
+ " as " + ConversationMessageViewColumns.SENDER_FIRST_NAME + ", "
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.DISPLAY_DESTINATION
+ " as " + ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION + ", "
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.NORMALIZED_DESTINATION
+ " as " + ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION + ", "
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.PROFILE_PHOTO_URI
+ " as " + ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI + ", "
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.CONTACT_ID
+ " as " + ConversationMessageViewColumns.SENDER_CONTACT_ID + ", "
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.LOOKUP_KEY
+ " as " + ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY + " ";
private static final String CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL =
" FROM " + DatabaseHelper.MESSAGES_TABLE
+ " LEFT JOIN " + DatabaseHelper.PARTS_TABLE
+ " ON (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns._ID
+ "=" + DatabaseHelper.PARTS_TABLE + "." + PartColumns.MESSAGE_ID + ") "
+ " LEFT JOIN " + DatabaseHelper.PARTICIPANTS_TABLE
+ " ON (" + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID
+ '=' + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns._ID + ")"
// Exclude draft messages from main view
+ " WHERE (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.STATUS
+ " <> " + MessageData.BUGLE_STATUS_OUTGOING_DRAFT;
// This query is mostly static, except for the injection of conversation id. This is for
// performance reasons, to ensure that the query uses indices and does not trigger full scans
// of the messages table. See b/17160946 for more details.
private static final String CONVERSATION_MESSAGES_QUERY_SQL = "SELECT "
+ CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL
+ CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL;
private static final String CONVERSATION_MESSAGE_IDS_PROJECTION_SQL =
DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID
+ " as " + ConversationMessageViewColumns._ID + " ";
private static final String CONVERSATION_MESSAGES_IDS_QUERY_SQL = "SELECT "
+ CONVERSATION_MESSAGE_IDS_PROJECTION_SQL
+ CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL;
// Note that we sort DESC and ConversationData reverses the cursor. This is a performance
// issue (improvement) for large cursors.
private static final String CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY =
" GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID
+ " ORDER BY "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC";
private static final String NOTIFICATION_QUERY_SQL_GROUP_BY =
" GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID
+ " ORDER BY "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC";
interface ConversationMessageViewColumns extends BaseColumns {
static final String _ID = MessageColumns._ID;
static final String CONVERSATION_ID = MessageColumns.CONVERSATION_ID;
static final String PARTICIPANT_ID = MessageColumns.SENDER_PARTICIPANT_ID;
static final String PARTS_COUNT = "parts_count";
static final String SENT_TIMESTAMP = MessageColumns.SENT_TIMESTAMP;
static final String RECEIVED_TIMESTAMP = MessageColumns.RECEIVED_TIMESTAMP;
static final String SEEN = MessageColumns.SEEN;
static final String READ = MessageColumns.READ;
static final String PROTOCOL = MessageColumns.PROTOCOL;
static final String STATUS = MessageColumns.STATUS;
static final String SMS_MESSAGE_URI = MessageColumns.SMS_MESSAGE_URI;
static final String SMS_PRIORITY = MessageColumns.SMS_PRIORITY;
static final String SMS_MESSAGE_SIZE = MessageColumns.SMS_MESSAGE_SIZE;
static final String MMS_SUBJECT = MessageColumns.MMS_SUBJECT;
static final String MMS_EXPIRY = MessageColumns.MMS_EXPIRY;
static final String RAW_TELEPHONY_STATUS = MessageColumns.RAW_TELEPHONY_STATUS;
static final String SELF_PARTICIPANT_ID = MessageColumns.SELF_PARTICIPANT_ID;
static final String SENDER_FULL_NAME = ParticipantColumns.FULL_NAME;
static final String SENDER_FIRST_NAME = ParticipantColumns.FIRST_NAME;
static final String SENDER_DISPLAY_DESTINATION = ParticipantColumns.DISPLAY_DESTINATION;
static final String SENDER_NORMALIZED_DESTINATION =
ParticipantColumns.NORMALIZED_DESTINATION;
static final String SENDER_PROFILE_PHOTO_URI = ParticipantColumns.PROFILE_PHOTO_URI;
static final String SENDER_CONTACT_ID = ParticipantColumns.CONTACT_ID;
static final String SENDER_CONTACT_LOOKUP_KEY = ParticipantColumns.LOOKUP_KEY;
static final String PARTS_IDS = "parts_ids";
static final String PARTS_CONTENT_TYPES = "parts_content_types";
static final String PARTS_CONTENT_URIS = "parts_content_uris";
static final String PARTS_WIDTHS = "parts_widths";
static final String PARTS_HEIGHTS = "parts_heights";
static final String PARTS_TEXTS = "parts_texts";
}
private static int sIndexIncrementer = 0;
private static final int INDEX_MESSAGE_ID = sIndexIncrementer++;
private static final int INDEX_CONVERSATION_ID = sIndexIncrementer++;
private static final int INDEX_PARTICIPANT_ID = sIndexIncrementer++;
private static final int INDEX_PARTS_IDS = sIndexIncrementer++;
private static final int INDEX_PARTS_CONTENT_TYPES = sIndexIncrementer++;
private static final int INDEX_PARTS_CONTENT_URIS = sIndexIncrementer++;
private static final int INDEX_PARTS_WIDTHS = sIndexIncrementer++;
private static final int INDEX_PARTS_HEIGHTS = sIndexIncrementer++;
private static final int INDEX_PARTS_TEXTS = sIndexIncrementer++;
private static final int INDEX_PARTS_COUNT = sIndexIncrementer++;
private static final int INDEX_SENT_TIMESTAMP = sIndexIncrementer++;
private static final int INDEX_RECEIVED_TIMESTAMP = sIndexIncrementer++;
private static final int INDEX_SEEN = sIndexIncrementer++;
private static final int INDEX_READ = sIndexIncrementer++;
private static final int INDEX_PROTOCOL = sIndexIncrementer++;
private static final int INDEX_STATUS = sIndexIncrementer++;
private static final int INDEX_SMS_MESSAGE_URI = sIndexIncrementer++;
private static final int INDEX_SMS_PRIORITY = sIndexIncrementer++;
private static final int INDEX_SMS_MESSAGE_SIZE = sIndexIncrementer++;
private static final int INDEX_MMS_SUBJECT = sIndexIncrementer++;
private static final int INDEX_MMS_EXPIRY = sIndexIncrementer++;
private static final int INDEX_RAW_TELEPHONY_STATUS = sIndexIncrementer++;
private static final int INDEX_SELF_PARTICIPIANT_ID = sIndexIncrementer++;
private static final int INDEX_SENDER_FULL_NAME = sIndexIncrementer++;
private static final int INDEX_SENDER_FIRST_NAME = sIndexIncrementer++;
private static final int INDEX_SENDER_DISPLAY_DESTINATION = sIndexIncrementer++;
private static final int INDEX_SENDER_NORMALIZED_DESTINATION = sIndexIncrementer++;
private static final int INDEX_SENDER_PROFILE_PHOTO_URI = sIndexIncrementer++;
private static final int INDEX_SENDER_CONTACT_ID = sIndexIncrementer++;
private static final int INDEX_SENDER_CONTACT_LOOKUP_KEY = sIndexIncrementer++;
private static String[] sProjection = {
ConversationMessageViewColumns._ID,
ConversationMessageViewColumns.CONVERSATION_ID,
ConversationMessageViewColumns.PARTICIPANT_ID,
ConversationMessageViewColumns.PARTS_IDS,
ConversationMessageViewColumns.PARTS_CONTENT_TYPES,
ConversationMessageViewColumns.PARTS_CONTENT_URIS,
ConversationMessageViewColumns.PARTS_WIDTHS,
ConversationMessageViewColumns.PARTS_HEIGHTS,
ConversationMessageViewColumns.PARTS_TEXTS,
ConversationMessageViewColumns.PARTS_COUNT,
ConversationMessageViewColumns.SENT_TIMESTAMP,
ConversationMessageViewColumns.RECEIVED_TIMESTAMP,
ConversationMessageViewColumns.SEEN,
ConversationMessageViewColumns.READ,
ConversationMessageViewColumns.PROTOCOL,
ConversationMessageViewColumns.STATUS,
ConversationMessageViewColumns.SMS_MESSAGE_URI,
ConversationMessageViewColumns.SMS_PRIORITY,
ConversationMessageViewColumns.SMS_MESSAGE_SIZE,
ConversationMessageViewColumns.MMS_SUBJECT,
ConversationMessageViewColumns.MMS_EXPIRY,
ConversationMessageViewColumns.RAW_TELEPHONY_STATUS,
ConversationMessageViewColumns.SELF_PARTICIPANT_ID,
ConversationMessageViewColumns.SENDER_FULL_NAME,
ConversationMessageViewColumns.SENDER_FIRST_NAME,
ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION,
ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION,
ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI,
ConversationMessageViewColumns.SENDER_CONTACT_ID,
ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY,
};
public static String[] getProjection() {
return sProjection;
}
}
@@ -1,125 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.database.Cursor;
import android.support.v4.util.SimpleArrayMap;
import com.google.common.annotations.VisibleForTesting;
import junit.framework.Assert;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* A class that contains the list of all participants potentially involved in a conversation.
* Includes both the participant records for each participant referenced in conversation
* participants table (i.e. "other" phone numbers) plus all participants representing self
* (i.e. one per sim recorded in the subscription manager db).
*/
public class ConversationParticipantsData implements Iterable<ParticipantData> {
// A map from a participant id to a participant
private final SimpleArrayMap<String, ParticipantData> mConversationParticipantsMap;
private int mParticipantCountExcludingSelf = 0;
public ConversationParticipantsData() {
mConversationParticipantsMap = new SimpleArrayMap<String, ParticipantData>();
}
public void bind(final Cursor cursor) {
mConversationParticipantsMap.clear();
mParticipantCountExcludingSelf = 0;
if (cursor != null) {
while (cursor.moveToNext()) {
final ParticipantData newParticipant = ParticipantData.getFromCursor(cursor);
if (!newParticipant.isSelf()) {
mParticipantCountExcludingSelf++;
}
mConversationParticipantsMap.put(newParticipant.getId(), newParticipant);
}
}
}
@VisibleForTesting
ParticipantData getParticipantById(final String participantId) {
return mConversationParticipantsMap.get(participantId);
}
ArrayList<ParticipantData> getParticipantListExcludingSelf() {
final ArrayList<ParticipantData> retList =
new ArrayList<ParticipantData>(mConversationParticipantsMap.size());
for (int i = 0; i < mConversationParticipantsMap.size(); i++) {
final ParticipantData participant = mConversationParticipantsMap.valueAt(i);
if (!participant.isSelf()) {
retList.add(participant);
}
}
return retList;
}
/**
* For a 1:1 conversation return the other (not self) participant
*/
public ParticipantData getOtherParticipant() {
if (mParticipantCountExcludingSelf == 1) {
for (int i = 0; i < mConversationParticipantsMap.size(); i++) {
final ParticipantData participant = mConversationParticipantsMap.valueAt(i);
if (!participant.isSelf()) {
return participant;
}
}
Assert.fail();
}
return null;
}
public int getNumberOfParticipantsExcludingSelf() {
return mParticipantCountExcludingSelf;
}
public boolean isLoaded() {
return !mConversationParticipantsMap.isEmpty();
}
@Override
public Iterator<ParticipantData> iterator() {
return new Iterator<ParticipantData>() {
private int mCurrentIndex = -1;
@Override
public boolean hasNext() {
return mCurrentIndex < mConversationParticipantsMap.size() - 1;
}
@Override
public ParticipantData next() {
mCurrentIndex++;
if (mCurrentIndex >= mConversationParticipantsMap.size()) {
throw new NoSuchElementException();
}
return mConversationParticipantsMap.valueAt(mCurrentIndex);
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}
@@ -1,855 +0,0 @@
/*
* 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.net.Uri;
import android.text.TextUtils;
import com.android.messaging.datamodel.MessageTextStats;
import com.android.messaging.datamodel.action.ReadDraftDataAction;
import com.android.messaging.datamodel.action.ReadDraftDataAction.ReadDraftDataActionListener;
import com.android.messaging.datamodel.action.ReadDraftDataAction.ReadDraftDataActionMonitor;
import com.android.messaging.datamodel.action.WriteDraftMessageAction;
import com.android.messaging.datamodel.binding.BindableData;
import com.android.messaging.datamodel.binding.Binding;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.sms.MmsConfig;
import com.android.messaging.sms.MmsSmsUtils;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.Assert;
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
import com.android.messaging.util.Assert.RunsOnMainThread;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.SafeAsyncTask;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
public class DraftMessageData extends BindableData implements ReadDraftDataActionListener {
/**
* Interface for DraftMessageData listeners
*/
public interface DraftMessageDataListener {
@RunsOnMainThread
void onDraftChanged(DraftMessageData data, int changeFlags);
@RunsOnMainThread
void onDraftAttachmentLimitReached(DraftMessageData data);
@RunsOnMainThread
void onDraftAttachmentLoadFailed();
}
/**
* Interface for providing subscription-related data to DraftMessageData
*/
public interface DraftMessageSubscriptionDataProvider {
int getConversationSelfSubId();
}
// Flags sent to onDraftChanged to help the receiver limit the amount of work done
public static int ATTACHMENTS_CHANGED = 0x0001;
public static int MESSAGE_TEXT_CHANGED = 0x0002;
public static int MESSAGE_SUBJECT_CHANGED = 0x0004;
// Whether the self participant data has been loaded
public static int SELF_CHANGED = 0x0008;
public static int ALL_CHANGED = 0x00FF;
// ALL_CHANGED intentionally doesn't include WIDGET_CHANGED. ConversationFragment needs to
// be notified if the draft it is looking at is changed externally (by a desktop widget) so it
// can reload the draft.
public static int WIDGET_CHANGED = 0x0100;
private final String mConversationId;
private ReadDraftDataActionMonitor mMonitor;
private final DraftMessageDataEventDispatcher mListeners;
private DraftMessageSubscriptionDataProvider mSubscriptionDataProvider;
private boolean mIncludeEmailAddress;
private boolean mIsGroupConversation;
private String mMessageText;
private String mMessageSubject;
private String mSelfId;
private MessageTextStats mMessageTextStats;
private boolean mSending;
/** Keeps track of completed attachments in the message draft. This data is persisted to db */
private final List<MessagePartData> mAttachments;
/** A read-only wrapper on mAttachments surfaced to the UI layer for rendering */
private final List<MessagePartData> mReadOnlyAttachments;
/** Keeps track of pending attachments that are being loaded. The pending attachments are
* transient, because they are not persisted to the database and are dropped once we go
* to the background (after the UI calls saveToStorage) */
private final List<PendingAttachmentData> mPendingAttachments;
/** A read-only wrapper on mPendingAttachments surfaced to the UI layer for rendering */
private final List<PendingAttachmentData> mReadOnlyPendingAttachments;
/** Is the current draft a cached copy of what's been saved to the database. If so, we
* may skip loading from database if we are still bound */
private boolean mIsDraftCachedCopy;
/** Whether we are currently asynchronously validating the draft before sending. */
private CheckDraftForSendTask mCheckDraftForSendTask;
public DraftMessageData(final String conversationId) {
mConversationId = conversationId;
mAttachments = new ArrayList<MessagePartData>();
mReadOnlyAttachments = Collections.unmodifiableList(mAttachments);
mPendingAttachments = new ArrayList<PendingAttachmentData>();
mReadOnlyPendingAttachments = Collections.unmodifiableList(mPendingAttachments);
mListeners = new DraftMessageDataEventDispatcher();
mMessageTextStats = new MessageTextStats();
}
public void addListener(final DraftMessageDataListener listener) {
mListeners.add(listener);
}
public void setSubscriptionDataProvider(final DraftMessageSubscriptionDataProvider provider) {
mSubscriptionDataProvider = provider;
}
public void updateFromMessageData(final MessageData message, final String bindingId) {
// New attachments have arrived - only update if the user hasn't already edited
Assert.notNull(bindingId);
// The draft is now synced with actual MessageData and no longer a cached copy.
mIsDraftCachedCopy = false;
// Do not use the loaded draft if the user began composing a message before the draft loaded
// During config changes (orientation), the text fields preserve their data, so allow them
// to be the same and still consider the draft unchanged by the user
if (isDraftEmpty() || (TextUtils.equals(mMessageText, message.getMessageText()) &&
TextUtils.equals(mMessageSubject, message.getMmsSubject()) &&
mAttachments.isEmpty())) {
// No need to clear as just checked it was empty or a subset
setMessageText(message.getMessageText(), false /* notify */);
setMessageSubject(message.getMmsSubject(), false /* notify */);
for (final MessagePartData part : message.getParts()) {
if (part.isAttachment() && getAttachmentCount() >= getAttachmentLimit()) {
dispatchAttachmentLimitReached();
break;
}
if (part instanceof PendingAttachmentData) {
// This is a pending attachment data from share intent (e.g. an shared image
// that we need to persist locally).
final PendingAttachmentData data = (PendingAttachmentData) part;
Assert.equals(PendingAttachmentData.STATE_PENDING, data.getCurrentState());
addOnePendingAttachmentNoNotify(data, bindingId);
} else if (part.isAttachment()) {
addOneAttachmentNoNotify(part);
}
}
dispatchChanged(ALL_CHANGED);
} else {
// The user has started a new message so we throw out the draft message data if there
// is one but we also loaded the self metadata and need to let our listeners know.
dispatchChanged(SELF_CHANGED);
}
}
/**
* Create a MessageData object containing a copy of all the parts in this DraftMessageData.
*
* @param clearLocalCopy whether we should clear out the in-memory copy of the parts. If we
* are simply pausing/resuming and not sending the message, then we can keep
* @return the MessageData for the draft, null if self id is not set
*/
public MessageData createMessageWithCurrentAttachments(final boolean clearLocalCopy) {
MessageData message = null;
if (getIsMms()) {
message = MessageData.createDraftMmsMessage(mConversationId, mSelfId,
mMessageText, mMessageSubject);
for (final MessagePartData attachment : mAttachments) {
message.addPart(attachment);
}
} else {
message = MessageData.createDraftSmsMessage(mConversationId, mSelfId,
mMessageText);
}
if (clearLocalCopy) {
// The message now owns all the attachments and the text...
clearLocalDraftCopy();
dispatchChanged(ALL_CHANGED);
} else {
// The draft message becomes a cached copy for UI.
mIsDraftCachedCopy = true;
}
return message;
}
private void clearLocalDraftCopy() {
mIsDraftCachedCopy = false;
mAttachments.clear();
setMessageText("");
setMessageSubject("");
}
public String getConversationId() {
return mConversationId;
}
public String getMessageText() {
return mMessageText;
}
public String getMessageSubject() {
return mMessageSubject;
}
public boolean getIsMms() {
final int selfSubId = getSelfSubId();
return MmsSmsUtils.getRequireMmsForEmailAddress(mIncludeEmailAddress, selfSubId) ||
(mIsGroupConversation && MmsUtils.groupMmsEnabled(selfSubId)) ||
mMessageTextStats.getMessageLengthRequiresMms() || !mAttachments.isEmpty() ||
!TextUtils.isEmpty(mMessageSubject);
}
public boolean getIsGroupMmsConversation() {
return getIsMms() && mIsGroupConversation;
}
public String getSelfId() {
return mSelfId;
}
public int getNumMessagesToBeSent() {
return mMessageTextStats.getNumMessagesToBeSent();
}
public int getCodePointsRemainingInCurrentMessage() {
return mMessageTextStats.getCodePointsRemainingInCurrentMessage();
}
public int getSelfSubId() {
return mSubscriptionDataProvider == null ? ParticipantData.DEFAULT_SELF_SUB_ID :
mSubscriptionDataProvider.getConversationSelfSubId();
}
private void setMessageText(final String messageText, final boolean notify) {
mMessageText = messageText;
mMessageTextStats.updateMessageTextStats(getSelfSubId(), mMessageText);
if (notify) {
dispatchChanged(MESSAGE_TEXT_CHANGED);
}
}
private void setMessageSubject(final String subject, final boolean notify) {
mMessageSubject = subject;
if (notify) {
dispatchChanged(MESSAGE_SUBJECT_CHANGED);
}
}
public void setMessageText(final String messageText) {
setMessageText(messageText, false);
}
public void setMessageSubject(final String subject) {
setMessageSubject(subject, false);
}
public void addAttachments(final Collection<? extends MessagePartData> attachments) {
// If the incoming attachments contains a single-only attachment, we need to clear
// the existing attachments.
for (final MessagePartData data : attachments) {
if (data.isSinglePartOnly()) {
// clear any existing attachments because the attachment we're adding can only
// exist by itself.
destroyAttachments();
break;
}
}
// If the existing attachments contain a single-only attachment, we need to clear the
// existing attachments to make room for the incoming attachment.
for (final MessagePartData data : mAttachments) {
if (data.isSinglePartOnly()) {
// clear any existing attachments because the single attachment can only exist
// by itself
destroyAttachments();
break;
}
}
// If any of the pending attachments contain a single-only attachment, we need to clear the
// existing attachments to make room for the incoming attachment.
for (final MessagePartData data : mPendingAttachments) {
if (data.isSinglePartOnly()) {
// clear any existing attachments because the single attachment can only exist
// by itself
destroyAttachments();
break;
}
}
boolean reachedLimit = false;
for (final MessagePartData data : attachments) {
// Don't break out of loop even if limit has been reached so we can destroy all
// of the over-limit attachments.
reachedLimit |= addOneAttachmentNoNotify(data);
}
if (reachedLimit) {
dispatchAttachmentLimitReached();
}
dispatchChanged(ATTACHMENTS_CHANGED);
}
public boolean containsAttachment(final Uri contentUri) {
for (final MessagePartData existingAttachment : mAttachments) {
if (existingAttachment.getContentUri().equals(contentUri)) {
return true;
}
}
for (final PendingAttachmentData pendingAttachment : mPendingAttachments) {
if (pendingAttachment.getContentUri().equals(contentUri)) {
return true;
}
}
return false;
}
/**
* Try to add one attachment to the attachment list, while guarding against duplicates and
* going over the limit.
* @return true if the attachment limit was reached, false otherwise
*/
private boolean addOneAttachmentNoNotify(final MessagePartData attachment) {
Assert.isTrue(attachment.isAttachment());
final boolean reachedLimit = getAttachmentCount() >= getAttachmentLimit();
if (reachedLimit || containsAttachment(attachment.getContentUri())) {
// Never go over the limit. Never add duplicated attachments.
attachment.destroyAsync();
return reachedLimit;
} else {
addAttachment(attachment, null /*pendingAttachment*/);
return false;
}
}
private void addAttachment(final MessagePartData attachment,
final PendingAttachmentData pendingAttachment) {
if (attachment != null && attachment.isSinglePartOnly()) {
// clear any existing attachments because the attachment we're adding can only
// exist by itself.
destroyAttachments();
}
if (pendingAttachment != null && pendingAttachment.isSinglePartOnly()) {
// clear any existing attachments because the attachment we're adding can only
// exist by itself.
destroyAttachments();
}
// If the existing attachments contain a single-only attachment, we need to clear the
// existing attachments to make room for the incoming attachment.
for (final MessagePartData data : mAttachments) {
if (data.isSinglePartOnly()) {
// clear any existing attachments because the single attachment can only exist
// by itself
destroyAttachments();
break;
}
}
// If any of the pending attachments contain a single-only attachment, we need to clear the
// existing attachments to make room for the incoming attachment.
for (final MessagePartData data : mPendingAttachments) {
if (data.isSinglePartOnly()) {
// clear any existing attachments because the single attachment can only exist
// by itself
destroyAttachments();
break;
}
}
if (attachment != null) {
mAttachments.add(attachment);
} else if (pendingAttachment != null) {
mPendingAttachments.add(pendingAttachment);
}
}
public void addPendingAttachment(final PendingAttachmentData pendingAttachment,
final BindingBase<DraftMessageData> binding) {
final boolean reachedLimit = addOnePendingAttachmentNoNotify(pendingAttachment,
binding.getBindingId());
if (reachedLimit) {
dispatchAttachmentLimitReached();
}
dispatchChanged(ATTACHMENTS_CHANGED);
}
/**
* Try to add one pending attachment, while guarding against duplicates and
* going over the limit.
* @return true if the attachment limit was reached, false otherwise
*/
private boolean addOnePendingAttachmentNoNotify(final PendingAttachmentData pendingAttachment,
final String bindingId) {
final boolean reachedLimit = getAttachmentCount() >= getAttachmentLimit();
if (reachedLimit || containsAttachment(pendingAttachment.getContentUri())) {
// Never go over the limit. Never add duplicated attachments.
pendingAttachment.destroyAsync();
return reachedLimit;
} else {
Assert.isTrue(!mPendingAttachments.contains(pendingAttachment));
Assert.equals(PendingAttachmentData.STATE_PENDING, pendingAttachment.getCurrentState());
addAttachment(null /*attachment*/, pendingAttachment);
pendingAttachment.loadAttachmentForDraft(this, bindingId);
return false;
}
}
public void setSelfId(final String selfId, final boolean notify) {
LogUtil.d(LogUtil.BUGLE_TAG, "DraftMessageData: set selfId=" + selfId
+ " for conversationId=" + mConversationId);
mSelfId = selfId;
if (notify) {
dispatchChanged(SELF_CHANGED);
}
}
public boolean hasAttachments() {
return !mAttachments.isEmpty();
}
public boolean hasPendingAttachments() {
return !mPendingAttachments.isEmpty();
}
private int getAttachmentCount() {
return mAttachments.size() + mPendingAttachments.size();
}
private int getVideoAttachmentCount() {
int count = 0;
for (MessagePartData part : mAttachments) {
if (part.isVideo()) {
count++;
}
}
for (MessagePartData part : mPendingAttachments) {
if (part.isVideo()) {
count++;
}
}
return count;
}
private int getAttachmentLimit() {
return BugleGservices.get().getInt(
BugleGservicesKeys.MMS_ATTACHMENT_LIMIT,
BugleGservicesKeys.MMS_ATTACHMENT_LIMIT_DEFAULT);
}
public void removeAttachment(final MessagePartData attachment) {
for (final MessagePartData existingAttachment : mAttachments) {
if (existingAttachment.getContentUri().equals(attachment.getContentUri())) {
mAttachments.remove(existingAttachment);
existingAttachment.destroyAsync();
dispatchChanged(ATTACHMENTS_CHANGED);
break;
}
}
}
public void removeExistingAttachments(final Set<MessagePartData> attachmentsToRemove) {
boolean removed = false;
final Iterator<MessagePartData> iterator = mAttachments.iterator();
while (iterator.hasNext()) {
final MessagePartData existingAttachment = iterator.next();
if (attachmentsToRemove.contains(existingAttachment)) {
iterator.remove();
existingAttachment.destroyAsync();
removed = true;
}
}
if (removed) {
dispatchChanged(ATTACHMENTS_CHANGED);
}
}
public void removePendingAttachment(final PendingAttachmentData pendingAttachment) {
for (final PendingAttachmentData existingAttachment : mPendingAttachments) {
if (existingAttachment.getContentUri().equals(pendingAttachment.getContentUri())) {
mPendingAttachments.remove(pendingAttachment);
pendingAttachment.destroyAsync();
dispatchChanged(ATTACHMENTS_CHANGED);
break;
}
}
}
public void updatePendingAttachment(final MessagePartData updatedAttachment,
final PendingAttachmentData pendingAttachment) {
for (final PendingAttachmentData existingAttachment : mPendingAttachments) {
if (existingAttachment.getContentUri().equals(pendingAttachment.getContentUri())) {
mPendingAttachments.remove(pendingAttachment);
if (pendingAttachment.isSinglePartOnly()) {
updatedAttachment.setSinglePartOnly(true);
}
mAttachments.add(updatedAttachment);
dispatchChanged(ATTACHMENTS_CHANGED);
return;
}
}
// If we are here, this means the pending attachment has been dropped before the task
// to load it was completed. In this case destroy the temporarily staged file since it
// is no longer needed.
updatedAttachment.destroyAsync();
}
/**
* Remove the attachments from the draft and notify any listeners.
* @param flags typically this will be ATTACHMENTS_CHANGED. When attachments are cleared in a
* widget, flags will also contain WIDGET_CHANGED.
*/
public void clearAttachments(final int flags) {
destroyAttachments();
dispatchChanged(flags);
}
public List<MessagePartData> getReadOnlyAttachments() {
return mReadOnlyAttachments;
}
public List<PendingAttachmentData> getReadOnlyPendingAttachments() {
return mReadOnlyPendingAttachments;
}
public boolean loadFromStorage(final BindingBase<DraftMessageData> binding,
final MessageData optionalIncomingDraft, boolean clearLocalDraft) {
LogUtil.d(LogUtil.BUGLE_TAG, "DraftMessageData: "
+ (optionalIncomingDraft == null ? "loading" : "setting")
+ " for conversationId=" + mConversationId);
if (clearLocalDraft) {
clearLocalDraftCopy();
}
final boolean isDraftCachedCopy = mIsDraftCachedCopy;
mIsDraftCachedCopy = false;
// Before reading message from db ensure the caller is bound to us (and knows the id)
if (mMonitor == null && !isDraftCachedCopy && isBound(binding.getBindingId())) {
mMonitor = ReadDraftDataAction.readDraftData(mConversationId,
optionalIncomingDraft, binding.getBindingId(), this);
return true;
}
return false;
}
/**
* Saves the current draft to db. This will save the draft and drop any pending attachments
* we have. The UI typically goes into the background when this is called, and instead of
* trying to persist the state of the pending attachments (the app may be killed, the activity
* may be destroyed), we simply drop the pending attachments for consistency.
*/
public void saveToStorage(final BindingBase<DraftMessageData> binding) {
saveToStorageInternal(binding);
dropPendingAttachments();
}
private void saveToStorageInternal(final BindingBase<DraftMessageData> binding) {
// Create MessageData to store to db, but don't clear the in-memory copy so UI will
// continue to display it.
// If self id is null then we'll not attempt to change the conversation's self id.
final MessageData message = createMessageWithCurrentAttachments(false /* clearLocalCopy */);
// Before writing message to db ensure the caller is bound to us (and knows the id)
if (isBound(binding.getBindingId())){
WriteDraftMessageAction.writeDraftMessage(mConversationId, message);
}
}
/**
* Called when we are ready to send the message. This will assemble/return the MessageData for
* sending and clear the local draft data, both from memory and from DB. This will also bind
* the message data with a self Id through which the message will be sent.
*
* @param binding the binding object from our consumer. We need to make sure we are still bound
* to that binding before saving to storage.
*/
public MessageData prepareMessageForSending(final BindingBase<DraftMessageData> binding) {
// We can't send the message while there's still stuff pending.
Assert.isTrue(!hasPendingAttachments());
mSending = true;
// Assembles the message to send and empty working draft data.
// If self id is null then message is sent with conversation's self id.
final MessageData messageToSend =
createMessageWithCurrentAttachments(true /* clearLocalCopy */);
// Note sending message will empty the draft data in DB.
mSending = false;
return messageToSend;
}
public boolean isSending() {
return mSending;
}
@Override // ReadDraftMessageActionListener.onReadDraftMessageSucceeded
public void onReadDraftDataSucceeded(final ReadDraftDataAction action, final Object data,
final MessageData message, final ConversationListItemData conversation) {
final String bindingId = (String) data;
// Before passing draft message on to ui ensure the data is bound to the same bindingid
if (isBound(bindingId)) {
mSelfId = message.getSelfId();
mIsGroupConversation = conversation.getIsGroup();
mIncludeEmailAddress = conversation.getIncludeEmailAddress();
updateFromMessageData(message, bindingId);
LogUtil.d(LogUtil.BUGLE_TAG, "DraftMessageData: draft loaded. "
+ "conversationId=" + mConversationId + " selfId=" + mSelfId);
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "DraftMessageData: draft loaded but not bound. "
+ "conversationId=" + mConversationId);
}
mMonitor = null;
}
@Override // ReadDraftMessageActionListener.onReadDraftDataFailed
public void onReadDraftDataFailed(final ReadDraftDataAction action, final Object data) {
LogUtil.w(LogUtil.BUGLE_TAG, "DraftMessageData: draft not loaded. "
+ "conversationId=" + mConversationId);
// The draft is now synced with actual MessageData and no longer a cached copy.
mIsDraftCachedCopy = false;
// Just clear the monitor - no update to draft data
mMonitor = null;
}
/**
* Check if Bugle is default sms app
* @return
*/
public boolean getIsDefaultSmsApp() {
return PhoneUtils.getDefault().isDefaultSmsApp();
}
@Override //BindableData.unregisterListeners
protected void unregisterListeners() {
if (mMonitor != null) {
mMonitor.unregister();
}
mMonitor = null;
mListeners.clear();
}
private void destroyAttachments() {
for (final MessagePartData attachment : mAttachments) {
attachment.destroyAsync();
}
mAttachments.clear();
mPendingAttachments.clear();
}
private void dispatchChanged(final int changeFlags) {
// No change is expected to be made to the draft if it is in cached copy state.
if (mIsDraftCachedCopy) {
return;
}
// Any change in the draft will cancel any pending draft checking task, since the
// size/status of the draft may have changed.
if (mCheckDraftForSendTask != null) {
mCheckDraftForSendTask.cancel(true /* mayInterruptIfRunning */);
mCheckDraftForSendTask = null;
}
mListeners.onDraftChanged(this, changeFlags);
}
private void dispatchAttachmentLimitReached() {
mListeners.onDraftAttachmentLimitReached(this);
}
/**
* Drop any pending attachments that haven't finished. This is called after the UI goes to
* the background and we persist the draft data to the database.
*/
private void dropPendingAttachments() {
mPendingAttachments.clear();
}
private boolean isDraftEmpty() {
return TextUtils.isEmpty(mMessageText) && mAttachments.isEmpty() &&
TextUtils.isEmpty(mMessageSubject);
}
public boolean isCheckingDraft() {
return mCheckDraftForSendTask != null && !mCheckDraftForSendTask.isCancelled();
}
public void checkDraftForAction(final boolean checkMessageSize, final int selfSubId,
final CheckDraftTaskCallback callback, final Binding<DraftMessageData> binding) {
new CheckDraftForSendTask(checkMessageSize, selfSubId, callback, binding)
.executeOnThreadPool((Void) null);
}
/**
* Allows us to have multiple data listeners for DraftMessageData
*/
private class DraftMessageDataEventDispatcher
extends ArrayList<DraftMessageDataListener>
implements DraftMessageDataListener {
@Override
@RunsOnMainThread
public void onDraftChanged(DraftMessageData data, int changeFlags) {
Assert.isMainThread();
for (final DraftMessageDataListener listener : this) {
listener.onDraftChanged(data, changeFlags);
}
}
@Override
@RunsOnMainThread
public void onDraftAttachmentLimitReached(DraftMessageData data) {
Assert.isMainThread();
for (final DraftMessageDataListener listener : this) {
listener.onDraftAttachmentLimitReached(data);
}
}
@Override
@RunsOnMainThread
public void onDraftAttachmentLoadFailed() {
Assert.isMainThread();
for (final DraftMessageDataListener listener : this) {
listener.onDraftAttachmentLoadFailed();
}
}
}
public interface CheckDraftTaskCallback {
void onDraftChecked(DraftMessageData data, int result);
}
public class CheckDraftForSendTask extends SafeAsyncTask<Void, Void, Integer> {
public static final int RESULT_PASSED = 0;
public static final int RESULT_HAS_PENDING_ATTACHMENTS = 1;
public static final int RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS = 2;
public static final int RESULT_MESSAGE_OVER_LIMIT = 3;
public static final int RESULT_VIDEO_ATTACHMENT_LIMIT_EXCEEDED = 4;
public static final int RESULT_SIM_NOT_READY = 5;
private final boolean mCheckMessageSize;
private final int mSelfSubId;
private final CheckDraftTaskCallback mCallback;
private final String mBindingId;
private final List<MessagePartData> mAttachmentsCopy;
private int mPreExecuteResult = RESULT_PASSED;
public CheckDraftForSendTask(final boolean checkMessageSize, final int selfSubId,
final CheckDraftTaskCallback callback, final Binding<DraftMessageData> binding) {
mCheckMessageSize = checkMessageSize;
mSelfSubId = selfSubId;
mCallback = callback;
mBindingId = binding.getBindingId();
// Obtain an immutable copy of the attachment list so we can operate on it in the
// background thread.
mAttachmentsCopy = new ArrayList<MessagePartData>(mAttachments);
mCheckDraftForSendTask = this;
}
@Override
protected void onPreExecute() {
// Perform checking work that can happen on the main thread.
if (hasPendingAttachments()) {
mPreExecuteResult = RESULT_HAS_PENDING_ATTACHMENTS;
return;
}
if (getIsGroupMmsConversation()) {
try {
if (TextUtils.isEmpty(PhoneUtils.get(mSelfSubId).getSelfRawNumber(true))) {
mPreExecuteResult = RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS;
return;
}
} catch (IllegalStateException e) {
// This happens when there is no active subscription, e.g. on Nova
// when the phone switches carrier.
mPreExecuteResult = RESULT_SIM_NOT_READY;
return;
}
}
if (getVideoAttachmentCount() > MmsUtils.MAX_VIDEO_ATTACHMENT_COUNT) {
mPreExecuteResult = RESULT_VIDEO_ATTACHMENT_LIMIT_EXCEEDED;
return;
}
}
@Override
protected Integer doInBackgroundTimed(Void... params) {
if (mPreExecuteResult != RESULT_PASSED) {
return mPreExecuteResult;
}
if (mCheckMessageSize && getIsMessageOverLimit()) {
return RESULT_MESSAGE_OVER_LIMIT;
}
return RESULT_PASSED;
}
@Override
protected void onPostExecute(Integer result) {
mCheckDraftForSendTask = null;
// Only call back if we are bound to the original binding.
if (isBound(mBindingId) && !isCancelled()) {
mCallback.onDraftChecked(DraftMessageData.this, result);
} else {
if (!isBound(mBindingId)) {
LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: draft not bound");
}
if (isCancelled()) {
LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: draft is cancelled");
}
}
}
@Override
protected void onCancelled() {
mCheckDraftForSendTask = null;
}
/**
* 1. Check if the draft message contains too many attachments to send
* 2. Computes the minimum size that this message could be compressed/downsampled/encoded
* before sending and check if it meets the carrier max size for sending.
* @see MessagePartData#getMinimumSizeInBytesForSending()
*/
@DoesNotRunOnMainThread
private boolean getIsMessageOverLimit() {
Assert.isNotMainThread();
if (mAttachmentsCopy.size() > getAttachmentLimit()) {
return true;
}
// Aggregate the size from all the attachments.
long totalSize = 0;
for (final MessagePartData attachment : mAttachmentsCopy) {
totalSize += attachment.getMinimumSizeInBytesForSending();
}
return totalSize > MmsConfig.get(mSelfSubId).getMaxMessageSize();
}
}
public void onPendingAttachmentLoadFailed(PendingAttachmentData data) {
mListeners.onDraftAttachmentLoadFailed();
}
}
@@ -1,128 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.database.Cursor;
import android.graphics.Rect;
import android.net.Uri;
import android.provider.BaseColumns;
import android.provider.MediaStore.Images.Media;
import android.text.TextUtils;
import com.android.messaging.datamodel.media.FileImageRequestDescriptor;
import com.android.messaging.datamodel.media.ImageRequest;
import com.android.messaging.datamodel.media.UriImageRequestDescriptor;
import com.android.messaging.util.Assert;
/**
* Provides data for GalleryGridItemView
*/
public class GalleryGridItemData {
public static final String[] IMAGE_PROJECTION = new String[] {
Media._ID,
Media.DATA,
Media.WIDTH,
Media.HEIGHT,
Media.MIME_TYPE,
Media.DATE_MODIFIED};
public static final String[] SPECIAL_ITEM_COLUMNS = new String[] {
BaseColumns._ID
};
private static final int INDEX_ID = 0;
// For local image gallery.
private static final int INDEX_DATA_PATH = 1;
private static final int INDEX_WIDTH = 2;
private static final int INDEX_HEIGHT = 3;
private static final int INDEX_MIME_TYPE = 4;
private static final int INDEX_DATE_MODIFIED = 5;
/** A special item's id for picking images from document picker */
public static final String ID_DOCUMENT_PICKER_ITEM = "-1";
private UriImageRequestDescriptor mImageData;
private String mContentType;
private boolean mIsDocumentPickerItem;
private long mDateSeconds;
public GalleryGridItemData() {
}
public void bind(final Cursor cursor, final int desiredWidth, final int desiredHeight) {
mIsDocumentPickerItem = TextUtils.equals(cursor.getString(INDEX_ID),
ID_DOCUMENT_PICKER_ITEM);
if (mIsDocumentPickerItem) {
mImageData = null;
mContentType = null;
} else {
int sourceWidth = cursor.getInt(INDEX_WIDTH);
int sourceHeight = cursor.getInt(INDEX_HEIGHT);
// Guard against bad data
if (sourceWidth <= 0) {
sourceWidth = ImageRequest.UNSPECIFIED_SIZE;
}
if (sourceHeight <= 0) {
sourceHeight = ImageRequest.UNSPECIFIED_SIZE;
}
mContentType = cursor.getString(INDEX_MIME_TYPE);
final String dateModified = cursor.getString(INDEX_DATE_MODIFIED);
mDateSeconds = !TextUtils.isEmpty(dateModified) ? Long.parseLong(dateModified) : -1;
mImageData = new FileImageRequestDescriptor(
cursor.getString(INDEX_DATA_PATH),
desiredWidth,
desiredHeight,
sourceWidth,
sourceHeight,
true /* canUseThumbnail */,
true /* allowCompression */,
true /* isStatic */);
}
}
public boolean isDocumentPickerItem() {
return mIsDocumentPickerItem;
}
public Uri getImageUri() {
return mImageData.uri;
}
public UriImageRequestDescriptor getImageRequestDescriptor() {
return mImageData;
}
public MessagePartData constructMessagePartData(final Rect startRect) {
Assert.isTrue(!mIsDocumentPickerItem);
return new MediaPickerMessagePartData(startRect, mContentType,
mImageData.uri, mImageData.sourceWidth, mImageData.sourceHeight);
}
/**
* @return The date in seconds. This can be negative if we could not retreive date info
*/
public long getDateSeconds() {
return mDateSeconds;
}
public String getContentType() {
return mContentType;
}
}
@@ -1,90 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import com.android.messaging.datamodel.action.ActionMonitor;
import com.android.messaging.datamodel.action.GetOrCreateConversationAction;
import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionListener;
import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionMonitor;
import com.android.messaging.datamodel.binding.BindableData;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.util.Assert;
import com.android.messaging.util.Assert.RunsOnMainThread;
import com.android.messaging.util.LogUtil;
public class LaunchConversationData extends BindableData implements
GetOrCreateConversationActionListener {
public interface LaunchConversationDataListener {
void onGetOrCreateNewConversation(String conversationId);
void onGetOrCreateNewConversationFailed();
}
private LaunchConversationDataListener mListener;
private GetOrCreateConversationActionMonitor mMonitor;
public LaunchConversationData(final LaunchConversationDataListener listener) {
mListener = listener;
}
@Override
protected void unregisterListeners() {
mListener = null;
if (mMonitor != null) {
mMonitor.unregister();
}
mMonitor = null;
}
public void getOrCreateConversation(final BindingBase<LaunchConversationData> binding,
final String[] recipients) {
final String bindingId = binding.getBindingId();
// Start a new conversation from the list of contacts.
if (isBound(bindingId) && mMonitor == null) {
mMonitor = GetOrCreateConversationAction.getOrCreateConversation(recipients,
bindingId, this);
}
}
@Override
@RunsOnMainThread
public void onGetOrCreateConversationSucceeded(final ActionMonitor monitor,
final Object data, final String conversationId) {
Assert.isTrue(monitor == mMonitor);
Assert.isTrue(conversationId != null);
final String bindingId = (String) data;
if (isBound(bindingId) && mListener != null) {
mListener.onGetOrCreateNewConversation(conversationId);
}
mMonitor = null;
}
@Override
@RunsOnMainThread
public void onGetOrCreateConversationFailed(final ActionMonitor monitor,
final Object data) {
Assert.isTrue(monitor == mMonitor);
final String bindingId = (String) data;
if (isBound(bindingId) && mListener != null) {
mListener.onGetOrCreateNewConversationFailed();
}
LogUtil.e(LogUtil.BUGLE_TAG, "onGetOrCreateConversationFailed");
mMonitor = null;
}
}
@@ -1,175 +0,0 @@
/*
* 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.os.Bundle;
import android.support.annotation.Nullable;
import com.android.messaging.datamodel.BoundCursorLoader;
import com.android.messaging.datamodel.GalleryBoundCursorLoader;
import com.android.messaging.datamodel.binding.BindableData;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.util.Assert;
import com.android.messaging.util.BuglePrefs;
import com.android.messaging.util.BuglePrefsKeys;
import com.android.messaging.util.LogUtil;
/**
* Services data needs for MediaPicker.
*/
public class MediaPickerData extends BindableData {
public interface MediaPickerDataListener {
void onMediaPickerDataUpdated(MediaPickerData mediaPickerData, Object data, int loaderId);
}
private static final String BINDING_ID = "bindingId";
private final Context mContext;
private LoaderManager mLoaderManager;
private final GalleryLoaderCallbacks mGalleryLoaderCallbacks;
private MediaPickerDataListener mListener;
public MediaPickerData(final Context context) {
mContext = context;
mGalleryLoaderCallbacks = new GalleryLoaderCallbacks();
}
public static final int GALLERY_IMAGE_LOADER = 1;
/**
* A trampoline class so that we can inherit from LoaderManager.LoaderCallbacks multiple times.
*/
private class GalleryLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
@Override
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
final String bindingId = args.getString(BINDING_ID);
// Check if data still bound to the requesting ui element
if (isBound(bindingId)) {
switch (id) {
case GALLERY_IMAGE_LOADER:
return new GalleryBoundCursorLoader(bindingId, mContext);
default:
Assert.fail("Unknown loader id for gallery picker!");
break;
}
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "Loader created after unbinding the media picker");
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) {
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
if (isBound(cursorLoader.getBindingId())) {
switch (loader.getId()) {
case GALLERY_IMAGE_LOADER:
mListener.onMediaPickerDataUpdated(MediaPickerData.this, data,
GALLERY_IMAGE_LOADER);
break;
default:
Assert.fail("Unknown loader id for gallery picker!");
break;
}
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "Loader finished after unbinding the media picker");
}
}
/**
* {@inheritDoc}
*/
@Override
public void onLoaderReset(final Loader<Cursor> loader) {
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
if (isBound(cursorLoader.getBindingId())) {
switch (loader.getId()) {
case GALLERY_IMAGE_LOADER:
mListener.onMediaPickerDataUpdated(MediaPickerData.this, null,
GALLERY_IMAGE_LOADER);
break;
default:
Assert.fail("Unknown loader id for media picker!");
break;
}
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "Loader reset after unbinding the media picker");
}
}
}
public void startLoader(final int loaderId, final BindingBase<MediaPickerData> binding,
@Nullable Bundle args, final MediaPickerDataListener listener) {
if (args == null) {
args = new Bundle();
}
args.putString(BINDING_ID, binding.getBindingId());
if (loaderId == GALLERY_IMAGE_LOADER) {
mLoaderManager.initLoader(loaderId, args, mGalleryLoaderCallbacks).forceLoad();
} else {
Assert.fail("Unsupported loader id for media picker!");
}
mListener = listener;
}
public void destroyLoader(final int loaderId) {
mLoaderManager.destroyLoader(loaderId);
}
public void init(final LoaderManager loaderManager) {
mLoaderManager = loaderManager;
}
@Override
protected void unregisterListeners() {
// This could be null if we bind but the caller doesn't init the BindableData
if (mLoaderManager != null) {
mLoaderManager.destroyLoader(GALLERY_IMAGE_LOADER);
mLoaderManager = null;
}
}
/**
* Gets the last selected chooser index, or -1 if no selection has been saved.
*/
public int getSelectedChooserIndex() {
return BuglePrefs.getApplicationPrefs().getInt(
BuglePrefsKeys.SELECTED_MEDIA_PICKER_CHOOSER_INDEX,
BuglePrefsKeys.SELECTED_MEDIA_PICKER_CHOOSER_INDEX_DEFAULT);
}
/**
* Saves the selected media chooser index.
* @param selectedIndex the selected media chooser index.
*/
public void saveSelectedChooserIndex(final int selectedIndex) {
BuglePrefs.getApplicationPrefs().putInt(BuglePrefsKeys.SELECTED_MEDIA_PICKER_CHOOSER_INDEX,
selectedIndex);
}
}
@@ -1,64 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.graphics.Rect;
import android.net.Uri;
public class MediaPickerMessagePartData extends MessagePartData {
private final Rect mStartRect;
public MediaPickerMessagePartData(final Rect startRect, final String contentType,
final Uri contentUri, final int width, final int height) {
this(startRect, null /* messageText */, contentType, contentUri, width, height);
}
public MediaPickerMessagePartData(final Rect startRect, final String messageText,
final String contentType, final Uri contentUri, final int width, final int height) {
this(startRect, messageText, contentType, contentUri, width, height,
false /*onlySingleAttachment*/);
}
public MediaPickerMessagePartData(final Rect startRect, final String contentType,
final Uri contentUri, final int width, final int height,
final boolean onlySingleAttachment) {
this(startRect, null /* messageText */, contentType, contentUri, width, height,
onlySingleAttachment);
}
public MediaPickerMessagePartData(final Rect startRect, final String messageText,
final String contentType, final Uri contentUri, final int width, final int height,
final boolean onlySingleAttachment) {
super(messageText, contentType, contentUri, width, height, onlySingleAttachment);
mStartRect = startRect;
}
/**
* @return The starting rect to animate the attachment preview from in order to perform a smooth
* transition
*/
public Rect getStartRect() {
return mStartRect;
}
/**
* Modify the start rect of the attachment.
*/
public void setStartRect(final Rect startRect) {
mStartRect.set(startRect);
}
}
@@ -1,922 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteStatement;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import com.android.messaging.datamodel.DatabaseHelper;
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.Assert;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.Dates;
import com.android.messaging.util.DebugUtils;
import com.android.messaging.util.OsUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class MessageData implements Parcelable {
private static final String[] sProjection = {
MessageColumns._ID,
MessageColumns.CONVERSATION_ID,
MessageColumns.SENDER_PARTICIPANT_ID,
MessageColumns.SELF_PARTICIPANT_ID,
MessageColumns.SENT_TIMESTAMP,
MessageColumns.RECEIVED_TIMESTAMP,
MessageColumns.SEEN,
MessageColumns.READ,
MessageColumns.PROTOCOL,
MessageColumns.STATUS,
MessageColumns.SMS_MESSAGE_URI,
MessageColumns.SMS_PRIORITY,
MessageColumns.SMS_MESSAGE_SIZE,
MessageColumns.MMS_SUBJECT,
MessageColumns.MMS_TRANSACTION_ID,
MessageColumns.MMS_CONTENT_LOCATION,
MessageColumns.MMS_EXPIRY,
MessageColumns.RAW_TELEPHONY_STATUS,
MessageColumns.RETRY_START_TIMESTAMP,
};
private static final int INDEX_ID = 0;
private static final int INDEX_CONVERSATION_ID = 1;
private static final int INDEX_PARTICIPANT_ID = 2;
private static final int INDEX_SELF_ID = 3;
private static final int INDEX_SENT_TIMESTAMP = 4;
private static final int INDEX_RECEIVED_TIMESTAMP = 5;
private static final int INDEX_SEEN = 6;
private static final int INDEX_READ = 7;
private static final int INDEX_PROTOCOL = 8;
private static final int INDEX_BUGLE_STATUS = 9;
private static final int INDEX_SMS_MESSAGE_URI = 10;
private static final int INDEX_SMS_PRIORITY = 11;
private static final int INDEX_SMS_MESSAGE_SIZE = 12;
private static final int INDEX_MMS_SUBJECT = 13;
private static final int INDEX_MMS_TRANSACTION_ID = 14;
private static final int INDEX_MMS_CONTENT_LOCATION = 15;
private static final int INDEX_MMS_EXPIRY = 16;
private static final int INDEX_RAW_TELEPHONY_STATUS = 17;
private static final int INDEX_RETRY_START_TIMESTAMP = 18;
// SQL statement to insert a "complete" message row (columns based on the projection above).
private static final String INSERT_MESSAGE_SQL =
"INSERT INTO " + DatabaseHelper.MESSAGES_TABLE + " ( "
+ TextUtils.join(", ", Arrays.copyOfRange(sProjection, 1,
INDEX_RETRY_START_TIMESTAMP + 1))
+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
private String mMessageId;
private String mConversationId;
private String mParticipantId;
private String mSelfId;
private long mSentTimestamp;
private long mReceivedTimestamp;
private boolean mSeen;
private boolean mRead;
private int mProtocol;
private Uri mSmsMessageUri;
private int mSmsPriority;
private long mSmsMessageSize;
private String mMmsSubject;
private String mMmsTransactionId;
private String mMmsContentLocation;
private long mMmsExpiry;
private int mRawStatus;
private int mStatus;
private final ArrayList<MessagePartData> mParts;
private long mRetryStartTimestamp;
// PROTOCOL Values
public static final int PROTOCOL_UNKNOWN = -1; // Unknown type
public static final int PROTOCOL_SMS = 0; // SMS message
public static final int PROTOCOL_MMS = 1; // MMS message
public static final int PROTOCOL_MMS_PUSH_NOTIFICATION = 2; // MMS WAP push notification
// Bugle STATUS Values
public static final int BUGLE_STATUS_UNKNOWN = 0;
// Outgoing
public static final int BUGLE_STATUS_OUTGOING_COMPLETE = 1;
public static final int BUGLE_STATUS_OUTGOING_DELIVERED = 2;
// Transitions to either YET_TO_SEND or SEND_AFTER_PROCESSING depending attachments.
public static final int BUGLE_STATUS_OUTGOING_DRAFT = 3;
public static final int BUGLE_STATUS_OUTGOING_YET_TO_SEND = 4;
public static final int BUGLE_STATUS_OUTGOING_SENDING = 5;
public static final int BUGLE_STATUS_OUTGOING_RESENDING = 6;
public static final int BUGLE_STATUS_OUTGOING_AWAITING_RETRY = 7;
public static final int BUGLE_STATUS_OUTGOING_FAILED = 8;
public static final int BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER = 9;
// Incoming
public static final int BUGLE_STATUS_INCOMING_COMPLETE = 100;
public static final int BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD = 101;
public static final int BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD = 102;
public static final int BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING = 103;
public static final int BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD = 104;
public static final int BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING = 105;
public static final int BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED = 106;
public static final int BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE = 107;
public static final String getStatusDescription(int status) {
switch (status) {
case BUGLE_STATUS_UNKNOWN:
return "UNKNOWN";
case BUGLE_STATUS_OUTGOING_COMPLETE:
return "OUTGOING_COMPLETE";
case BUGLE_STATUS_OUTGOING_DELIVERED:
return "OUTGOING_DELIVERED";
case BUGLE_STATUS_OUTGOING_DRAFT:
return "OUTGOING_DRAFT";
case BUGLE_STATUS_OUTGOING_YET_TO_SEND:
return "OUTGOING_YET_TO_SEND";
case BUGLE_STATUS_OUTGOING_SENDING:
return "OUTGOING_SENDING";
case BUGLE_STATUS_OUTGOING_RESENDING:
return "OUTGOING_RESENDING";
case BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
return "OUTGOING_AWAITING_RETRY";
case BUGLE_STATUS_OUTGOING_FAILED:
return "OUTGOING_FAILED";
case BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
return "OUTGOING_FAILED_EMERGENCY_NUMBER";
case BUGLE_STATUS_INCOMING_COMPLETE:
return "INCOMING_COMPLETE";
case BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD:
return "INCOMING_YET_TO_MANUAL_DOWNLOAD";
case BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD:
return "INCOMING_RETRYING_MANUAL_DOWNLOAD";
case BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING:
return "INCOMING_MANUAL_DOWNLOADING";
case BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD:
return "INCOMING_RETRYING_AUTO_DOWNLOAD";
case BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING:
return "INCOMING_AUTO_DOWNLOADING";
case BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED:
return "INCOMING_DOWNLOAD_FAILED";
case BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE:
return "INCOMING_EXPIRED_OR_NOT_AVAILABLE";
default:
return String.valueOf(status) + " (check MessageData)";
}
}
// All incoming messages expect to have status >= BUGLE_STATUS_FIRST_INCOMING
public static final int BUGLE_STATUS_FIRST_INCOMING = BUGLE_STATUS_INCOMING_COMPLETE;
// Detailed MMS failures. Most of the values are defined in PduHeaders. However, a few are
// defined here instead. These are never returned in the MMS HTTP response, but are used
// internally. The values here must not conflict with any of the existing PduHeader values.
public static final int RAW_TELEPHONY_STATUS_UNDEFINED = MmsUtils.PDU_HEADER_VALUE_UNDEFINED;
public static final int RAW_TELEPHONY_STATUS_MESSAGE_TOO_BIG = 10000;
// Unknown result code for MMS sending/downloading. This is used as the default value
// for result code returned from platform MMS API.
public static final int UNKNOWN_RESULT_CODE = 0;
/**
* Create an "empty" message
*/
public MessageData() {
mParts = new ArrayList<MessagePartData>();
}
public static String[] getProjection() {
return sProjection;
}
/**
* Create a draft message for a particular conversation based on supplied content
*/
public static MessageData createDraftMessage(final String conversationId,
final String selfId, final MessageData content) {
final MessageData message = new MessageData();
message.mStatus = BUGLE_STATUS_OUTGOING_DRAFT;
message.mProtocol = PROTOCOL_UNKNOWN;
message.mConversationId = conversationId;
message.mParticipantId = selfId;
message.mReceivedTimestamp = System.currentTimeMillis();
if (content == null) {
message.mParts.add(MessagePartData.createTextMessagePart(""));
} else {
if (!TextUtils.isEmpty(content.mParticipantId)) {
message.mParticipantId = content.mParticipantId;
}
if (!TextUtils.isEmpty(content.mMmsSubject)) {
message.mMmsSubject = content.mMmsSubject;
}
for (final MessagePartData part : content.getParts()) {
message.mParts.add(part);
}
}
message.mSelfId = selfId;
return message;
}
/**
* Create a draft sms message for a particular conversation
*/
public static MessageData createDraftSmsMessage(final String conversationId,
final String selfId, final String messageText) {
final MessageData message = new MessageData();
message.mStatus = BUGLE_STATUS_OUTGOING_DRAFT;
message.mProtocol = PROTOCOL_SMS;
message.mConversationId = conversationId;
message.mParticipantId = selfId;
message.mSelfId = selfId;
message.mParts.add(MessagePartData.createTextMessagePart(messageText));
message.mReceivedTimestamp = System.currentTimeMillis();
return message;
}
/**
* Create a draft mms message for a particular conversation
*/
public static MessageData createDraftMmsMessage(final String conversationId,
final String selfId, final String messageText, final String subjectText) {
final MessageData message = new MessageData();
message.mStatus = BUGLE_STATUS_OUTGOING_DRAFT;
message.mProtocol = PROTOCOL_MMS;
message.mConversationId = conversationId;
message.mParticipantId = selfId;
message.mSelfId = selfId;
message.mMmsSubject = subjectText;
message.mReceivedTimestamp = System.currentTimeMillis();
if (!TextUtils.isEmpty(messageText)) {
message.mParts.add(MessagePartData.createTextMessagePart(messageText));
}
return message;
}
/**
* Create a message received from a particular number in a particular conversation
*/
public static MessageData createReceivedSmsMessage(final Uri uri, final String conversationId,
final String participantId, final String selfId, final String messageText,
final String subject, final long sent, final long recieved,
final boolean seen, final boolean read) {
final MessageData message = new MessageData();
message.mSmsMessageUri = uri;
message.mConversationId = conversationId;
message.mParticipantId = participantId;
message.mSelfId = selfId;
message.mProtocol = PROTOCOL_SMS;
message.mStatus = BUGLE_STATUS_INCOMING_COMPLETE;
message.mMmsSubject = subject;
message.mReceivedTimestamp = recieved;
message.mSentTimestamp = sent;
message.mParts.add(MessagePartData.createTextMessagePart(messageText));
message.mSeen = seen;
message.mRead = read;
return message;
}
/**
* Create a message not yet associated with a particular conversation
*/
public static MessageData createSharedMessage(final String messageText) {
final MessageData message = new MessageData();
message.mStatus = BUGLE_STATUS_OUTGOING_DRAFT;
if (!TextUtils.isEmpty(messageText)) {
message.mParts.add(MessagePartData.createTextMessagePart(messageText));
}
return message;
}
/**
* Create a message from Sms table fields
*/
public static MessageData createSmsMessage(final String messageUri, final String participantId,
final String selfId, final String conversationId, final int bugleStatus,
final boolean seen, final boolean read, final long sent,
final long recieved, final String messageText) {
final MessageData message = new MessageData();
message.mParticipantId = participantId;
message.mSelfId = selfId;
message.mConversationId = conversationId;
message.mSentTimestamp = sent;
message.mReceivedTimestamp = recieved;
message.mSeen = seen;
message.mRead = read;
message.mProtocol = PROTOCOL_SMS;
message.mStatus = bugleStatus;
message.mSmsMessageUri = Uri.parse(messageUri);
message.mParts.add(MessagePartData.createTextMessagePart(messageText));
return message;
}
/**
* Create a message from Mms table fields
*/
public static MessageData createMmsMessage(final String messageUri, final String participantId,
final String selfId, final String conversationId, final boolean isNotification,
final int bugleStatus, final String contentLocation, final String transactionId,
final int smsPriority, final String subject, final boolean seen, final boolean read,
final long size, final int rawStatus, final long expiry, final long sent,
final long received) {
final MessageData message = new MessageData();
message.mParticipantId = participantId;
message.mSelfId = selfId;
message.mConversationId = conversationId;
message.mSentTimestamp = sent;
message.mReceivedTimestamp = received;
message.mMmsContentLocation = contentLocation;
message.mMmsTransactionId = transactionId;
message.mSeen = seen;
message.mRead = read;
message.mStatus = bugleStatus;
message.mProtocol = (isNotification ? PROTOCOL_MMS_PUSH_NOTIFICATION : PROTOCOL_MMS);
message.mSmsMessageUri = Uri.parse(messageUri);
message.mSmsPriority = smsPriority;
message.mSmsMessageSize = size;
message.mMmsSubject = subject;
message.mMmsExpiry = expiry;
message.mRawStatus = rawStatus;
if (bugleStatus == BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD ||
bugleStatus == BUGLE_STATUS_OUTGOING_RESENDING) {
// Set the retry start timestamp if this message is already in process of retrying
// Either as autodownload is starting or sending already in progress (MMS update)
message.mRetryStartTimestamp = received;
}
return message;
}
public void addPart(final MessagePartData part) {
if (part instanceof PendingAttachmentData) {
// Pending attachments may only be added to shared message data that's not associated
// with any particular conversation, in order to store shared images.
Assert.isTrue(mConversationId == null);
}
mParts.add(part);
}
public Iterable<MessagePartData> getParts() {
return mParts;
}
public void bind(final Cursor cursor) {
mMessageId = cursor.getString(INDEX_ID);
mConversationId = cursor.getString(INDEX_CONVERSATION_ID);
mParticipantId = cursor.getString(INDEX_PARTICIPANT_ID);
mSelfId = cursor.getString(INDEX_SELF_ID);
mSentTimestamp = cursor.getLong(INDEX_SENT_TIMESTAMP);
mReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP);
mSeen = (cursor.getInt(INDEX_SEEN) != 0);
mRead = (cursor.getInt(INDEX_READ) != 0);
mProtocol = cursor.getInt(INDEX_PROTOCOL);
mStatus = cursor.getInt(INDEX_BUGLE_STATUS);
final String smsMessageUri = cursor.getString(INDEX_SMS_MESSAGE_URI);
mSmsMessageUri = (smsMessageUri == null) ? null : Uri.parse(smsMessageUri);
mSmsPriority = cursor.getInt(INDEX_SMS_PRIORITY);
mSmsMessageSize = cursor.getLong(INDEX_SMS_MESSAGE_SIZE);
mMmsExpiry = cursor.getLong(INDEX_MMS_EXPIRY);
mRawStatus = cursor.getInt(INDEX_RAW_TELEPHONY_STATUS);
mMmsSubject = cursor.getString(INDEX_MMS_SUBJECT);
mMmsTransactionId = cursor.getString(INDEX_MMS_TRANSACTION_ID);
mMmsContentLocation = cursor.getString(INDEX_MMS_CONTENT_LOCATION);
mRetryStartTimestamp = cursor.getLong(INDEX_RETRY_START_TIMESTAMP);
}
/**
* Bind to the draft message data for a conversation. The conversation's self id is used as
* the draft's self id.
*/
public void bindDraft(final Cursor cursor, final String conversationSelfId) {
bind(cursor);
mSelfId = conversationSelfId;
}
protected static String getParticipantId(final Cursor cursor) {
return cursor.getString(INDEX_PARTICIPANT_ID);
}
public void populate(final ContentValues values) {
values.put(MessageColumns.CONVERSATION_ID, mConversationId);
values.put(MessageColumns.SENDER_PARTICIPANT_ID, mParticipantId);
values.put(MessageColumns.SELF_PARTICIPANT_ID, mSelfId);
values.put(MessageColumns.SENT_TIMESTAMP, mSentTimestamp);
values.put(MessageColumns.RECEIVED_TIMESTAMP, mReceivedTimestamp);
values.put(MessageColumns.SEEN, mSeen ? 1 : 0);
values.put(MessageColumns.READ, mRead ? 1 : 0);
values.put(MessageColumns.PROTOCOL, mProtocol);
values.put(MessageColumns.STATUS, mStatus);
final String smsMessageUri = ((mSmsMessageUri == null) ? null : mSmsMessageUri.toString());
values.put(MessageColumns.SMS_MESSAGE_URI, smsMessageUri);
values.put(MessageColumns.SMS_PRIORITY, mSmsPriority);
values.put(MessageColumns.SMS_MESSAGE_SIZE, mSmsMessageSize);
values.put(MessageColumns.MMS_EXPIRY, mMmsExpiry);
values.put(MessageColumns.MMS_SUBJECT, mMmsSubject);
values.put(MessageColumns.MMS_TRANSACTION_ID, mMmsTransactionId);
values.put(MessageColumns.MMS_CONTENT_LOCATION, mMmsContentLocation);
values.put(MessageColumns.RAW_TELEPHONY_STATUS, mRawStatus);
values.put(MessageColumns.RETRY_START_TIMESTAMP, mRetryStartTimestamp);
}
/**
* Note this is not thread safe so callers need to make sure they own the wrapper + statements
* while they call this and use the returned value.
*/
public SQLiteStatement getInsertStatement(final DatabaseWrapper db) {
final SQLiteStatement insert = db.getStatementInTransaction(
DatabaseWrapper.INDEX_INSERT_MESSAGE, INSERT_MESSAGE_SQL);
insert.clearBindings();
insert.bindString(INDEX_CONVERSATION_ID, mConversationId);
insert.bindString(INDEX_PARTICIPANT_ID, mParticipantId);
insert.bindString(INDEX_SELF_ID, mSelfId);
insert.bindLong(INDEX_SENT_TIMESTAMP, mSentTimestamp);
insert.bindLong(INDEX_RECEIVED_TIMESTAMP, mReceivedTimestamp);
insert.bindLong(INDEX_SEEN, mSeen ? 1 : 0);
insert.bindLong(INDEX_READ, mRead ? 1 : 0);
insert.bindLong(INDEX_PROTOCOL, mProtocol);
insert.bindLong(INDEX_BUGLE_STATUS, mStatus);
if (mSmsMessageUri != null) {
insert.bindString(INDEX_SMS_MESSAGE_URI, mSmsMessageUri.toString());
}
insert.bindLong(INDEX_SMS_PRIORITY, mSmsPriority);
insert.bindLong(INDEX_SMS_MESSAGE_SIZE, mSmsMessageSize);
insert.bindLong(INDEX_MMS_EXPIRY, mMmsExpiry);
if (mMmsSubject != null) {
insert.bindString(INDEX_MMS_SUBJECT, mMmsSubject);
}
if (mMmsTransactionId != null) {
insert.bindString(INDEX_MMS_TRANSACTION_ID, mMmsTransactionId);
}
if (mMmsContentLocation != null) {
insert.bindString(INDEX_MMS_CONTENT_LOCATION, mMmsContentLocation);
}
insert.bindLong(INDEX_RAW_TELEPHONY_STATUS, mRawStatus);
insert.bindLong(INDEX_RETRY_START_TIMESTAMP, mRetryStartTimestamp);
return insert;
}
public final String getMessageId() {
return mMessageId;
}
public final String getConversationId() {
return mConversationId;
}
public final String getParticipantId() {
return mParticipantId;
}
public final String getSelfId() {
return mSelfId;
}
public final long getSentTimeStamp() {
return mSentTimestamp;
}
public final long getReceivedTimeStamp() {
return mReceivedTimestamp;
}
public final String getFormattedReceivedTimeStamp() {
return Dates.getMessageTimeString(mReceivedTimestamp).toString();
}
public final int getProtocol() {
return mProtocol;
}
public final int getStatus() {
return mStatus;
}
public final Uri getSmsMessageUri() {
return mSmsMessageUri;
}
public final int getSmsPriority() {
return mSmsPriority;
}
public final long getSmsMessageSize() {
return mSmsMessageSize;
}
public final String getMmsSubject() {
return mMmsSubject;
}
public final void setMmsSubject(final String subject) {
mMmsSubject = subject;
}
public final String getMmsContentLocation() {
return mMmsContentLocation;
}
public final String getMmsTransactionId() {
return mMmsTransactionId;
}
public final boolean getMessageSeen() {
return mSeen;
}
/**
* For incoming MMS messages this returns the retrieve-status value
* For sent MMS messages this returns the response-status value
* See PduHeaders.java for possible values
* Otherwise (SMS etc) this is RAW_TELEPHONY_STATUS_UNDEFINED
*/
public final int getRawTelephonyStatus() {
return mRawStatus;
}
public final void setMessageSeen(final boolean hasSeen) {
mSeen = hasSeen;
}
public final boolean getInResendWindow(final long now) {
final long maxAgeToResend = BugleGservices.get().getLong(
BugleGservicesKeys.MESSAGE_RESEND_TIMEOUT_MS,
BugleGservicesKeys.MESSAGE_RESEND_TIMEOUT_MS_DEFAULT);
final long age = now - mRetryStartTimestamp;
return age < maxAgeToResend;
}
public final boolean getInDownloadWindow(final long now) {
final long maxAgeToRedownload = BugleGservices.get().getLong(
BugleGservicesKeys.MESSAGE_DOWNLOAD_TIMEOUT_MS,
BugleGservicesKeys.MESSAGE_DOWNLOAD_TIMEOUT_MS_DEFAULT);
final long age = now - mRetryStartTimestamp;
return age < maxAgeToRedownload;
}
static boolean getShowDownloadMessage(final int status) {
if (OsUtil.isSecondaryUser()) {
// Secondary users can't download mms's. Mms's are downloaded by bugle running as the
// primary user.
return false;
}
// Should show option for manual download iff status is manual download or failed
return (status == BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED ||
status == BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD ||
// If debug is enabled, allow to download an expired or unavailable message.
(DebugUtils.isDebugEnabled()
&& status == BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE));
}
public boolean canDownloadMessage() {
if (OsUtil.isSecondaryUser()) {
// Secondary users can't download mms's. Mms's are downloaded by bugle running as the
// primary user.
return false;
}
// Can download iff status is retrying auto/manual downloading
return (mStatus == BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD ||
mStatus == BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD);
}
public boolean canRedownloadMessage() {
if (OsUtil.isSecondaryUser()) {
// Secondary users can't download mms's. Mms's are downloaded by bugle running as the
// primary user.
return false;
}
// Can redownload iff status is manual download not started or download failed
return (mStatus == BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED ||
mStatus == BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD ||
// If debug is enabled, allow to download an expired or unavailable message.
(DebugUtils.isDebugEnabled()
&& mStatus == BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE));
}
static boolean getShowResendMessage(final int status) {
// Should show option to resend iff status is failed
return (status == BUGLE_STATUS_OUTGOING_FAILED);
}
static boolean getOneClickResendMessage(final int status, final int rawStatus) {
// Should show option to resend iff status is failed
return (status == BUGLE_STATUS_OUTGOING_FAILED
&& rawStatus == RAW_TELEPHONY_STATUS_UNDEFINED);
}
public boolean canResendMessage() {
// Manual retry allowed only from failed
return (mStatus == BUGLE_STATUS_OUTGOING_FAILED);
}
public boolean canSendMessage() {
// Sending messages must be in yet_to_send or awaiting_retry state
return (mStatus == BUGLE_STATUS_OUTGOING_YET_TO_SEND ||
mStatus == BUGLE_STATUS_OUTGOING_AWAITING_RETRY);
}
public final boolean getYetToSend() {
return (mStatus == BUGLE_STATUS_OUTGOING_YET_TO_SEND);
}
public final boolean getIsMms() {
return mProtocol == MessageData.PROTOCOL_MMS
|| mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION;
}
public static final boolean getIsMmsNotification(final int protocol) {
return (protocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION);
}
public final boolean getIsMmsNotification() {
return getIsMmsNotification(mProtocol);
}
public static final boolean getIsSms(final int protocol) {
return protocol == (MessageData.PROTOCOL_SMS);
}
public final boolean getIsSms() {
return getIsSms(mProtocol);
}
public static boolean getIsIncoming(final int status) {
return (status >= MessageData.BUGLE_STATUS_FIRST_INCOMING);
}
public boolean getIsIncoming() {
return getIsIncoming(mStatus);
}
public long getRetryStartTimestamp() {
return mRetryStartTimestamp;
}
public final String getMessageText() {
final String separator = System.getProperty("line.separator");
final StringBuilder text = new StringBuilder();
for (final MessagePartData part : mParts) {
if (!part.isAttachment() && !TextUtils.isEmpty(part.getText())) {
if (text.length() > 0) {
text.append(separator);
}
text.append(part.getText());
}
}
return text.toString();
}
/**
* Takes all captions from attachments and adds them as a prefix to the first text part or
* appends a text part
*/
public final void consolidateText() {
final String separator = System.getProperty("line.separator");
final StringBuilder captionText = new StringBuilder();
MessagePartData firstTextPart = null;
int firstTextPartIndex = -1;
for (int i = 0; i < mParts.size(); i++) {
final MessagePartData part = mParts.get(i);
if (firstTextPart == null && !part.isAttachment()) {
firstTextPart = part;
firstTextPartIndex = i;
}
if (part.isAttachment() && !TextUtils.isEmpty(part.getText())) {
if (captionText.length() > 0) {
captionText.append(separator);
}
captionText.append(part.getText());
}
}
if (captionText.length() == 0) {
// Nothing to consolidate
return;
}
if (firstTextPart == null) {
addPart(MessagePartData.createTextMessagePart(captionText.toString()));
} else {
final String partText = firstTextPart.getText();
if (partText.length() > 0) {
captionText.append(separator);
captionText.append(partText);
}
mParts.set(firstTextPartIndex,
MessagePartData.createTextMessagePart(captionText.toString()));
}
}
public final MessagePartData getFirstAttachment() {
for (final MessagePartData part : mParts) {
if (part.isAttachment()) {
return part;
}
}
return null;
}
/**
* Updates the messageId for this message.
* Can be used to reset the messageId prior to persisting (which will assign a new messageId)
* or can be called on a message that does not yet have a valid messageId to set it.
*/
public void updateMessageId(final String messageId) {
Assert.isTrue(TextUtils.isEmpty(messageId) || TextUtils.isEmpty(mMessageId));
mMessageId = messageId;
// TODO : This should probably also call updateMessageId on the message parts. We
// may also want to make messages effectively immutable once they have a valid message id.
}
public final void updateSendingMessage(final String conversationId, final Uri messageUri,
final long timestamp) {
mConversationId = conversationId;
mSmsMessageUri = messageUri;
mRead = true;
mSeen = true;
mReceivedTimestamp = timestamp;
mSentTimestamp = timestamp;
mStatus = BUGLE_STATUS_OUTGOING_YET_TO_SEND;
mRetryStartTimestamp = timestamp;
}
public final void markMessageManualResend(final long timestamp) {
// Manual send updates timestamp and transitions back to initial sending status.
mReceivedTimestamp = timestamp;
mSentTimestamp = timestamp;
mStatus = BUGLE_STATUS_OUTGOING_SENDING;
}
public final void markMessageSending(final long timestamp) {
// Initial send
mStatus = BUGLE_STATUS_OUTGOING_SENDING;
mSentTimestamp = timestamp;
}
public final void markMessageResending(final long timestamp) {
// Auto resend of message
mStatus = BUGLE_STATUS_OUTGOING_RESENDING;
mSentTimestamp = timestamp;
}
public final void markMessageSent(final long timestamp) {
mSentTimestamp = timestamp;
mStatus = BUGLE_STATUS_OUTGOING_COMPLETE;
}
public final void markMessageFailed(final long timestamp) {
mSentTimestamp = timestamp;
mStatus = BUGLE_STATUS_OUTGOING_FAILED;
}
public final void markMessageFailedEmergencyNumber(final long timestamp) {
mSentTimestamp = timestamp;
mStatus = BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER;
}
public final void markMessageNotSent(final long timestamp) {
mSentTimestamp = timestamp;
mStatus = BUGLE_STATUS_OUTGOING_AWAITING_RETRY;
}
public final void updateSizesForImageParts() {
for (final MessagePartData part : getParts()) {
part.decodeAndSaveSizeIfImage(false /* saveToStorage */);
}
}
public final void setRetryStartTimestamp(final long timestamp) {
mRetryStartTimestamp = timestamp;
}
public final void setRawTelephonyStatus(final int rawStatus) {
mRawStatus = rawStatus;
}
public boolean hasContent() {
return !TextUtils.isEmpty(mMmsSubject) ||
getFirstAttachment() != null ||
!TextUtils.isEmpty(getMessageText());
}
public final void bindSelfId(final String selfId) {
mSelfId = selfId;
}
public final void bindParticipantId(final String participantId) {
mParticipantId = participantId;
}
protected MessageData(final Parcel in) {
mMessageId = in.readString();
mConversationId = in.readString();
mParticipantId = in.readString();
mSelfId = in.readString();
mSentTimestamp = in.readLong();
mReceivedTimestamp = in.readLong();
mSeen = (in.readInt() != 0);
mRead = (in.readInt() != 0);
mProtocol = in.readInt();
mStatus = in.readInt();
final String smsMessageUri = in.readString();
mSmsMessageUri = (smsMessageUri == null ? null : Uri.parse(smsMessageUri));
mSmsPriority = in.readInt();
mSmsMessageSize = in.readLong();
mMmsExpiry = in.readLong();
mMmsSubject = in.readString();
mMmsTransactionId = in.readString();
mMmsContentLocation = in.readString();
mRawStatus = in.readInt();
mRetryStartTimestamp = in.readLong();
// Read parts
mParts = new ArrayList<MessagePartData>();
final int partCount = in.readInt();
for (int i = 0; i < partCount; i++) {
mParts.add((MessagePartData) in.readParcelable(MessagePartData.class.getClassLoader()));
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(mMessageId);
dest.writeString(mConversationId);
dest.writeString(mParticipantId);
dest.writeString(mSelfId);
dest.writeLong(mSentTimestamp);
dest.writeLong(mReceivedTimestamp);
dest.writeInt(mRead ? 1 : 0);
dest.writeInt(mSeen ? 1 : 0);
dest.writeInt(mProtocol);
dest.writeInt(mStatus);
final String smsMessageUri = (mSmsMessageUri == null) ? null : mSmsMessageUri.toString();
dest.writeString(smsMessageUri);
dest.writeInt(mSmsPriority);
dest.writeLong(mSmsMessageSize);
dest.writeLong(mMmsExpiry);
dest.writeString(mMmsSubject);
dest.writeString(mMmsTransactionId);
dest.writeString(mMmsContentLocation);
dest.writeInt(mRawStatus);
dest.writeLong(mRetryStartTimestamp);
// Write parts
dest.writeInt(mParts.size());
for (final MessagePartData messagePartData : mParts) {
dest.writeParcelable(messagePartData, flags);
}
}
public static final Parcelable.Creator<MessageData> CREATOR
= new Parcelable.Creator<MessageData>() {
@Override
public MessageData createFromParcel(final Parcel in) {
return new MessageData(in);
}
@Override
public MessageData[] newArray(final int size) {
return new MessageData[size];
}
};
@Override
public String toString() {
return toString(mMessageId, mParts);
}
public static String toString(String messageId, List<MessagePartData> parts) {
StringBuilder sb = new StringBuilder();
if (messageId != null) {
sb.append(messageId);
sb.append(": ");
}
for (MessagePartData part : parts) {
sb.append(part.toString());
sb.append(" ");
}
return sb.toString();
}
}
@@ -1,534 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteStatement;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.DatabaseHelper;
import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MediaScratchFileProvider;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.action.UpdateMessagePartSizeAction;
import com.android.messaging.datamodel.media.ImageRequest;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.Assert;
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.GifTranscoder;
import com.android.messaging.util.ImageUtils;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.SafeAsyncTask;
import com.android.messaging.util.UriUtil;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* Represents a single message part. Messages consist of one or more parts which may contain
* either text or media.
*/
public class MessagePartData implements Parcelable {
public static final int UNSPECIFIED_SIZE = MessagingContentProvider.UNSPECIFIED_SIZE;
public static final String[] ACCEPTABLE_IMAGE_TYPES =
new String[] { ContentType.IMAGE_JPEG, ContentType.IMAGE_JPG, ContentType.IMAGE_PNG,
ContentType.IMAGE_GIF };
private static final String[] sProjection = {
PartColumns._ID,
PartColumns.MESSAGE_ID,
PartColumns.TEXT,
PartColumns.CONTENT_URI,
PartColumns.CONTENT_TYPE,
PartColumns.WIDTH,
PartColumns.HEIGHT,
};
private static final int INDEX_ID = 0;
private static final int INDEX_MESSAGE_ID = 1;
private static final int INDEX_TEXT = 2;
private static final int INDEX_CONTENT_URI = 3;
private static final int INDEX_CONTENT_TYPE = 4;
private static final int INDEX_WIDTH = 5;
private static final int INDEX_HEIGHT = 6;
// This isn't part of the projection
private static final int INDEX_CONVERSATION_ID = 7;
// SQL statement to insert a "complete" message part row (columns based on projection above).
private static final String INSERT_MESSAGE_PART_SQL =
"INSERT INTO " + DatabaseHelper.PARTS_TABLE + " ( "
+ TextUtils.join(",", Arrays.copyOfRange(sProjection, 1, INDEX_CONVERSATION_ID))
+ ", " + PartColumns.CONVERSATION_ID
+ ") VALUES (?, ?, ?, ?, ?, ?, ?)";
// Used for stuff that's ignored or arbitrarily compressed.
private static final long NO_MINIMUM_SIZE = 0;
private String mPartId;
private String mMessageId;
private String mText;
private Uri mContentUri;
private String mContentType;
private int mWidth;
private int mHeight;
// This kind of part can only be attached once and with no other attachment
private boolean mSinglePartOnly;
/** Transient data: true if destroy was already called */
private boolean mDestroyed;
/**
* Create an "empty" message part
*/
protected MessagePartData() {
this(null, null, UNSPECIFIED_SIZE, UNSPECIFIED_SIZE);
}
/**
* Create a populated text message part
*/
protected MessagePartData(final String messageText) {
this(null, messageText, ContentType.TEXT_PLAIN, null, UNSPECIFIED_SIZE, UNSPECIFIED_SIZE,
false /*singlePartOnly*/);
}
/**
* Create a populated attachment message part
*/
protected MessagePartData(final String contentType, final Uri contentUri,
final int width, final int height) {
this(null, null, contentType, contentUri, width, height, false /*singlePartOnly*/);
}
/**
* Create a populated attachment message part, with additional caption text
*/
protected MessagePartData(final String messageText, final String contentType,
final Uri contentUri, final int width, final int height) {
this(null, messageText, contentType, contentUri, width, height, false /*singlePartOnly*/);
}
/**
* Create a populated attachment message part, with additional caption text, single part only
*/
protected MessagePartData(final String messageText, final String contentType,
final Uri contentUri, final int width, final int height, final boolean singlePartOnly) {
this(null, messageText, contentType, contentUri, width, height, singlePartOnly);
}
/**
* Create a populated message part
*/
private MessagePartData(final String messageId, final String messageText,
final String contentType, final Uri contentUri, final int width, final int height,
final boolean singlePartOnly) {
mMessageId = messageId;
mText = messageText;
mContentType = contentType;
mContentUri = contentUri;
mWidth = width;
mHeight = height;
mSinglePartOnly = singlePartOnly;
}
/**
* Create a "text" message part
*/
public static MessagePartData createTextMessagePart(final String messageText) {
return new MessagePartData(messageText);
}
/**
* Create a "media" message part
*/
public static MessagePartData createMediaMessagePart(final String contentType,
final Uri contentUri, final int width, final int height) {
return new MessagePartData(contentType, contentUri, width, height);
}
/**
* Create a "media" message part with caption
*/
public static MessagePartData createMediaMessagePart(final String caption,
final String contentType, final Uri contentUri, final int width, final int height) {
return new MessagePartData(null, caption, contentType, contentUri, width, height,
false /*singlePartOnly*/
);
}
/**
* Create an empty "text" message part
*/
public static MessagePartData createEmptyMessagePart() {
return new MessagePartData("");
}
/**
* Creates a new message part reading from the cursor
*/
public static MessagePartData createFromCursor(final Cursor cursor) {
final MessagePartData part = new MessagePartData();
part.bind(cursor);
return part;
}
public static String[] getProjection() {
return sProjection;
}
/**
* Updates the part id.
* Can be used to reset the partId just prior to persisting (which will assign a new partId)
* or can be called on a part that does not yet have a valid part id to set it.
*/
public void updatePartId(final String partId) {
Assert.isTrue(TextUtils.isEmpty(partId) || TextUtils.isEmpty(mPartId));
mPartId = partId;
}
/**
* Updates the messageId for the part.
* Can be used to reset the messageId prior to persisting (which will assign a new messageId)
* or can be called on a part that does not yet have a valid messageId to set it.
*/
public void updateMessageId(final String messageId) {
Assert.isTrue(TextUtils.isEmpty(messageId) || TextUtils.isEmpty(mMessageId));
mMessageId = messageId;
}
protected static String getMessageId(final Cursor cursor) {
return cursor.getString(INDEX_MESSAGE_ID);
}
protected void bind(final Cursor cursor) {
mPartId = cursor.getString(INDEX_ID);
mMessageId = cursor.getString(INDEX_MESSAGE_ID);
mText = cursor.getString(INDEX_TEXT);
mContentUri = UriUtil.uriFromString(cursor.getString(INDEX_CONTENT_URI));
mContentType = cursor.getString(INDEX_CONTENT_TYPE);
mWidth = cursor.getInt(INDEX_WIDTH);
mHeight = cursor.getInt(INDEX_HEIGHT);
}
public final void populate(final ContentValues values) {
// Must have a valid messageId on a part
Assert.isTrue(!TextUtils.isEmpty(mMessageId));
values.put(PartColumns.MESSAGE_ID, mMessageId);
values.put(PartColumns.TEXT, mText);
values.put(PartColumns.CONTENT_URI, UriUtil.stringFromUri(mContentUri));
values.put(PartColumns.CONTENT_TYPE, mContentType);
if (mWidth != UNSPECIFIED_SIZE) {
values.put(PartColumns.WIDTH, mWidth);
}
if (mHeight != UNSPECIFIED_SIZE) {
values.put(PartColumns.HEIGHT, mHeight);
}
}
/**
* Note this is not thread safe so callers need to make sure they own the wrapper + statements
* while they call this and use the returned value.
*/
public SQLiteStatement getInsertStatement(final DatabaseWrapper db,
final String conversationId) {
final SQLiteStatement insert = db.getStatementInTransaction(
DatabaseWrapper.INDEX_INSERT_MESSAGE_PART, INSERT_MESSAGE_PART_SQL);
insert.clearBindings();
insert.bindString(INDEX_MESSAGE_ID, mMessageId);
if (mText != null) {
insert.bindString(INDEX_TEXT, mText);
}
if (mContentUri != null) {
insert.bindString(INDEX_CONTENT_URI, mContentUri.toString());
}
if (mContentType != null) {
insert.bindString(INDEX_CONTENT_TYPE, mContentType);
}
insert.bindLong(INDEX_WIDTH, mWidth);
insert.bindLong(INDEX_HEIGHT, mHeight);
insert.bindString(INDEX_CONVERSATION_ID, conversationId);
return insert;
}
public final String getPartId() {
return mPartId;
}
public final String getMessageId() {
return mMessageId;
}
public final String getText() {
return mText;
}
public final Uri getContentUri() {
return mContentUri;
}
public boolean isAttachment() {
return mContentUri != null;
}
public boolean isText() {
return ContentType.isTextType(mContentType);
}
public boolean isImage() {
return ContentType.isImageType(mContentType);
}
public boolean isMedia() {
return ContentType.isMediaType(mContentType);
}
public boolean isVCard() {
return ContentType.isVCardType(mContentType);
}
public boolean isAudio() {
return ContentType.isAudioType(mContentType);
}
public boolean isVideo() {
return ContentType.isVideoType(mContentType);
}
public final String getContentType() {
return mContentType;
}
public final int getWidth() {
return mWidth;
}
public final int getHeight() {
return mHeight;
}
/**
*
* @return true if this part can only exist by itself, with no other attachments
*/
public boolean getSinglePartOnly() {
return mSinglePartOnly;
}
@Override
public int describeContents() {
return 0;
}
protected MessagePartData(final Parcel in) {
mMessageId = in.readString();
mText = in.readString();
mContentUri = UriUtil.uriFromString(in.readString());
mContentType = in.readString();
mWidth = in.readInt();
mHeight = in.readInt();
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
Assert.isTrue(!mDestroyed);
dest.writeString(mMessageId);
dest.writeString(mText);
dest.writeString(UriUtil.stringFromUri(mContentUri));
dest.writeString(mContentType);
dest.writeInt(mWidth);
dest.writeInt(mHeight);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof MessagePartData)) {
return false;
}
MessagePartData lhs = (MessagePartData) o;
return mWidth == lhs.mWidth && mHeight == lhs.mHeight &&
TextUtils.equals(mMessageId, lhs.mMessageId) &&
TextUtils.equals(mText, lhs.mText) &&
TextUtils.equals(mContentType, lhs.mContentType) &&
(mContentUri == null ? lhs.mContentUri == null
: mContentUri.equals(lhs.mContentUri));
}
@Override public int hashCode() {
int result = 17;
result = 31 * result + mWidth;
result = 31 * result + mHeight;
result = 31 * result + (mMessageId == null ? 0 : mMessageId.hashCode());
result = 31 * result + (mText == null ? 0 : mText.hashCode());
result = 31 * result + (mContentType == null ? 0 : mContentType.hashCode());
result = 31 * result + (mContentUri == null ? 0 : mContentUri.hashCode());
return result;
}
public static final Parcelable.Creator<MessagePartData> CREATOR
= new Parcelable.Creator<MessagePartData>() {
@Override
public MessagePartData createFromParcel(final Parcel in) {
return new MessagePartData(in);
}
@Override
public MessagePartData[] newArray(final int size) {
return new MessagePartData[size];
}
};
protected Uri shouldDestroy() {
// We should never double-destroy.
Assert.isTrue(!mDestroyed);
mDestroyed = true;
Uri contentUri = mContentUri;
mContentUri = null;
mContentType = null;
// Only destroy the image if it's staged in our scratch space.
if (!MediaScratchFileProvider.isMediaScratchSpaceUri(contentUri)) {
contentUri = null;
}
return contentUri;
}
/**
* If application owns content associated with this part delete it (on background thread)
*/
public void destroyAsync() {
final Uri contentUri = shouldDestroy();
if (contentUri != null) {
SafeAsyncTask.executeOnThreadPool(new Runnable() {
@Override
public void run() {
Factory.get().getApplicationContext().getContentResolver().delete(
contentUri, null, null);
}
});
}
}
/**
* If application owns content associated with this part delete it
*/
public void destroySync() {
final Uri contentUri = shouldDestroy();
if (contentUri != null) {
Factory.get().getApplicationContext().getContentResolver().delete(
contentUri, null, null);
}
}
/**
* If this is an image part, decode the image header and potentially save the size to the db.
*/
public void decodeAndSaveSizeIfImage(final boolean saveToStorage) {
if (isImage()) {
final Rect imageSize = ImageUtils.decodeImageBounds(
Factory.get().getApplicationContext(), mContentUri);
if (imageSize.width() != ImageRequest.UNSPECIFIED_SIZE &&
imageSize.height() != ImageRequest.UNSPECIFIED_SIZE) {
mWidth = imageSize.width();
mHeight = imageSize.height();
if (saveToStorage) {
UpdateMessagePartSizeAction.updateSize(mPartId, mWidth, mHeight);
}
}
}
}
/**
* Computes the minimum size that this MessagePartData could be compressed/downsampled/encoded
* before sending to meet the maximum message size imposed by the carriers. This is used to
* determine right before sending a message whether a message could possibly be sent. If not
* then the user is given a chance to unselect some/all of the attachments.
*
* TODO: computing the minimum size could be expensive. Should we cache the
* computed value in db to be retrieved later?
*
* @return the carrier-independent minimum size, in bytes.
*/
@DoesNotRunOnMainThread
public long getMinimumSizeInBytesForSending() {
Assert.isNotMainThread();
if (!isAttachment()) {
// No limit is imposed on non-attachment part (i.e. plain text), so treat it as zero.
return NO_MINIMUM_SIZE;
} else if (isImage()) {
// GIFs are resized by the native transcoder (exposed by GifTranscoder).
if (ImageUtils.isGif(mContentType, mContentUri)) {
final long originalImageSize = UriUtil.getContentSize(mContentUri);
// Wish we could save the size here, but we don't have a part id yet
decodeAndSaveSizeIfImage(false /* saveToStorage */);
return GifTranscoder.canBeTranscoded(mWidth, mHeight) ?
GifTranscoder.estimateFileSizeAfterTranscode(originalImageSize)
: originalImageSize;
}
// Other images should be arbitrarily resized by ImageResizer before sending.
return MmsUtils.MIN_IMAGE_BYTE_SIZE;
} else if (isAudio()) {
// Audios are already recorded with the lowest sampling settings (AMR_NB), so just
// return the file size as the minimum size.
return UriUtil.getContentSize(mContentUri);
} else if (isVideo()) {
final int mediaDurationMs = UriUtil.getMediaDurationMs(mContentUri);
return MmsUtils.MIN_VIDEO_BYTES_PER_SECOND * mediaDurationMs
/ TimeUnit.SECONDS.toMillis(1);
} else if (isVCard()) {
// We can't compress vCards.
return UriUtil.getContentSize(mContentUri);
} else {
// This is some unknown media type that we don't know how to handle. Log an error
// and try sending it anyway.
LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "Unknown attachment type " + getContentType());
return NO_MINIMUM_SIZE;
}
}
@Override
public String toString() {
if (isText()) {
return LogUtil.sanitizePII(getText());
} else {
return getContentType() + " (" + getContentUri() + ")";
}
}
/**
*
* @return true if this part can only exist by itself, with no other attachments
*/
public boolean isSinglePartOnly() {
return mSinglePartOnly;
}
public void setSinglePartOnly(final boolean isSinglePartOnly) {
mSinglePartOnly = isSinglePartOnly;
}
}
@@ -1,569 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.content.ContentValues;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Color;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v7.mms.MmsManager;
import android.telephony.SubscriptionInfo;
import android.text.TextUtils;
import com.android.ex.chips.RecipientEntry;
import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.datamodel.DatabaseHelper;
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.sms.MmsSmsUtils;
import com.android.messaging.util.Assert;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.TextUtil;
/**
* A class that encapsulates all of the data for a specific participant in a conversation.
*/
public class ParticipantData implements Parcelable {
// We always use -1 as default/invalid sub id although system may give us anything negative
public static final int DEFAULT_SELF_SUB_ID = MmsManager.DEFAULT_SUB_ID;
// This needs to be something apart from valid or DEFAULT_SELF_SUB_ID
public static final int OTHER_THAN_SELF_SUB_ID = DEFAULT_SELF_SUB_ID - 1;
// Active slot ids are non-negative. Using -1 to designate to inactive self participants.
public static final int INVALID_SLOT_ID = -1;
// TODO: may make sense to move this to common place?
public static final long PARTICIPANT_CONTACT_ID_NOT_RESOLVED = -1;
public static final long PARTICIPANT_CONTACT_ID_NOT_FOUND = -2;
public static class ParticipantsQuery {
public static final String[] PROJECTION = new String[] {
ParticipantColumns._ID,
ParticipantColumns.SUB_ID,
ParticipantColumns.SIM_SLOT_ID,
ParticipantColumns.NORMALIZED_DESTINATION,
ParticipantColumns.SEND_DESTINATION,
ParticipantColumns.DISPLAY_DESTINATION,
ParticipantColumns.FULL_NAME,
ParticipantColumns.FIRST_NAME,
ParticipantColumns.PROFILE_PHOTO_URI,
ParticipantColumns.CONTACT_ID,
ParticipantColumns.LOOKUP_KEY,
ParticipantColumns.BLOCKED,
ParticipantColumns.SUBSCRIPTION_COLOR,
ParticipantColumns.SUBSCRIPTION_NAME,
ParticipantColumns.CONTACT_DESTINATION,
};
public static final int INDEX_ID = 0;
public static final int INDEX_SUB_ID = 1;
public static final int INDEX_SIM_SLOT_ID = 2;
public static final int INDEX_NORMALIZED_DESTINATION = 3;
public static final int INDEX_SEND_DESTINATION = 4;
public static final int INDEX_DISPLAY_DESTINATION = 5;
public static final int INDEX_FULL_NAME = 6;
public static final int INDEX_FIRST_NAME = 7;
public static final int INDEX_PROFILE_PHOTO_URI = 8;
public static final int INDEX_CONTACT_ID = 9;
public static final int INDEX_LOOKUP_KEY = 10;
public static final int INDEX_BLOCKED = 11;
public static final int INDEX_SUBSCRIPTION_COLOR = 12;
public static final int INDEX_SUBSCRIPTION_NAME = 13;
public static final int INDEX_CONTACT_DESTINATION = 14;
}
/**
* @return The MMS unknown sender participant entity
*/
public static String getUnknownSenderDestination() {
// This is a hard coded string rather than a localized one because we don't want it to
// change when you change locale.
return "\u02BCUNKNOWN_SENDER!\u02BC";
}
private String mParticipantId;
private int mSubId;
private int mSlotId;
private String mNormalizedDestination;
private String mSendDestination;
private String mDisplayDestination;
private String mContactDestination;
private String mFullName;
private String mFirstName;
private String mProfilePhotoUri;
private long mContactId;
private String mLookupKey;
private int mSubscriptionColor;
private String mSubscriptionName;
private boolean mIsEmailAddress;
private boolean mBlocked;
// Don't call constructor directly
private ParticipantData() {
}
public static ParticipantData getFromCursor(final Cursor cursor) {
final ParticipantData pd = new ParticipantData();
pd.mParticipantId = cursor.getString(ParticipantsQuery.INDEX_ID);
pd.mSubId = cursor.getInt(ParticipantsQuery.INDEX_SUB_ID);
pd.mSlotId = cursor.getInt(ParticipantsQuery.INDEX_SIM_SLOT_ID);
pd.mNormalizedDestination = cursor.getString(
ParticipantsQuery.INDEX_NORMALIZED_DESTINATION);
pd.mSendDestination = cursor.getString(ParticipantsQuery.INDEX_SEND_DESTINATION);
pd.mDisplayDestination = cursor.getString(ParticipantsQuery.INDEX_DISPLAY_DESTINATION);
pd.mContactDestination = cursor.getString(ParticipantsQuery.INDEX_CONTACT_DESTINATION);
pd.mFullName = cursor.getString(ParticipantsQuery.INDEX_FULL_NAME);
pd.mFirstName = cursor.getString(ParticipantsQuery.INDEX_FIRST_NAME);
pd.mProfilePhotoUri = cursor.getString(ParticipantsQuery.INDEX_PROFILE_PHOTO_URI);
pd.mContactId = cursor.getLong(ParticipantsQuery.INDEX_CONTACT_ID);
pd.mLookupKey = cursor.getString(ParticipantsQuery.INDEX_LOOKUP_KEY);
pd.mIsEmailAddress = MmsSmsUtils.isEmailAddress(pd.mSendDestination);
pd.mBlocked = cursor.getInt(ParticipantsQuery.INDEX_BLOCKED) != 0;
pd.mSubscriptionColor = cursor.getInt(ParticipantsQuery.INDEX_SUBSCRIPTION_COLOR);
pd.mSubscriptionName = cursor.getString(ParticipantsQuery.INDEX_SUBSCRIPTION_NAME);
pd.maybeSetupUnknownSender();
return pd;
}
public static ParticipantData getFromId(final DatabaseWrapper dbWrapper,
final String participantId) {
Cursor cursor = null;
try {
cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
ParticipantsQuery.PROJECTION,
ParticipantColumns._ID + " =?",
new String[] { participantId }, null, null, null);
if (cursor.moveToFirst()) {
return ParticipantData.getFromCursor(cursor);
} else {
return null;
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
public static ParticipantData getFromRecipientEntry(final RecipientEntry recipientEntry) {
final ParticipantData pd = new ParticipantData();
pd.mParticipantId = null;
pd.mSubId = OTHER_THAN_SELF_SUB_ID;
pd.mSlotId = INVALID_SLOT_ID;
pd.mSendDestination = TextUtil.replaceUnicodeDigits(recipientEntry.getDestination());
pd.mIsEmailAddress = MmsSmsUtils.isEmailAddress(pd.mSendDestination);
pd.mNormalizedDestination = pd.mIsEmailAddress ?
pd.mSendDestination :
PhoneUtils.getDefault().getCanonicalBySystemLocale(pd.mSendDestination);
pd.mDisplayDestination = pd.mIsEmailAddress ?
pd.mNormalizedDestination :
PhoneUtils.getDefault().formatForDisplay(pd.mNormalizedDestination);
pd.mFullName = recipientEntry.getDisplayName();
pd.mFirstName = null;
pd.mProfilePhotoUri = (recipientEntry.getPhotoThumbnailUri() == null) ? null :
recipientEntry.getPhotoThumbnailUri().toString();
pd.mContactId = recipientEntry.getContactId();
if (pd.mContactId < 0) {
// ParticipantData only supports real contact ids (>=0) based on faith that the contacts
// provider will continue to only use non-negative ids. The UI uses contactId < 0 for
// special handling. We convert those to 'not resolved'
pd.mContactId = PARTICIPANT_CONTACT_ID_NOT_RESOLVED;
}
pd.mLookupKey = recipientEntry.getLookupKey();
pd.mBlocked = false;
pd.mSubscriptionColor = Color.TRANSPARENT;
pd.mSubscriptionName = null;
pd.maybeSetupUnknownSender();
return pd;
}
// Shared code for getFromRawPhoneBySystemLocale and getFromRawPhoneBySimLocale
private static ParticipantData getFromRawPhone(final String phoneNumber) {
Assert.isTrue(phoneNumber != null);
final ParticipantData pd = new ParticipantData();
pd.mParticipantId = null;
pd.mSubId = OTHER_THAN_SELF_SUB_ID;
pd.mSlotId = INVALID_SLOT_ID;
pd.mSendDestination = TextUtil.replaceUnicodeDigits(phoneNumber);
pd.mIsEmailAddress = MmsSmsUtils.isEmailAddress(pd.mSendDestination);
pd.mFullName = null;
pd.mFirstName = null;
pd.mProfilePhotoUri = null;
pd.mContactId = PARTICIPANT_CONTACT_ID_NOT_RESOLVED;
pd.mLookupKey = null;
pd.mBlocked = false;
pd.mSubscriptionColor = Color.TRANSPARENT;
pd.mSubscriptionName = null;
return pd;
}
/**
* Get an instance from a raw phone number and using system locale to normalize it.
*
* Use this when creating a participant that is for displaying UI and not associated
* with a specific SIM. For example, when creating a conversation using user entered
* phone number.
*
* @param phoneNumber The raw phone number
* @return instance
*/
public static ParticipantData getFromRawPhoneBySystemLocale(final String phoneNumber) {
final ParticipantData pd = getFromRawPhone(phoneNumber);
pd.mNormalizedDestination = pd.mIsEmailAddress ?
pd.mSendDestination :
PhoneUtils.getDefault().getCanonicalBySystemLocale(pd.mSendDestination);
pd.mDisplayDestination = pd.mIsEmailAddress ?
pd.mNormalizedDestination :
PhoneUtils.getDefault().formatForDisplay(pd.mNormalizedDestination);
pd.maybeSetupUnknownSender();
return pd;
}
/**
* Get an instance from a raw phone number and using SIM or system locale to normalize it.
*
* Use this when creating a participant that is associated with a specific SIM. For example,
* the sender of a received message or the recipient of a sending message that is already
* targeted at a specific SIM.
*
* @param phoneNumber The raw phone number
* @return instance
*/
public static ParticipantData getFromRawPhoneBySimLocale(
final String phoneNumber, final int subId) {
final ParticipantData pd = getFromRawPhone(phoneNumber);
pd.mNormalizedDestination = pd.mIsEmailAddress ?
pd.mSendDestination :
PhoneUtils.get(subId).getCanonicalBySimLocale(pd.mSendDestination);
pd.mDisplayDestination = pd.mIsEmailAddress ?
pd.mNormalizedDestination :
PhoneUtils.getDefault().formatForDisplay(pd.mNormalizedDestination);
pd.maybeSetupUnknownSender();
return pd;
}
public static ParticipantData getSelfParticipant(final int subId) {
Assert.isTrue(subId != OTHER_THAN_SELF_SUB_ID);
final ParticipantData pd = new ParticipantData();
pd.mParticipantId = null;
pd.mSubId = subId;
pd.mSlotId = INVALID_SLOT_ID;
pd.mIsEmailAddress = false;
pd.mSendDestination = null;
pd.mNormalizedDestination = null;
pd.mDisplayDestination = null;
pd.mFullName = null;
pd.mFirstName = null;
pd.mProfilePhotoUri = null;
pd.mContactId = PARTICIPANT_CONTACT_ID_NOT_RESOLVED;
pd.mLookupKey = null;
pd.mBlocked = false;
pd.mSubscriptionColor = Color.TRANSPARENT;
pd.mSubscriptionName = null;
return pd;
}
private void maybeSetupUnknownSender() {
if (isUnknownSender()) {
// Because your locale may change, we setup the display string for the unknown sender
// on the fly rather than relying on the version in the database.
final Resources resources = Factory.get().getApplicationContext().getResources();
mDisplayDestination = resources.getString(R.string.unknown_sender);
mFullName = mDisplayDestination;
}
}
public String getNormalizedDestination() {
return mNormalizedDestination;
}
public String getSendDestination() {
return mSendDestination;
}
public String getDisplayDestination() {
return mDisplayDestination;
}
public String getContactDestination() {
return mContactDestination;
}
public String getFullName() {
return mFullName;
}
public String getFirstName() {
return mFirstName;
}
public String getDisplayName(final boolean preferFullName) {
if (preferFullName) {
// Prefer full name over first name
if (!TextUtils.isEmpty(mFullName)) {
return mFullName;
}
if (!TextUtils.isEmpty(mFirstName)) {
return mFirstName;
}
} else {
// Prefer first name over full name
if (!TextUtils.isEmpty(mFirstName)) {
return mFirstName;
}
if (!TextUtils.isEmpty(mFullName)) {
return mFullName;
}
}
// Fallback to the display destination
if (!TextUtils.isEmpty(mDisplayDestination)) {
return mDisplayDestination;
}
return Factory.get().getApplicationContext().getResources().getString(
R.string.unknown_sender);
}
public String getProfilePhotoUri() {
return mProfilePhotoUri;
}
public long getContactId() {
return mContactId;
}
public String getLookupKey() {
return mLookupKey;
}
public boolean updatePhoneNumberForSelfIfChanged() {
final String phoneNumber =
PhoneUtils.get(mSubId).getCanonicalForSelf(true/*allowOverride*/);
boolean changed = false;
if (isSelf() && !TextUtils.equals(phoneNumber, mNormalizedDestination)) {
mNormalizedDestination = phoneNumber;
mSendDestination = phoneNumber;
mDisplayDestination = mIsEmailAddress ?
phoneNumber :
PhoneUtils.getDefault().formatForDisplay(phoneNumber);
changed = true;
}
return changed;
}
public boolean updateSubscriptionInfoForSelfIfChanged(final SubscriptionInfo subscriptionInfo) {
boolean changed = false;
if (isSelf()) {
if (subscriptionInfo == null) {
// The subscription is inactive. Check if the participant is still active.
if (isActiveSubscription()) {
mSlotId = INVALID_SLOT_ID;
mSubscriptionColor = Color.TRANSPARENT;
mSubscriptionName = "";
changed = true;
}
} else {
final int slotId = subscriptionInfo.getSimSlotIndex();
final int color = subscriptionInfo.getIconTint();
final CharSequence name = subscriptionInfo.getDisplayName();
if (mSlotId != slotId || mSubscriptionColor != color || mSubscriptionName != name) {
mSlotId = slotId;
mSubscriptionColor = color;
mSubscriptionName = name.toString();
changed = true;
}
}
}
return changed;
}
public void setFullName(final String fullName) {
mFullName = fullName;
}
public void setFirstName(final String firstName) {
mFirstName = firstName;
}
public void setProfilePhotoUri(final String profilePhotoUri) {
mProfilePhotoUri = profilePhotoUri;
}
public void setContactId(final long contactId) {
mContactId = contactId;
}
public void setLookupKey(final String lookupKey) {
mLookupKey = lookupKey;
}
public void setSendDestination(final String destination) {
mSendDestination = destination;
}
public void setContactDestination(final String destination) {
mContactDestination = destination;
}
public int getSubId() {
return mSubId;
}
/**
* @return whether this sub is active. Note that {@link ParticipantData#DEFAULT_SELF_SUB_ID} is
* is considered as active if there is any active SIM.
*/
public boolean isActiveSubscription() {
return mSlotId != INVALID_SLOT_ID;
}
public boolean isDefaultSelf() {
return mSubId == ParticipantData.DEFAULT_SELF_SUB_ID;
}
public int getSlotId() {
return mSlotId;
}
/**
* Slot IDs in the subscription manager is zero-based, but we want to show it
* as 1-based in UI.
*/
public int getDisplaySlotId() {
return getSlotId() + 1;
}
public int getSubscriptionColor() {
Assert.isTrue(isActiveSubscription());
// Force the alpha channel to 0xff to ensure the returned color is solid.
return mSubscriptionColor | 0xff000000;
}
public String getSubscriptionName() {
Assert.isTrue(isActiveSubscription());
return mSubscriptionName;
}
public String getId() {
return mParticipantId;
}
public boolean isSelf() {
return (mSubId != OTHER_THAN_SELF_SUB_ID);
}
public boolean isEmail() {
return mIsEmailAddress;
}
public boolean isContactIdResolved() {
return (mContactId != PARTICIPANT_CONTACT_ID_NOT_RESOLVED);
}
public boolean isBlocked() {
return mBlocked;
}
public boolean isUnknownSender() {
final String unknownSender = ParticipantData.getUnknownSenderDestination();
return (TextUtils.equals(mSendDestination, unknownSender));
}
public ContentValues toContentValues() {
final ContentValues values = new ContentValues();
values.put(ParticipantColumns.SUB_ID, mSubId);
values.put(ParticipantColumns.SIM_SLOT_ID, mSlotId);
values.put(DatabaseHelper.ParticipantColumns.SEND_DESTINATION, mSendDestination);
if (!isUnknownSender()) {
values.put(DatabaseHelper.ParticipantColumns.DISPLAY_DESTINATION, mDisplayDestination);
values.put(DatabaseHelper.ParticipantColumns.NORMALIZED_DESTINATION,
mNormalizedDestination);
values.put(ParticipantColumns.FULL_NAME, mFullName);
values.put(ParticipantColumns.FIRST_NAME, mFirstName);
}
values.put(ParticipantColumns.PROFILE_PHOTO_URI, mProfilePhotoUri);
values.put(ParticipantColumns.CONTACT_ID, mContactId);
values.put(ParticipantColumns.LOOKUP_KEY, mLookupKey);
values.put(ParticipantColumns.BLOCKED, mBlocked);
values.put(ParticipantColumns.SUBSCRIPTION_COLOR, mSubscriptionColor);
values.put(ParticipantColumns.SUBSCRIPTION_NAME, mSubscriptionName);
return values;
}
public ParticipantData(final Parcel in) {
mParticipantId = in.readString();
mSubId = in.readInt();
mSlotId = in.readInt();
mNormalizedDestination = in.readString();
mSendDestination = in.readString();
mDisplayDestination = in.readString();
mFullName = in.readString();
mFirstName = in.readString();
mProfilePhotoUri = in.readString();
mContactId = in.readLong();
mLookupKey = in.readString();
mIsEmailAddress = in.readInt() != 0;
mBlocked = in.readInt() != 0;
mSubscriptionColor = in.readInt();
mSubscriptionName = in.readString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(mParticipantId);
dest.writeInt(mSubId);
dest.writeInt(mSlotId);
dest.writeString(mNormalizedDestination);
dest.writeString(mSendDestination);
dest.writeString(mDisplayDestination);
dest.writeString(mFullName);
dest.writeString(mFirstName);
dest.writeString(mProfilePhotoUri);
dest.writeLong(mContactId);
dest.writeString(mLookupKey);
dest.writeInt(mIsEmailAddress ? 1 : 0);
dest.writeInt(mBlocked ? 1 : 0);
dest.writeInt(mSubscriptionColor);
dest.writeString(mSubscriptionName);
}
public static final Parcelable.Creator<ParticipantData> CREATOR
= new Parcelable.Creator<ParticipantData>() {
@Override
public ParticipantData createFromParcel(final Parcel in) {
return new ParticipantData(in);
}
@Override
public ParticipantData[] newArray(final int size) {
return new ParticipantData[size];
}
};
}
@@ -1,95 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import com.android.messaging.datamodel.action.BugleActionToasts;
import com.android.messaging.datamodel.action.UpdateDestinationBlockedAction;
import com.android.messaging.util.AvatarUriUtil;
/**
* Helps visualize a ParticipantData in a PersonItemView
*/
public class ParticipantListItemData extends PersonItemData {
private final Uri mAvatarUri;
private final String mDisplayName;
private final String mDetails;
private final long mContactId;
private final String mLookupKey;
private final String mNormalizedDestination;
/**
* Constructor. Takes necessary info from the incoming ParticipantData.
*/
public ParticipantListItemData(final ParticipantData participant) {
mAvatarUri = AvatarUriUtil.createAvatarUri(participant);
mContactId = participant.getContactId();
mLookupKey = participant.getLookupKey();
mNormalizedDestination = participant.getNormalizedDestination();
if (TextUtils.isEmpty(participant.getFullName())) {
mDisplayName = participant.getSendDestination();
mDetails = null;
} else {
mDisplayName = participant.getFullName();
mDetails = (participant.isUnknownSender()) ? null : participant.getSendDestination();
}
}
@Override
public Uri getAvatarUri() {
return mAvatarUri;
}
@Override
public String getDisplayName() {
return mDisplayName;
}
@Override
public String getDetails() {
return mDetails;
}
@Override
public Intent getClickIntent() {
return null;
}
@Override
public long getContactId() {
return mContactId;
}
@Override
public String getLookupKey() {
return mLookupKey;
}
@Override
public String getNormalizedDestination() {
return mNormalizedDestination;
}
public void unblock(final Context context) {
UpdateDestinationBlockedAction.updateDestinationBlocked(
mNormalizedDestination, false, null,
BugleActionToasts.makeUpdateDestinationBlockedActionListener(context));
}
}
@@ -1,176 +0,0 @@
/*
* 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.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.SafeAsyncTask;
import com.android.messaging.util.UriUtil;
/**
* Represents a "pending" message part that acts as a placeholder for the actual attachment being
* loaded. It handles the task to load and persist the attachment from a Uri to local scratch
* folder. This item is not persisted to the database.
*/
public class PendingAttachmentData extends MessagePartData {
/** The pending state. This is the initial state where we haven't started loading yet */
public static final int STATE_PENDING = 0;
/** The state for when we are currently loading the attachment to the scratch space */
public static final int STATE_LOADING = 1;
/** The attachment has been successfully loaded and no longer pending */
public static final int STATE_LOADED = 2;
/** The attachment failed to load */
public static final int STATE_FAILED = 3;
private static final int LOAD_MEDIA_TIME_LIMIT_MILLIS = 60 * 1000; // 60s
/** The current state of the pending attachment. Refer to the STATE_* states above */
private int mCurrentState;
/**
* Create a new instance of PendingAttachmentData with an output Uri.
* @param sourceUri the source Uri of the attachment. The Uri maybe temporary or remote,
* so we need to persist it to local storage.
*/
protected PendingAttachmentData(final String caption, final String contentType,
@NonNull final Uri sourceUri, final int width, final int height,
final boolean onlySingleAttachment) {
super(caption, contentType, sourceUri, width, height, onlySingleAttachment);
mCurrentState = STATE_PENDING;
}
/**
* Creates a pending attachment data that is able to load from the given source uri and
* persist the media resource locally in the scratch folder.
*/
public static PendingAttachmentData createPendingAttachmentData(final String contentType,
final Uri sourceUri) {
return createPendingAttachmentData(null, contentType, sourceUri, UNSPECIFIED_SIZE,
UNSPECIFIED_SIZE);
}
public static PendingAttachmentData createPendingAttachmentData(final String caption,
final String contentType, final Uri sourceUri, final int width, final int height) {
Assert.isTrue(ContentType.isMediaType(contentType));
return new PendingAttachmentData(caption, contentType, sourceUri, width, height,
false /*onlySingleAttachment*/);
}
public static PendingAttachmentData createPendingAttachmentData(final String caption,
final String contentType, final Uri sourceUri, final int width, final int height,
final boolean onlySingleAttachment) {
Assert.isTrue(ContentType.isMediaType(contentType));
return new PendingAttachmentData(caption, contentType, sourceUri, width, height,
onlySingleAttachment);
}
public int getCurrentState() {
return mCurrentState;
}
public void loadAttachmentForDraft(final DraftMessageData draftMessageData,
final String bindingId) {
if (mCurrentState != STATE_PENDING) {
return;
}
mCurrentState = STATE_LOADING;
// Kick off a SafeAsyncTask to load the content of the media and persist it locally.
// Note: we need to persist the media locally even if it's not remote, because we
// want to be able to resend the media in case the message failed to send.
new SafeAsyncTask<Void, Void, MessagePartData>(LOAD_MEDIA_TIME_LIMIT_MILLIS,
true /* cancelExecutionOnTimeout */) {
@Override
protected MessagePartData doInBackgroundTimed(final Void... params) {
final Uri contentUri = getContentUri();
final Uri persistedUri = UriUtil.persistContentToScratchSpace(contentUri);
if (persistedUri != null) {
return MessagePartData.createMediaMessagePart(
getText(),
getContentType(),
persistedUri,
getWidth(),
getHeight());
}
return null;
}
@Override
protected void onCancelled() {
LogUtil.w(LogUtil.BUGLE_TAG, "Timeout while retrieving media");
mCurrentState = STATE_FAILED;
if (draftMessageData.isBound(bindingId)) {
draftMessageData.removePendingAttachment(PendingAttachmentData.this);
}
}
@Override
protected void onPostExecute(final MessagePartData attachment) {
if (attachment != null) {
mCurrentState = STATE_LOADED;
if (draftMessageData.isBound(bindingId)) {
draftMessageData.updatePendingAttachment(attachment,
PendingAttachmentData.this);
} else {
// The draft message data is no longer bound, drop the loaded attachment.
attachment.destroyAsync();
}
} else {
// Media load failed. We already logged in doInBackground() so don't need to
// do that again.
mCurrentState = STATE_FAILED;
if (draftMessageData.isBound(bindingId)) {
draftMessageData.onPendingAttachmentLoadFailed(PendingAttachmentData.this);
draftMessageData.removePendingAttachment(PendingAttachmentData.this);
}
}
}
}.executeOnThreadPool();
}
protected PendingAttachmentData(final Parcel in) {
super(in);
mCurrentState = in.readInt();
}
@Override
public void writeToParcel(final Parcel out, final int flags) {
super.writeToParcel(out, flags);
out.writeInt(mCurrentState);
}
public static final Parcelable.Creator<PendingAttachmentData> CREATOR
= new Parcelable.Creator<PendingAttachmentData>() {
@Override
public PendingAttachmentData createFromParcel(final Parcel in) {
return new PendingAttachmentData(in);
}
@Override
public PendingAttachmentData[] newArray(final int size) {
return new PendingAttachmentData[size];
}
};
}
@@ -1,210 +0,0 @@
/*
* 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.net.Uri;
import android.os.Bundle;
import com.android.messaging.datamodel.BoundCursorLoader;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.action.BugleActionToasts;
import com.android.messaging.datamodel.action.UpdateConversationOptionsAction;
import com.android.messaging.datamodel.action.UpdateDestinationBlockedAction;
import com.android.messaging.datamodel.binding.BindableData;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import java.util.List;
/**
* Services data needs for PeopleAndOptionsFragment.
*/
public class PeopleAndOptionsData extends BindableData implements
LoaderManager.LoaderCallbacks<Cursor> {
public interface PeopleAndOptionsDataListener {
void onOptionsCursorUpdated(PeopleAndOptionsData data, Cursor cursor);
void onParticipantsListLoaded(PeopleAndOptionsData data,
List<ParticipantData> participants);
}
private static final String BINDING_ID = "bindingId";
private final Context mContext;
private final String mConversationId;
private final ConversationParticipantsData mParticipantData;
private LoaderManager mLoaderManager;
private PeopleAndOptionsDataListener mListener;
public PeopleAndOptionsData(final String conversationId, final Context context,
final PeopleAndOptionsDataListener listener) {
mListener = listener;
mContext = context;
mConversationId = conversationId;
mParticipantData = new ConversationParticipantsData();
}
private static final int CONVERSATION_OPTIONS_LOADER = 1;
private static final int PARTICIPANT_LOADER = 2;
@Override
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
final String bindingId = args.getString(BINDING_ID);
// Check if data still bound to the requesting ui element
if (isBound(bindingId)) {
switch (id) {
case CONVERSATION_OPTIONS_LOADER: {
final Uri uri =
MessagingContentProvider.buildConversationMetadataUri(mConversationId);
return new BoundCursorLoader(bindingId, mContext, uri,
PeopleOptionsItemData.PROJECTION, null, null, null);
}
case PARTICIPANT_LOADER: {
final Uri uri =
MessagingContentProvider
.buildConversationParticipantsUri(mConversationId);
return new BoundCursorLoader(bindingId, mContext, uri,
ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
}
default:
Assert.fail("Unknown loader id for PeopleAndOptionsFragment!");
break;
}
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "Loader created after unbinding PeopleAndOptionsFragment");
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) {
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
if (isBound(cursorLoader.getBindingId())) {
switch (loader.getId()) {
case CONVERSATION_OPTIONS_LOADER:
mListener.onOptionsCursorUpdated(this, data);
break;
case PARTICIPANT_LOADER:
mParticipantData.bind(data);
mListener.onParticipantsListLoaded(this,
mParticipantData.getParticipantListExcludingSelf());
break;
default:
Assert.fail("Unknown loader id for PeopleAndOptionsFragment!");
break;
}
} else {
LogUtil.w(LogUtil.BUGLE_TAG,
"Loader finished after unbinding PeopleAndOptionsFragment");
}
}
/**
* {@inheritDoc}
*/
@Override
public void onLoaderReset(final Loader<Cursor> loader) {
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
if (isBound(cursorLoader.getBindingId())) {
switch (loader.getId()) {
case CONVERSATION_OPTIONS_LOADER:
mListener.onOptionsCursorUpdated(this, null);
break;
case PARTICIPANT_LOADER:
mParticipantData.bind(null);
break;
default:
Assert.fail("Unknown loader id for PeopleAndOptionsFragment!");
break;
}
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "Loader reset after unbinding PeopleAndOptionsFragment");
}
}
public void init(final LoaderManager loaderManager,
final BindingBase<PeopleAndOptionsData> binding) {
final Bundle args = new Bundle();
args.putString(BINDING_ID, binding.getBindingId());
mLoaderManager = loaderManager;
mLoaderManager.initLoader(CONVERSATION_OPTIONS_LOADER, args, this);
mLoaderManager.initLoader(PARTICIPANT_LOADER, args, this);
}
@Override
protected void unregisterListeners() {
mListener = null;
// This could be null if we bind but the caller doesn't init the BindableData
if (mLoaderManager != null) {
mLoaderManager.destroyLoader(CONVERSATION_OPTIONS_LOADER);
mLoaderManager.destroyLoader(PARTICIPANT_LOADER);
mLoaderManager = null;
}
}
public void enableConversationNotifications(final BindingBase<PeopleAndOptionsData> binding,
final boolean enable) {
final String bindingId = binding.getBindingId();
if (isBound(bindingId)) {
UpdateConversationOptionsAction.enableConversationNotifications(
mConversationId, enable);
}
}
public void setConversationNotificationSound(final BindingBase<PeopleAndOptionsData> binding,
final String ringtoneUri) {
final String bindingId = binding.getBindingId();
if (isBound(bindingId)) {
UpdateConversationOptionsAction.setConversationNotificationSound(mConversationId,
ringtoneUri);
}
}
public void enableConversationNotificationVibration(
final BindingBase<PeopleAndOptionsData> binding, final boolean enable) {
final String bindingId = binding.getBindingId();
if (isBound(bindingId)) {
UpdateConversationOptionsAction.enableVibrationForConversationNotification(
mConversationId, enable);
}
}
public void setDestinationBlocked(final BindingBase<PeopleAndOptionsData> binding,
final boolean blocked) {
final String bindingId = binding.getBindingId();
final ParticipantData participantData = mParticipantData.getOtherParticipant();
if (isBound(bindingId) && participantData != null) {
UpdateDestinationBlockedAction.updateDestinationBlocked(
participantData.getNormalizedDestination(),
blocked, mConversationId,
BugleActionToasts.makeUpdateDestinationBlockedActionListener(mContext));
}
}
}
@@ -1,155 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.content.Context;
import android.database.Cursor;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import com.android.messaging.R;
import com.android.messaging.datamodel.data.ConversationListItemData.ConversationListViewColumns;
import com.android.messaging.util.Assert;
import com.android.messaging.util.RingtoneUtil;
public class PeopleOptionsItemData {
public static final String[] PROJECTION = {
ConversationListViewColumns.NOTIFICATION_ENABLED,
ConversationListViewColumns.NOTIFICATION_SOUND_URI,
ConversationListViewColumns.NOTIFICATION_VIBRATION,
};
// Column index for query projection.
private static final int INDEX_NOTIFICATION_ENABLED = 0;
private static final int INDEX_NOTIFICATION_SOUND_URI = 1;
private static final int INDEX_NOTIFICATION_VIBRATION = 2;
// Identification for each setting that's surfaced to the UI layer.
public static final int SETTING_NOTIFICATION_ENABLED = 0;
public static final int SETTING_NOTIFICATION_SOUND_URI = 1;
public static final int SETTING_NOTIFICATION_VIBRATION = 2;
public static final int SETTING_BLOCKED = 3;
public static final int SETTINGS_COUNT = 4;
// Type of UI switch to show for the toggle button.
public static final int TOGGLE_TYPE_CHECKBOX = 0;
public static final int TOGGLE_TYPE_SWITCH = 1;
private String mTitle;
private String mSubtitle;
private Uri mRingtoneUri;
private boolean mCheckable;
private boolean mChecked;
private boolean mEnabled;
private int mItemId;
private ParticipantData mOtherParticipant;
private final Context mContext;
public PeopleOptionsItemData(final Context context) {
mContext = context;
}
/**
* Bind to a specific setting column on conversation metadata cursor. (Note
* that it binds to columns because it treats individual columns of the cursor as
* separate options to display for the conversation, e.g. notification settings).
*/
public void bind(
final Cursor cursor, final ParticipantData otherParticipant, final int settingType) {
mSubtitle = null;
mRingtoneUri = null;
mCheckable = true;
mEnabled = true;
mItemId = settingType;
mOtherParticipant = otherParticipant;
final boolean notificationEnabled = cursor.getInt(INDEX_NOTIFICATION_ENABLED) == 1;
switch (settingType) {
case SETTING_NOTIFICATION_ENABLED:
mTitle = mContext.getString(R.string.notifications_enabled_conversation_pref_title);
mChecked = notificationEnabled;
break;
case SETTING_NOTIFICATION_SOUND_URI:
mTitle = mContext.getString(R.string.notification_sound_pref_title);
final String ringtoneString = cursor.getString(INDEX_NOTIFICATION_SOUND_URI);
Uri ringtoneUri = RingtoneUtil.getNotificationRingtoneUri(ringtoneString);
mSubtitle = mContext.getString(R.string.silent_ringtone);
if (ringtoneUri != null) {
final Ringtone ringtone = RingtoneManager.getRingtone(mContext, ringtoneUri);
if (ringtone != null) {
mSubtitle = ringtone.getTitle(mContext);
}
}
mCheckable = false;
mRingtoneUri = ringtoneUri;
mEnabled = notificationEnabled;
break;
case SETTING_NOTIFICATION_VIBRATION:
mTitle = mContext.getString(R.string.notification_vibrate_pref_title);
mChecked = cursor.getInt(INDEX_NOTIFICATION_VIBRATION) == 1;
mEnabled = notificationEnabled;
break;
case SETTING_BLOCKED:
Assert.notNull(otherParticipant);
final int resourceId = otherParticipant.isBlocked() ?
R.string.unblock_contact_title : R.string.block_contact_title;
mTitle = mContext.getString(resourceId, otherParticipant.getDisplayDestination());
mCheckable = false;
break;
default:
Assert.fail("Unsupported conversation option type!");
}
}
public String getTitle() {
return mTitle;
}
public String getSubtitle() {
return mSubtitle;
}
public boolean getCheckable() {
return mCheckable;
}
public boolean getChecked() {
return mChecked;
}
public boolean getEnabled() {
return mEnabled;
}
public int getItemId() {
return mItemId;
}
public Uri getRingtoneUri() {
return mRingtoneUri;
}
public ParticipantData getOtherParticipant() {
return mOtherParticipant;
}
}
@@ -1,67 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.content.Intent;
import android.net.Uri;
import com.android.messaging.datamodel.binding.BindableData;
/**
* Bridges between any particpant/contact related data and data displayed in the PersonItemView.
*/
public abstract class PersonItemData extends BindableData {
/**
* The UI component that listens for data change and update accordingly.
*/
public interface PersonItemDataListener {
void onPersonDataUpdated(PersonItemData data);
void onPersonDataFailed(PersonItemData data, Exception exception);
}
private PersonItemDataListener mListener;
public abstract Uri getAvatarUri();
public abstract String getDisplayName();
public abstract String getDetails();
public abstract Intent getClickIntent();
public abstract long getContactId();
public abstract String getLookupKey();
public abstract String getNormalizedDestination();
public void setListener(final PersonItemDataListener listener) {
if (isBound()) {
mListener = listener;
}
}
protected void notifyDataUpdated() {
if (isBound() && mListener != null) {
mListener.onPersonDataUpdated(this);
}
}
protected void notifyDataFailed(final Exception exception) {
if (isBound() && mListener != null) {
mListener.onPersonDataFailed(this, exception);
}
}
@Override
protected void unregisterListeners() {
mListener = null;
}
}
@@ -1,107 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.database.Cursor;
import android.support.v4.util.ArrayMap;
import java.util.ArrayList;
import java.util.List;
import com.android.messaging.util.OsUtil;
/**
* A class that contains the list of all self participants potentially involved in a conversation.
* This class contains both active/inactive self entries when there is multi-SIM support.
*/
public class SelfParticipantsData {
/**
* The map from self participant ids to self-participant data entries in the participants table.
* This includes both active, inactive and default (with subId ==
* {@link ParticipantData#DEFAULT_SELF_SUB_ID}) subscriptions.
*/
private final ArrayMap<String, ParticipantData> mSelfParticipantMap;
public SelfParticipantsData() {
mSelfParticipantMap = new ArrayMap<String, ParticipantData>();
}
public void bind(final Cursor cursor) {
mSelfParticipantMap.clear();
if (cursor != null) {
while (cursor.moveToNext()) {
final ParticipantData newParticipant = ParticipantData.getFromCursor(cursor);
mSelfParticipantMap.put(newParticipant.getId(), newParticipant);
}
}
}
/**
* Gets the list of self participants for all subscriptions.
* @param activeOnly if set, returns active self entries only (i.e. those with SIMs plugged in).
*/
public List<ParticipantData> getSelfParticipants(final boolean activeOnly) {
List<ParticipantData> list = new ArrayList<ParticipantData>();
for (final ParticipantData self : mSelfParticipantMap.values()) {
if (!activeOnly || self.isActiveSubscription()) {
list.add(self);
}
}
return list;
}
/**
* Gets the self participant corresponding to the given self id.
*/
ParticipantData getSelfParticipantById(final String selfId) {
return mSelfParticipantMap.get(selfId);
}
/**
* Returns if a given self id represents the default self.
*/
boolean isDefaultSelf(final String selfId) {
if (!OsUtil.isAtLeastL_MR1()) {
return true;
}
final ParticipantData self = getSelfParticipantById(selfId);
return self == null ? false : self.getSubId() == ParticipantData.DEFAULT_SELF_SUB_ID;
}
public int getSelfParticipantsCountExcludingDefault(final boolean activeOnly) {
int count = 0;
for (final ParticipantData self : mSelfParticipantMap.values()) {
if (!self.isDefaultSelf() && (!activeOnly || self.isActiveSubscription())) {
count++;
}
}
return count;
}
public ParticipantData getDefaultSelfParticipant() {
for (final ParticipantData self : mSelfParticipantMap.values()) {
if (self.isDefaultSelf()) {
return self;
}
}
return null;
}
boolean isLoaded() {
return !mSelfParticipantMap.isEmpty();
}
}
@@ -1,223 +0,0 @@
/*
* 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.os.Bundle;
import android.text.TextUtils;
import com.android.messaging.R;
import com.android.messaging.datamodel.BoundCursorLoader;
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.binding.BindableData;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import java.util.ArrayList;
import java.util.List;
/**
* Services SettingsFragment's data needs for loading active self participants to display
* the list of active subscriptions.
*/
public class SettingsData extends BindableData implements
LoaderManager.LoaderCallbacks<Cursor> {
public interface SettingsDataListener {
void onSelfParticipantDataLoaded(SettingsData data);
}
public static class SettingsItem {
public static final int TYPE_GENERAL_SETTINGS = 1;
public static final int TYPE_PER_SUBSCRIPTION_SETTINGS = 2;
private final String mDisplayName;
private final String mDisplayDetail;
private final String mActivityTitle;
private final int mType;
private final int mSubId;
private SettingsItem(final String displayName, final String displayDetail,
final String activityTitle, final int type, final int subId) {
mDisplayName = displayName;
mDisplayDetail = displayDetail;
mActivityTitle = activityTitle;
mType = type;
mSubId = subId;
}
public String getDisplayName() {
return mDisplayName;
}
public String getDisplayDetail() {
return mDisplayDetail;
}
public int getType() {
return mType;
}
public int getSubId() {
return mSubId;
}
public String getActivityTitle() {
return mActivityTitle;
}
public static SettingsItem fromSelfParticipant(final Context context,
final ParticipantData self) {
Assert.isTrue(self.isSelf());
Assert.isTrue(self.isActiveSubscription());
final String displayDetail = TextUtils.isEmpty(self.getDisplayDestination()) ?
context.getString(R.string.sim_settings_unknown_number) :
self.getDisplayDestination();
final String displayName = context.getString(R.string.sim_specific_settings,
self.getSubscriptionName());
return new SettingsItem(displayName, displayDetail, displayName,
TYPE_PER_SUBSCRIPTION_SETTINGS, self.getSubId());
}
public static SettingsItem createGeneralSettingsItem(final Context context) {
return new SettingsItem(context.getString(R.string.general_settings),
null, context.getString(R.string.general_settings_activity_title),
TYPE_GENERAL_SETTINGS, -1);
}
public static SettingsItem createDefaultMmsSettingsItem(final Context context,
final int subId) {
return new SettingsItem(context.getString(R.string.advanced_settings),
null, context.getString(R.string.advanced_settings_activity_title),
TYPE_PER_SUBSCRIPTION_SETTINGS, subId);
}
}
private static final String BINDING_ID = "bindingId";
private final Context mContext;
private final SelfParticipantsData mSelfParticipantsData;
private LoaderManager mLoaderManager;
private SettingsDataListener mListener;
public SettingsData(final Context context,
final SettingsDataListener listener) {
mListener = listener;
mContext = context;
mSelfParticipantsData = new SelfParticipantsData();
}
private static final int SELF_PARTICIPANT_LOADER = 1;
@Override
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
Assert.equals(SELF_PARTICIPANT_LOADER, id);
Loader<Cursor> loader = null;
final String bindingId = args.getString(BINDING_ID);
// Check if data still bound to the requesting ui element
if (isBound(bindingId)) {
loader = new BoundCursorLoader(bindingId, mContext,
MessagingContentProvider.PARTICIPANTS_URI,
ParticipantData.ParticipantsQuery.PROJECTION,
ParticipantColumns.SUB_ID + " <> ?",
new String[] { String.valueOf(ParticipantData.OTHER_THAN_SELF_SUB_ID) },
null);
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "Creating self loader after unbinding");
}
return loader;
}
@Override
public void onLoadFinished(final Loader<Cursor> generic, final Cursor data) {
final BoundCursorLoader loader = (BoundCursorLoader) generic;
// Check if data still bound to the requesting ui element
if (isBound(loader.getBindingId())) {
mSelfParticipantsData.bind(data);
mListener.onSelfParticipantDataLoaded(this);
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "Self loader finished after unbinding");
}
}
@Override
public void onLoaderReset(final Loader<Cursor> generic) {
final BoundCursorLoader loader = (BoundCursorLoader) generic;
// Check if data still bound to the requesting ui element
if (isBound(loader.getBindingId())) {
mSelfParticipantsData.bind(null);
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "Self loader reset after unbinding");
}
}
public void init(final LoaderManager loaderManager,
final BindingBase<SettingsData> binding) {
final Bundle args = new Bundle();
args.putString(BINDING_ID, binding.getBindingId());
mLoaderManager = loaderManager;
mLoaderManager.initLoader(SELF_PARTICIPANT_LOADER, args, this);
}
@Override
protected void unregisterListeners() {
mListener = null;
// This could be null if we bind but the caller doesn't init the BindableData
if (mLoaderManager != null) {
mLoaderManager.destroyLoader(SELF_PARTICIPANT_LOADER);
mLoaderManager = null;
}
}
public List<SettingsItem> getSettingsItems() {
final List<ParticipantData> selfs = mSelfParticipantsData.getSelfParticipants(true);
final List<SettingsItem> settingsItems = new ArrayList<SettingsItem>();
// First goes the general settings, followed by per-subscription settings.
settingsItems.add(SettingsItem.createGeneralSettingsItem(mContext));
// For per-subscription settings, show the actual SIM name with phone number if the
// platorm is at least L-MR1 and there are multiple active SIMs.
final int activeSubCountExcludingDefault =
mSelfParticipantsData.getSelfParticipantsCountExcludingDefault(true);
if (OsUtil.isAtLeastL_MR1() && activeSubCountExcludingDefault > 0) {
for (ParticipantData self : selfs) {
if (!self.isDefaultSelf()) {
if (activeSubCountExcludingDefault > 1) {
settingsItems.add(SettingsItem.fromSelfParticipant(mContext, self));
} else {
// This is the only active non-default SIM.
settingsItems.add(SettingsItem.createDefaultMmsSettingsItem(mContext,
self.getSubId()));
break;
}
}
}
} else {
// Either pre-L-MR1, or there's no active SIM, so show the default MMS settings.
settingsItems.add(SettingsItem.createDefaultMmsSettingsItem(mContext,
ParticipantData.DEFAULT_SELF_SUB_ID));
}
return settingsItems;
}
}
@@ -1,128 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.content.Context;
import android.net.Uri;
import android.text.TextUtils;
import com.android.messaging.R;
import com.android.messaging.util.Assert;
import com.android.messaging.util.AvatarUriUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* This is a UI facing data model component that holds a list of
* {@link SubscriptionListData.SubscriptionListEntry}'s, one for each *active* subscriptions.
*
* This is used to:
* 1) Show a list of SIMs in the SIM Selector
* 2) Show the currently selected SIM in the compose message view
* 3) Show SIM indicators on conversation message views
*
* It builds on top of SelfParticipantsData and performs additional logic such as determining
* the set of icons to use for the individual Subs.
*/
public class SubscriptionListData {
/**
* Represents a single sub that backs UI.
*/
public static class SubscriptionListEntry {
public final String selfParticipantId;
public final Uri iconUri;
public final Uri selectedIconUri;
public final String displayName;
public final int displayColor;
public final String displayDestination;
private SubscriptionListEntry(final String selfParticipantId, final Uri iconUri,
final Uri selectedIconUri, final String displayName, final int displayColor,
final String displayDestination) {
this.selfParticipantId = selfParticipantId;
this.iconUri = iconUri;
this.selectedIconUri = selectedIconUri;
this.displayName = displayName;
this.displayColor = displayColor;
this.displayDestination = displayDestination;
}
static SubscriptionListEntry fromSelfParticipantData(
final ParticipantData selfParticipantData, final Context context) {
Assert.isTrue(selfParticipantData.isSelf());
Assert.isTrue(selfParticipantData.isActiveSubscription());
final int slotId = selfParticipantData.getDisplaySlotId();
final String iconIdentifier = String.format(Locale.getDefault(), "%d", slotId);
final String subscriptionName = selfParticipantData.getSubscriptionName();
final String displayName = TextUtils.isEmpty(subscriptionName) ?
context.getString(R.string.sim_slot_identifier, slotId) : subscriptionName;
return new SubscriptionListEntry(selfParticipantData.getId(),
AvatarUriUtil.createAvatarUri(selfParticipantData, iconIdentifier,
false /* selected */, false /* incoming */),
AvatarUriUtil.createAvatarUri(selfParticipantData, iconIdentifier,
true /* selected */, false /* incoming */),
displayName, selfParticipantData.getSubscriptionColor(),
selfParticipantData.getDisplayDestination());
}
}
private final List<SubscriptionListEntry> mEntriesExcludingDefault;
private SubscriptionListEntry mDefaultEntry;
private final Context mContext;
public SubscriptionListData(final Context context) {
mEntriesExcludingDefault = new ArrayList<SubscriptionListEntry>();
mContext = context;
}
public void bind(final List<ParticipantData> subs) {
mEntriesExcludingDefault.clear();
mDefaultEntry = null;
for (final ParticipantData sub : subs) {
final SubscriptionListEntry entry =
SubscriptionListEntry.fromSelfParticipantData(sub, mContext);
if (!sub.isDefaultSelf()) {
mEntriesExcludingDefault.add(entry);
} else {
mDefaultEntry = entry;
}
}
}
public List<SubscriptionListEntry> getActiveSubscriptionEntriesExcludingDefault() {
return mEntriesExcludingDefault;
}
public SubscriptionListEntry getActiveSubscriptionEntryBySelfId(final String selfId,
final boolean excludeDefault) {
if (mDefaultEntry != null && TextUtils.equals(mDefaultEntry.selfParticipantId, selfId)) {
return excludeDefault ? null : mDefaultEntry;
}
for (final SubscriptionListEntry entry : mEntriesExcludingDefault) {
if (TextUtils.equals(entry.selfParticipantId, selfId)) {
return entry;
}
}
return null;
}
public boolean hasData() {
return !mEntriesExcludingDefault.isEmpty() || mDefaultEntry != null;
}
}
@@ -1,185 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel.data;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import com.android.messaging.R;
import com.android.messaging.datamodel.binding.Binding;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.datamodel.media.BindableMediaRequest;
import com.android.messaging.datamodel.media.MediaRequest;
import com.android.messaging.datamodel.media.MediaResourceManager;
import com.android.messaging.datamodel.media.MediaResourceManager.MediaResourceLoadListener;
import com.android.messaging.datamodel.media.VCardRequestDescriptor;
import com.android.messaging.datamodel.media.VCardResource;
import com.android.messaging.datamodel.media.VCardResourceEntry;
import com.android.messaging.util.Assert;
import com.android.messaging.util.AvatarUriUtil;
import com.android.messaging.util.ContactUtil;
import java.util.List;
/**
* Data class for visualizing and loading data for a VCard contact.
*/
public class VCardContactItemData extends PersonItemData
implements MediaResourceLoadListener<VCardResource> {
private final Context mContext;
private final Uri mVCardUri;
private String mDetails;
private final Binding<BindableMediaRequest<VCardResource>> mBinding =
BindingBase.createBinding(this);
private VCardResource mVCardResource;
private static final Uri sDefaultAvatarUri =
AvatarUriUtil.createAvatarUri(null, null, null, null);
/**
* Constructor. This parses data from the given MessagePartData describing the vcard
*/
public VCardContactItemData(final Context context, final MessagePartData messagePartData) {
this(context, messagePartData.getContentUri());
Assert.isTrue(messagePartData.isVCard());
}
/**
* Constructor. This parses data from the given VCard Uri
*/
public VCardContactItemData(final Context context, final Uri vCardUri) {
mContext = context;
mDetails = mContext.getString(R.string.loading_vcard);
mVCardUri = vCardUri;
}
@Override
public Uri getAvatarUri() {
if (hasValidVCard()) {
final List<VCardResourceEntry> vcards = mVCardResource.getVCards();
Assert.isTrue(vcards.size() > 0);
if (vcards.size() == 1) {
return vcards.get(0).getAvatarUri();
}
}
return sDefaultAvatarUri;
}
@Override
public String getDisplayName() {
if (hasValidVCard()) {
final List<VCardResourceEntry> vcards = mVCardResource.getVCards();
Assert.isTrue(vcards.size() > 0);
if (vcards.size() == 1) {
return vcards.get(0).getDisplayName();
} else {
return mContext.getResources().getQuantityString(
R.plurals.vcard_multiple_display_name, vcards.size(), vcards.size());
}
}
return null;
}
@Override
public String getDetails() {
return mDetails;
}
@Override
public Intent getClickIntent() {
return null;
}
@Override
public long getContactId() {
return ContactUtil.INVALID_CONTACT_ID;
}
@Override
public String getLookupKey() {
return null;
}
@Override
public String getNormalizedDestination() {
return null;
}
public VCardResource getVCardResource() {
return hasValidVCard() ? mVCardResource : null;
}
public Uri getVCardUri() {
return hasValidVCard() ? mVCardUri : null;
}
public boolean hasValidVCard() {
return isBound() && mVCardResource != null;
}
@Override
public void bind(final String bindingId) {
super.bind(bindingId);
// Bind and request the VCard from media resource manager.
mBinding.bind(new VCardRequestDescriptor(mVCardUri).buildAsyncMediaRequest(mContext, this));
MediaResourceManager.get().requestMediaResourceAsync(mBinding.getData());
}
@Override
public void unbind(final String bindingId) {
super.unbind(bindingId);
mBinding.unbind();
if (mVCardResource != null) {
mVCardResource.release();
mVCardResource = null;
}
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof VCardContactItemData)) {
return false;
}
final VCardContactItemData lhs = (VCardContactItemData) o;
return mVCardUri.equals(lhs.mVCardUri);
}
@Override
public void onMediaResourceLoaded(final MediaRequest<VCardResource> request,
final VCardResource resource, final boolean isCached) {
Assert.isTrue(mVCardResource == null);
mBinding.ensureBound();
mDetails = mContext.getString(R.string.vcard_tap_hint);
mVCardResource = resource;
mVCardResource.addRef();
notifyDataUpdated();
}
@Override
public void onMediaResourceLoadError(final MediaRequest<VCardResource> request,
final Exception exception) {
mBinding.ensureBound();
mDetails = mContext.getString(R.string.failed_loading_vcard);
notifyDataFailed(exception);
}
}