summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/datamodel/data
diff options
context:
space:
mode:
authorMike Dodd <mdodd@google.com>2015-08-11 11:16:59 -0700
committerMike Dodd <mdodd@google.com>2015-08-12 08:58:28 -0700
commit461a34b466cb4b13dbbc2ec6330b31e217b2ac4e (patch)
treebc4b489af52d0e2521e21167d2ad76a47256f348 /src/com/android/messaging/datamodel/data
parent8b3e2b9c1b0a09423a7ba5d1091b9192106502f8 (diff)
downloadandroid_packages_apps_Messaging-461a34b466cb4b13dbbc2ec6330b31e217b2ac4e.tar.gz
android_packages_apps_Messaging-461a34b466cb4b13dbbc2ec6330b31e217b2ac4e.tar.bz2
android_packages_apps_Messaging-461a34b466cb4b13dbbc2ec6330b31e217b2ac4e.zip
Initial checkin of AOSP Messaging app.
b/23110861 Change-Id: I9aa980d7569247d6b2ca78f5dcb4502e1eaadb8a
Diffstat (limited to 'src/com/android/messaging/datamodel/data')
-rw-r--r--src/com/android/messaging/datamodel/data/BlockedParticipantsData.java103
-rw-r--r--src/com/android/messaging/datamodel/data/ContactListItemData.java160
-rw-r--r--src/com/android/messaging/datamodel/data/ContactPickerData.java194
-rw-r--r--src/com/android/messaging/datamodel/data/ConversationData.java849
-rw-r--r--src/com/android/messaging/datamodel/data/ConversationListData.java211
-rw-r--r--src/com/android/messaging/datamodel/data/ConversationListItemData.java510
-rw-r--r--src/com/android/messaging/datamodel/data/ConversationMessageBubbleData.java37
-rw-r--r--src/com/android/messaging/datamodel/data/ConversationMessageData.java917
-rw-r--r--src/com/android/messaging/datamodel/data/ConversationParticipantsData.java125
-rw-r--r--src/com/android/messaging/datamodel/data/DraftMessageData.java855
-rw-r--r--src/com/android/messaging/datamodel/data/GalleryGridItemData.java128
-rw-r--r--src/com/android/messaging/datamodel/data/LaunchConversationData.java90
-rw-r--r--src/com/android/messaging/datamodel/data/MediaPickerData.java175
-rw-r--r--src/com/android/messaging/datamodel/data/MediaPickerMessagePartData.java64
-rw-r--r--src/com/android/messaging/datamodel/data/MessageData.java922
-rw-r--r--src/com/android/messaging/datamodel/data/MessagePartData.java534
-rw-r--r--src/com/android/messaging/datamodel/data/ParticipantData.java569
-rw-r--r--src/com/android/messaging/datamodel/data/ParticipantListItemData.java95
-rw-r--r--src/com/android/messaging/datamodel/data/PendingAttachmentData.java176
-rw-r--r--src/com/android/messaging/datamodel/data/PeopleAndOptionsData.java210
-rw-r--r--src/com/android/messaging/datamodel/data/PeopleOptionsItemData.java155
-rw-r--r--src/com/android/messaging/datamodel/data/PersonItemData.java67
-rw-r--r--src/com/android/messaging/datamodel/data/SelfParticipantsData.java107
-rw-r--r--src/com/android/messaging/datamodel/data/SettingsData.java223
-rw-r--r--src/com/android/messaging/datamodel/data/SubscriptionListData.java128
-rw-r--r--src/com/android/messaging/datamodel/data/VCardContactItemData.java185
26 files changed, 7789 insertions, 0 deletions
diff --git a/src/com/android/messaging/datamodel/data/BlockedParticipantsData.java b/src/com/android/messaging/datamodel/data/BlockedParticipantsData.java
new file mode 100644
index 0000000..4e94ee1
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/BlockedParticipantsData.java
@@ -0,0 +1,103 @@
+/*
+ * 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));
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/ContactListItemData.java b/src/com/android/messaging/datamodel/data/ContactListItemData.java
new file mode 100644
index 0000000..dcc7e20
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/ContactListItemData.java
@@ -0,0 +1,160 @@
+/*
+ * 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;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/ContactPickerData.java b/src/com/android/messaging/datamodel/data/ContactPickerData.java
new file mode 100644
index 0000000..fd6fca0
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/ContactPickerData.java
@@ -0,0 +1,194 @@
+/*
+ * 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());
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/ConversationData.java b/src/com/android/messaging/datamodel/data/ConversationData.java
new file mode 100644
index 0000000..d504928
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/ConversationData.java
@@ -0,0 +1,849 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.messaging.datamodel.data;
+
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.Loader;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.database.sqlite.SQLiteFullException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.android.common.contacts.DataUsageStatUpdater;
+import com.android.messaging.Factory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.BoundCursorLoader;
+import com.android.messaging.datamodel.BugleNotifications;
+import com.android.messaging.datamodel.DataModel;
+import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
+import com.android.messaging.datamodel.MessagingContentProvider;
+import com.android.messaging.datamodel.action.DeleteConversationAction;
+import com.android.messaging.datamodel.action.DeleteMessageAction;
+import com.android.messaging.datamodel.action.InsertNewMessageAction;
+import com.android.messaging.datamodel.action.RedownloadMmsAction;
+import com.android.messaging.datamodel.action.ResendMessageAction;
+import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
+import com.android.messaging.datamodel.binding.BindableData;
+import com.android.messaging.datamodel.binding.Binding;
+import com.android.messaging.datamodel.binding.BindingBase;
+import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
+import com.android.messaging.sms.MmsSmsUtils;
+import com.android.messaging.sms.MmsUtils;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.Assert.RunsOnMainThread;
+import com.android.messaging.util.ContactUtil;
+import com.android.messaging.util.LogUtil;
+import com.android.messaging.util.OsUtil;
+import com.android.messaging.util.PhoneUtils;
+import com.android.messaging.util.SafeAsyncTask;
+import com.android.messaging.widget.WidgetConversationProvider;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class ConversationData extends BindableData {
+
+ private static final String TAG = "bugle_datamodel";
+ private static final String BINDING_ID = "bindingId";
+ private static final long LAST_MESSAGE_TIMESTAMP_NaN = -1;
+ private static final int MESSAGE_COUNT_NaN = -1;
+
+ /**
+ * Takes a conversation id and a list of message ids and computes the positions
+ * for each message.
+ */
+ public List<Integer> getPositions(final String conversationId, final List<Long> ids) {
+ final ArrayList<Integer> result = new ArrayList<Integer>();
+
+ if (ids.isEmpty()) {
+ return result;
+ }
+
+ final Cursor c = new ConversationData.ReversedCursor(
+ DataModel.get().getDatabase().rawQuery(
+ ConversationMessageData.getConversationMessageIdsQuerySql(),
+ new String [] { conversationId }));
+ if (c != null) {
+ try {
+ final Set<Long> idsSet = new HashSet<Long>(ids);
+ if (c.moveToLast()) {
+ do {
+ final long messageId = c.getLong(0);
+ if (idsSet.contains(messageId)) {
+ result.add(c.getPosition());
+ }
+ } while (c.moveToPrevious());
+ }
+ } finally {
+ c.close();
+ }
+ }
+ Collections.sort(result);
+ return result;
+ }
+
+ public interface ConversationDataListener {
+ public void onConversationMessagesCursorUpdated(ConversationData data, Cursor cursor,
+ @Nullable ConversationMessageData newestMessage, boolean isSync);
+ public void onConversationMetadataUpdated(ConversationData data);
+ public void closeConversation(String conversationId);
+ public void onConversationParticipantDataLoaded(ConversationData data);
+ public void onSubscriptionListDataLoaded(ConversationData data);
+ }
+
+ private static class ReversedCursor extends CursorWrapper {
+ final int mCount;
+
+ public ReversedCursor(final Cursor cursor) {
+ super(cursor);
+ mCount = cursor.getCount();
+ }
+
+ @Override
+ public boolean moveToPosition(final int position) {
+ return super.moveToPosition(mCount - position - 1);
+ }
+
+ @Override
+ public int getPosition() {
+ return mCount - super.getPosition() - 1;
+ }
+
+ @Override
+ public boolean isAfterLast() {
+ return super.isBeforeFirst();
+ }
+
+ @Override
+ public boolean isBeforeFirst() {
+ return super.isAfterLast();
+ }
+
+ @Override
+ public boolean isFirst() {
+ return super.isLast();
+ }
+
+ @Override
+ public boolean isLast() {
+ return super.isFirst();
+ }
+
+ @Override
+ public boolean move(final int offset) {
+ return super.move(-offset);
+ }
+
+ @Override
+ public boolean moveToFirst() {
+ return super.moveToLast();
+ }
+
+ @Override
+ public boolean moveToLast() {
+ return super.moveToFirst();
+ }
+
+ @Override
+ public boolean moveToNext() {
+ return super.moveToPrevious();
+ }
+
+ @Override
+ public boolean moveToPrevious() {
+ return super.moveToNext();
+ }
+ }
+
+ /**
+ * A trampoline class so that we can inherit from LoaderManager.LoaderCallbacks multiple times.
+ */
+ private class MetadataLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
+ @Override
+ public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
+ Assert.equals(CONVERSATION_META_DATA_LOADER, id);
+ Loader<Cursor> loader = null;
+
+ final String bindingId = args.getString(BINDING_ID);
+ // Check if data still bound to the requesting ui element
+ if (isBound(bindingId)) {
+ final Uri uri =
+ MessagingContentProvider.buildConversationMetadataUri(mConversationId);
+ loader = new BoundCursorLoader(bindingId, mContext, uri,
+ ConversationListItemData.PROJECTION, null, null, null);
+ } else {
+ LogUtil.w(TAG, "Creating messages loader after unbinding mConversationId = " +
+ mConversationId);
+ }
+ return loader;
+ }
+
+ @Override
+ public void onLoadFinished(final Loader<Cursor> generic, final Cursor data) {
+ final BoundCursorLoader loader = (BoundCursorLoader) generic;
+
+ // Check if data still bound to the requesting ui element
+ if (isBound(loader.getBindingId())) {
+ if (data.moveToNext()) {
+ Assert.isTrue(data.getCount() == 1);
+ mConversationMetadata.bind(data);
+ mListeners.onConversationMetadataUpdated(ConversationData.this);
+ } else {
+ // Close the conversation, no meta data means conversation was deleted
+ LogUtil.w(TAG, "Meta data loader returned nothing for mConversationId = " +
+ mConversationId);
+ mListeners.closeConversation(mConversationId);
+ // Notify the widget the conversation is deleted so it can go into its
+ // configure state.
+ WidgetConversationProvider.notifyConversationDeleted(
+ Factory.get().getApplicationContext(),
+ mConversationId);
+ }
+ } else {
+ LogUtil.w(TAG, "Meta data loader finished after unbinding mConversationId = " +
+ mConversationId);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(final Loader<Cursor> generic) {
+ final BoundCursorLoader loader = (BoundCursorLoader) generic;
+
+ // Check if data still bound to the requesting ui element
+ if (isBound(loader.getBindingId())) {
+ // Clear the conversation meta data
+ mConversationMetadata = new ConversationListItemData();
+ mListeners.onConversationMetadataUpdated(ConversationData.this);
+ } else {
+ LogUtil.w(TAG, "Meta data loader reset after unbinding mConversationId = " +
+ mConversationId);
+ }
+ }
+ }
+
+ /**
+ * A trampoline class so that we can inherit from LoaderManager.LoaderCallbacks multiple times.
+ */
+ private class MessagesLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
+ @Override
+ public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
+ Assert.equals(CONVERSATION_MESSAGES_LOADER, id);
+ Loader<Cursor> loader = null;
+
+ final String bindingId = args.getString(BINDING_ID);
+ // Check if data still bound to the requesting ui element
+ if (isBound(bindingId)) {
+ final Uri uri =
+ MessagingContentProvider.buildConversationMessagesUri(mConversationId);
+ loader = new BoundCursorLoader(bindingId, mContext, uri,
+ ConversationMessageData.getProjection(), null, null, null);
+ mLastMessageTimestamp = LAST_MESSAGE_TIMESTAMP_NaN;
+ mMessageCount = MESSAGE_COUNT_NaN;
+ } else {
+ LogUtil.w(TAG, "Creating messages loader after unbinding mConversationId = " +
+ mConversationId);
+ }
+ return loader;
+ }
+
+ @Override
+ public void onLoadFinished(final Loader<Cursor> generic, final Cursor rawData) {
+ final BoundCursorLoader loader = (BoundCursorLoader) generic;
+
+ // Check if data still bound to the requesting ui element
+ if (isBound(loader.getBindingId())) {
+ // Check if we have a new message, or if we had a message sync.
+ ConversationMessageData newMessage = null;
+ boolean isSync = false;
+ Cursor data = null;
+ if (rawData != null) {
+ // Note that the cursor is sorted DESC so here we reverse it.
+ // This is a performance issue (improvement) for large cursors.
+ data = new ReversedCursor(rawData);
+
+ final int messageCountOld = mMessageCount;
+ mMessageCount = data.getCount();
+ final ConversationMessageData lastMessage = getLastMessage(data);
+ if (lastMessage != null) {
+ final long lastMessageTimestampOld = mLastMessageTimestamp;
+ mLastMessageTimestamp = lastMessage.getReceivedTimeStamp();
+ final String lastMessageIdOld = mLastMessageId;
+ mLastMessageId = lastMessage.getMessageId();
+ if (TextUtils.equals(lastMessageIdOld, mLastMessageId) &&
+ messageCountOld < mMessageCount) {
+ // Last message stays the same (no incoming message) but message
+ // count increased, which means there has been a message sync.
+ isSync = true;
+ } else if (messageCountOld != MESSAGE_COUNT_NaN && // Ignore initial load
+ mLastMessageTimestamp != LAST_MESSAGE_TIMESTAMP_NaN &&
+ mLastMessageTimestamp > lastMessageTimestampOld) {
+ newMessage = lastMessage;
+ }
+ } else {
+ mLastMessageTimestamp = LAST_MESSAGE_TIMESTAMP_NaN;
+ }
+ } else {
+ mMessageCount = MESSAGE_COUNT_NaN;
+ }
+
+ mListeners.onConversationMessagesCursorUpdated(ConversationData.this, data,
+ newMessage, isSync);
+ } else {
+ LogUtil.w(TAG, "Messages loader finished after unbinding mConversationId = " +
+ mConversationId);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(final Loader<Cursor> generic) {
+ final BoundCursorLoader loader = (BoundCursorLoader) generic;
+
+ // Check if data still bound to the requesting ui element
+ if (isBound(loader.getBindingId())) {
+ mListeners.onConversationMessagesCursorUpdated(ConversationData.this, null, null,
+ false);
+ mLastMessageTimestamp = LAST_MESSAGE_TIMESTAMP_NaN;
+ mMessageCount = MESSAGE_COUNT_NaN;
+ } else {
+ LogUtil.w(TAG, "Messages loader reset after unbinding mConversationId = " +
+ mConversationId);
+ }
+ }
+
+ private ConversationMessageData getLastMessage(final Cursor cursor) {
+ if (cursor != null && cursor.getCount() > 0) {
+ final int position = cursor.getPosition();
+ if (cursor.moveToLast()) {
+ final ConversationMessageData messageData = new ConversationMessageData();
+ messageData.bind(cursor);
+ cursor.move(position);
+ return messageData;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * A trampoline class so that we can inherit from LoaderManager.LoaderCallbacks multiple times.
+ */
+ private class ParticipantLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
+ @Override
+ public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
+ Assert.equals(PARTICIPANT_LOADER, id);
+ Loader<Cursor> loader = null;
+
+ final String bindingId = args.getString(BINDING_ID);
+ // Check if data still bound to the requesting ui element
+ if (isBound(bindingId)) {
+ final Uri uri =
+ MessagingContentProvider.buildConversationParticipantsUri(mConversationId);
+ loader = new BoundCursorLoader(bindingId, mContext, uri,
+ ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
+ } else {
+ LogUtil.w(TAG, "Creating participant loader after unbinding mConversationId = " +
+ mConversationId);
+ }
+ return loader;
+ }
+
+ @Override
+ public void onLoadFinished(final Loader<Cursor> generic, final Cursor data) {
+ final BoundCursorLoader loader = (BoundCursorLoader) generic;
+
+ // Check if data still bound to the requesting ui element
+ if (isBound(loader.getBindingId())) {
+ mParticipantData.bind(data);
+ mListeners.onConversationParticipantDataLoaded(ConversationData.this);
+ } else {
+ LogUtil.w(TAG, "Participant loader finished after unbinding mConversationId = " +
+ mConversationId);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(final Loader<Cursor> generic) {
+ final BoundCursorLoader loader = (BoundCursorLoader) generic;
+
+ // Check if data still bound to the requesting ui element
+ if (isBound(loader.getBindingId())) {
+ mParticipantData.bind(null);
+ } else {
+ LogUtil.w(TAG, "Participant loader reset after unbinding mConversationId = " +
+ mConversationId);
+ }
+ }
+ }
+
+ /**
+ * A trampoline class so that we can inherit from LoaderManager.LoaderCallbacks multiple times.
+ */
+ private class SelfParticipantLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
+ @Override
+ public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
+ Assert.equals(SELF_PARTICIPANT_LOADER, id);
+ Loader<Cursor> loader = null;
+
+ final String bindingId = args.getString(BINDING_ID);
+ // Check if data still bound to the requesting ui element
+ if (isBound(bindingId)) {
+ loader = new BoundCursorLoader(bindingId, mContext,
+ MessagingContentProvider.PARTICIPANTS_URI,
+ ParticipantData.ParticipantsQuery.PROJECTION,
+ ParticipantColumns.SUB_ID + " <> ?",
+ new String[] { String.valueOf(ParticipantData.OTHER_THAN_SELF_SUB_ID) },
+ null);
+ } else {
+ LogUtil.w(TAG, "Creating self loader after unbinding mConversationId = " +
+ mConversationId);
+ }
+ return loader;
+ }
+
+ @Override
+ public void onLoadFinished(final Loader<Cursor> generic, final Cursor data) {
+ final BoundCursorLoader loader = (BoundCursorLoader) generic;
+
+ // Check if data still bound to the requesting ui element
+ if (isBound(loader.getBindingId())) {
+ mSelfParticipantsData.bind(data);
+ mSubscriptionListData.bind(mSelfParticipantsData.getSelfParticipants(true));
+ mListeners.onSubscriptionListDataLoaded(ConversationData.this);
+ } else {
+ LogUtil.w(TAG, "Self loader finished after unbinding mConversationId = " +
+ mConversationId);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(final Loader<Cursor> generic) {
+ final BoundCursorLoader loader = (BoundCursorLoader) generic;
+
+ // Check if data still bound to the requesting ui element
+ if (isBound(loader.getBindingId())) {
+ mSelfParticipantsData.bind(null);
+ } else {
+ LogUtil.w(TAG, "Self loader reset after unbinding mConversationId = " +
+ mConversationId);
+ }
+ }
+ }
+
+ private final ConversationDataEventDispatcher mListeners;
+ private final MetadataLoaderCallbacks mMetadataLoaderCallbacks;
+ private final MessagesLoaderCallbacks mMessagesLoaderCallbacks;
+ private final ParticipantLoaderCallbacks mParticipantsLoaderCallbacks;
+ private final SelfParticipantLoaderCallbacks mSelfParticipantLoaderCallbacks;
+ private final Context mContext;
+ private final String mConversationId;
+ private final ConversationParticipantsData mParticipantData;
+ private final SelfParticipantsData mSelfParticipantsData;
+ private ConversationListItemData mConversationMetadata;
+ private final SubscriptionListData mSubscriptionListData;
+ private LoaderManager mLoaderManager;
+ private long mLastMessageTimestamp = LAST_MESSAGE_TIMESTAMP_NaN;
+ private int mMessageCount = MESSAGE_COUNT_NaN;
+ private String mLastMessageId;
+
+ public ConversationData(final Context context, final ConversationDataListener listener,
+ final String conversationId) {
+ Assert.isTrue(conversationId != null);
+ mContext = context;
+ mConversationId = conversationId;
+ mMetadataLoaderCallbacks = new MetadataLoaderCallbacks();
+ mMessagesLoaderCallbacks = new MessagesLoaderCallbacks();
+ mParticipantsLoaderCallbacks = new ParticipantLoaderCallbacks();
+ mSelfParticipantLoaderCallbacks = new SelfParticipantLoaderCallbacks();
+ mParticipantData = new ConversationParticipantsData();
+ mConversationMetadata = new ConversationListItemData();
+ mSelfParticipantsData = new SelfParticipantsData();
+ mSubscriptionListData = new SubscriptionListData(context);
+
+ mListeners = new ConversationDataEventDispatcher();
+ mListeners.add(listener);
+ }
+
+ @RunsOnMainThread
+ public void addConversationDataListener(final ConversationDataListener listener) {
+ Assert.isMainThread();
+ mListeners.add(listener);
+ }
+
+ public String getConversationName() {
+ return mConversationMetadata.getName();
+ }
+
+ public boolean getIsArchived() {
+ return mConversationMetadata.getIsArchived();
+ }
+
+ public String getIcon() {
+ return mConversationMetadata.getIcon();
+ }
+
+ public String getConversationId() {
+ return mConversationId;
+ }
+
+ public void setFocus() {
+ DataModel.get().setFocusedConversation(mConversationId);
+ // As we are loading the conversation assume the user has read the messages...
+ // Do this late though so that it doesn't get in the way of other actions
+ BugleNotifications.markMessagesAsRead(mConversationId);
+ }
+
+ public void unsetFocus() {
+ DataModel.get().setFocusedConversation(null);
+ }
+
+ public boolean isFocused() {
+ return isBound() && DataModel.get().isFocusedConversation(mConversationId);
+ }
+
+ private static final int CONVERSATION_META_DATA_LOADER = 1;
+ private static final int CONVERSATION_MESSAGES_LOADER = 2;
+ private static final int PARTICIPANT_LOADER = 3;
+ private static final int SELF_PARTICIPANT_LOADER = 4;
+
+ public void init(final LoaderManager loaderManager,
+ final BindingBase<ConversationData> binding) {
+ // Remember the binding id so that loader callbacks can check if data is still bound
+ // to same ui component
+ final Bundle args = new Bundle();
+ args.putString(BINDING_ID, binding.getBindingId());
+ mLoaderManager = loaderManager;
+ mLoaderManager.initLoader(CONVERSATION_META_DATA_LOADER, args, mMetadataLoaderCallbacks);
+ mLoaderManager.initLoader(CONVERSATION_MESSAGES_LOADER, args, mMessagesLoaderCallbacks);
+ mLoaderManager.initLoader(PARTICIPANT_LOADER, args, mParticipantsLoaderCallbacks);
+ mLoaderManager.initLoader(SELF_PARTICIPANT_LOADER, args, mSelfParticipantLoaderCallbacks);
+ }
+
+ @Override
+ protected void unregisterListeners() {
+ mListeners.clear();
+ // Make sure focus has moved away from this conversation
+ // TODO: May false trigger if destroy happens after "new" conversation is focused.
+ // Assert.isTrue(!DataModel.get().isFocusedConversation(mConversationId));
+
+ // This could be null if we bind but the caller doesn't init the BindableData
+ if (mLoaderManager != null) {
+ mLoaderManager.destroyLoader(CONVERSATION_META_DATA_LOADER);
+ mLoaderManager.destroyLoader(CONVERSATION_MESSAGES_LOADER);
+ mLoaderManager.destroyLoader(PARTICIPANT_LOADER);
+ mLoaderManager.destroyLoader(SELF_PARTICIPANT_LOADER);
+ mLoaderManager = null;
+ }
+ }
+
+ /**
+ * Gets the default self participant in the participant table (NOT the conversation's self).
+ * This is available as soon as self participant data is loaded.
+ */
+ public ParticipantData getDefaultSelfParticipant() {
+ return mSelfParticipantsData.getDefaultSelfParticipant();
+ }
+
+ public List<ParticipantData> getSelfParticipants(final boolean activeOnly) {
+ return mSelfParticipantsData.getSelfParticipants(activeOnly);
+ }
+
+ public int getSelfParticipantsCountExcludingDefault(final boolean activeOnly) {
+ return mSelfParticipantsData.getSelfParticipantsCountExcludingDefault(activeOnly);
+ }
+
+ public ParticipantData getSelfParticipantById(final String selfId) {
+ return mSelfParticipantsData.getSelfParticipantById(selfId);
+ }
+
+ /**
+ * For a 1:1 conversation return the other (not self) participant (else null)
+ */
+ public ParticipantData getOtherParticipant() {
+ return mParticipantData.getOtherParticipant();
+ }
+
+ /**
+ * Return true once the participants are loaded
+ */
+ public boolean getParticipantsLoaded() {
+ return mParticipantData.isLoaded();
+ }
+
+ public void sendMessage(final BindingBase<ConversationData> binding,
+ final MessageData message) {
+ Assert.isTrue(TextUtils.equals(mConversationId, message.getConversationId()));
+ Assert.isTrue(binding.getData() == this);
+
+ if (!OsUtil.isAtLeastL_MR1() || message.getSelfId() == null) {
+ InsertNewMessageAction.insertNewMessage(message);
+ } else {
+ final int systemDefaultSubId = PhoneUtils.getDefault().getDefaultSmsSubscriptionId();
+ if (systemDefaultSubId != ParticipantData.DEFAULT_SELF_SUB_ID &&
+ mSelfParticipantsData.isDefaultSelf(message.getSelfId())) {
+ // Lock the sub selection to the system default SIM as soon as the user clicks on
+ // the send button to avoid races between this and when InsertNewMessageAction is
+ // actually executed on the data model thread, during which the user can potentially
+ // change the system default SIM in Settings.
+ InsertNewMessageAction.insertNewMessage(message, systemDefaultSubId);
+ } else {
+ InsertNewMessageAction.insertNewMessage(message);
+ }
+ }
+ // Update contacts so Frequents will reflect messaging activity.
+ if (!getParticipantsLoaded()) {
+ return; // oh well, not critical
+ }
+ final ArrayList<String> phones = new ArrayList<>();
+ final ArrayList<String> emails = new ArrayList<>();
+ for (final ParticipantData participant : mParticipantData) {
+ if (!participant.isSelf()) {
+ if (participant.isEmail()) {
+ emails.add(participant.getSendDestination());
+ } else {
+ phones.add(participant.getSendDestination());
+ }
+ }
+ }
+
+ if (ContactUtil.hasReadContactsPermission()) {
+ SafeAsyncTask.executeOnThreadPool(new Runnable() {
+ @Override
+ public void run() {
+ final DataUsageStatUpdater updater = new DataUsageStatUpdater(
+ Factory.get().getApplicationContext());
+ try {
+ if (!phones.isEmpty()) {
+ updater.updateWithPhoneNumber(phones);
+ }
+ if (!emails.isEmpty()) {
+ updater.updateWithAddress(emails);
+ }
+ } catch (final SQLiteFullException ex) {
+ LogUtil.w(TAG, "Unable to update contact", ex);
+ }
+ }
+ });
+ }
+ }
+
+ public void downloadMessage(final BindingBase<ConversationData> binding,
+ final String messageId) {
+ Assert.isTrue(binding.getData() == this);
+ Assert.notNull(messageId);
+ RedownloadMmsAction.redownloadMessage(messageId);
+ }
+
+ public void resendMessage(final BindingBase<ConversationData> binding, final String messageId) {
+ Assert.isTrue(binding.getData() == this);
+ Assert.notNull(messageId);
+ ResendMessageAction.resendMessage(messageId);
+ }
+
+ public void deleteMessage(final BindingBase<ConversationData> binding, final String messageId) {
+ Assert.isTrue(binding.getData() == this);
+ Assert.notNull(messageId);
+ DeleteMessageAction.deleteMessage(messageId);
+ }
+
+ public void deleteConversation(final Binding<ConversationData> binding) {
+ Assert.isTrue(binding.getData() == this);
+ // If possible use timestamp of last message shown to delete only messages user is aware of
+ if (mConversationMetadata == null) {
+ DeleteConversationAction.deleteConversation(mConversationId,
+ System.currentTimeMillis());
+ } else {
+ mConversationMetadata.deleteConversation();
+ }
+ }
+
+ public void archiveConversation(final BindingBase<ConversationData> binding) {
+ Assert.isTrue(binding.getData() == this);
+ UpdateConversationArchiveStatusAction.archiveConversation(mConversationId);
+ }
+
+ public void unarchiveConversation(final BindingBase<ConversationData> binding) {
+ Assert.isTrue(binding.getData() == this);
+ UpdateConversationArchiveStatusAction.unarchiveConversation(mConversationId);
+ }
+
+ public ConversationParticipantsData getParticipants() {
+ return mParticipantData;
+ }
+
+ /**
+ * Returns a dialable phone number for the participant if we are in a 1-1 conversation.
+ * @return the participant phone number, or null if the phone number is not valid or if there
+ * are more than one participant.
+ */
+ public String getParticipantPhoneNumber() {
+ final ParticipantData participant = this.getOtherParticipant();
+ if (participant != null) {
+ final String phoneNumber = participant.getSendDestination();
+ if (!TextUtils.isEmpty(phoneNumber) && MmsSmsUtils.isPhoneNumber(phoneNumber)) {
+ return phoneNumber;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Create a message to be forwarded from an existing message.
+ */
+ public MessageData createForwardedMessage(final ConversationMessageData message) {
+ final MessageData forwardedMessage = new MessageData();
+
+ final String originalSubject =
+ MmsUtils.cleanseMmsSubject(mContext.getResources(), message.getMmsSubject());
+ if (!TextUtils.isEmpty(originalSubject)) {
+ forwardedMessage.setMmsSubject(
+ mContext.getResources().getString(R.string.message_fwd, originalSubject));
+ }
+
+ for (final MessagePartData part : message.getParts()) {
+ MessagePartData forwardedPart;
+
+ // Depending on the part type, if it is text, we can directly create a text part;
+ // if it is attachment, then we need to create a pending attachment data out of it, so
+ // that we may persist the attachment locally in the scratch folder when the user picks
+ // a conversation to forward to.
+ if (part.isText()) {
+ forwardedPart = MessagePartData.createTextMessagePart(part.getText());
+ } else {
+ final PendingAttachmentData pendingAttachmentData = PendingAttachmentData
+ .createPendingAttachmentData(part.getContentType(), part.getContentUri());
+ forwardedPart = pendingAttachmentData;
+ }
+ forwardedMessage.addPart(forwardedPart);
+ }
+ return forwardedMessage;
+ }
+
+ public int getNumberOfParticipantsExcludingSelf() {
+ return mParticipantData.getNumberOfParticipantsExcludingSelf();
+ }
+
+ /**
+ * Returns {@link com.android.messaging.datamodel.data.SubscriptionListData
+ * .SubscriptionListEntry} for a given self participant so UI can display SIM-related info
+ * (icon, name etc.) for multi-SIM.
+ */
+ public SubscriptionListEntry getSubscriptionEntryForSelfParticipant(
+ final String selfParticipantId, final boolean excludeDefault) {
+ return getSubscriptionEntryForSelfParticipant(selfParticipantId, excludeDefault,
+ mSubscriptionListData, mSelfParticipantsData);
+ }
+
+ /**
+ * Returns {@link com.android.messaging.datamodel.data.SubscriptionListData
+ * .SubscriptionListEntry} for a given self participant so UI can display SIM-related info
+ * (icon, name etc.) for multi-SIM.
+ */
+ public static SubscriptionListEntry getSubscriptionEntryForSelfParticipant(
+ final String selfParticipantId, final boolean excludeDefault,
+ final SubscriptionListData subscriptionListData,
+ final SelfParticipantsData selfParticipantsData) {
+ // SIM indicators are shown in the UI only if:
+ // 1. Framework has MSIM support AND
+ // 2. The device has had multiple *active* subscriptions. AND
+ // 3. The message's subscription is active.
+ if (OsUtil.isAtLeastL_MR1() &&
+ selfParticipantsData.getSelfParticipantsCountExcludingDefault(true) > 1) {
+ return subscriptionListData.getActiveSubscriptionEntryBySelfId(selfParticipantId,
+ excludeDefault);
+ }
+ return null;
+ }
+
+ public SubscriptionListData getSubscriptionListData() {
+ return mSubscriptionListData;
+ }
+
+ /**
+ * A dummy implementation of {@link ConversationDataListener} so that subclasses may opt to
+ * implement some, but not all, of the interface methods.
+ */
+ public static class SimpleConversationDataListener implements ConversationDataListener {
+
+ @Override
+ public void onConversationMessagesCursorUpdated(final ConversationData data, final Cursor cursor,
+ @Nullable
+ final
+ ConversationMessageData newestMessage, final boolean isSync) {}
+
+ @Override
+ public void onConversationMetadataUpdated(final ConversationData data) {}
+
+ @Override
+ public void closeConversation(final String conversationId) {}
+
+ @Override
+ public void onConversationParticipantDataLoaded(final ConversationData data) {}
+
+ @Override
+ public void onSubscriptionListDataLoaded(final ConversationData data) {}
+
+ }
+
+ private class ConversationDataEventDispatcher
+ extends ArrayList<ConversationDataListener>
+ implements ConversationDataListener {
+
+ @Override
+ public void onConversationMessagesCursorUpdated(final ConversationData data, final Cursor cursor,
+ @Nullable
+ final ConversationMessageData newestMessage, final boolean isSync) {
+ for (final ConversationDataListener listener : this) {
+ listener.onConversationMessagesCursorUpdated(data, cursor, newestMessage, isSync);
+ }
+ }
+
+ @Override
+ public void onConversationMetadataUpdated(final ConversationData data) {
+ for (final ConversationDataListener listener : this) {
+ listener.onConversationMetadataUpdated(data);
+ }
+ }
+
+ @Override
+ public void closeConversation(final String conversationId) {
+ for (final ConversationDataListener listener : this) {
+ listener.closeConversation(conversationId);
+ }
+ }
+
+ @Override
+ public void onConversationParticipantDataLoaded(final ConversationData data) {
+ for (final ConversationDataListener listener : this) {
+ listener.onConversationParticipantDataLoaded(data);
+ }
+ }
+
+ @Override
+ public void onSubscriptionListDataLoaded(final ConversationData data) {
+ for (final ConversationDataListener listener : this) {
+ listener.onSubscriptionListDataLoaded(data);
+ }
+ }
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/ConversationListData.java b/src/com/android/messaging/datamodel/data/ConversationListData.java
new file mode 100644
index 0000000..3d27ecd
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/ConversationListData.java
@@ -0,0 +1,211 @@
+/*
+ * 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;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/ConversationListItemData.java b/src/com/android/messaging/datamodel/data/ConversationListItemData.java
new file mode 100644
index 0000000..b2e6e1c
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/ConversationListItemData.java
@@ -0,0 +1,510 @@
+/*
+ * 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);
+ }
+
+}
diff --git a/src/com/android/messaging/datamodel/data/ConversationMessageBubbleData.java b/src/com/android/messaging/datamodel/data/ConversationMessageBubbleData.java
new file mode 100644
index 0000000..f329f46
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/ConversationMessageBubbleData.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.datamodel.data;
+
+import android.text.TextUtils;
+
+/**
+ * Holds data for conversation message bubble which keeps track of whether it's been bound to
+ * a new message.
+ */
+public class ConversationMessageBubbleData {
+ private String mMessageId;
+
+ /**
+ * Binds to ConversationMessageData instance.
+ * @return true if we are binding to a different message, false if we are binding to the
+ * same message (e.g. in order to update the status text)
+ */
+ public boolean bind(final ConversationMessageData data) {
+ final boolean changed = !TextUtils.equals(mMessageId, data.getMessageId());
+ mMessageId = data.getMessageId();
+ return changed;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/ConversationMessageData.java b/src/com/android/messaging/datamodel/data/ConversationMessageData.java
new file mode 100644
index 0000000..19e1b97
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/ConversationMessageData.java
@@ -0,0 +1,917 @@
+/*
+ * 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;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/ConversationParticipantsData.java b/src/com/android/messaging/datamodel/data/ConversationParticipantsData.java
new file mode 100644
index 0000000..0b5ef51
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/ConversationParticipantsData.java
@@ -0,0 +1,125 @@
+/*
+ * 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();
+ }
+ };
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/DraftMessageData.java b/src/com/android/messaging/datamodel/data/DraftMessageData.java
new file mode 100644
index 0000000..7a7199a
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/DraftMessageData.java
@@ -0,0 +1,855 @@
+/*
+ * 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();
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/GalleryGridItemData.java b/src/com/android/messaging/datamodel/data/GalleryGridItemData.java
new file mode 100644
index 0000000..6649757
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/GalleryGridItemData.java
@@ -0,0 +1,128 @@
+/*
+ * 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;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/LaunchConversationData.java b/src/com/android/messaging/datamodel/data/LaunchConversationData.java
new file mode 100644
index 0000000..7eea580
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/LaunchConversationData.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.messaging.datamodel.data;
+
+import com.android.messaging.datamodel.action.ActionMonitor;
+import com.android.messaging.datamodel.action.GetOrCreateConversationAction;
+import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionListener;
+import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionMonitor;
+import com.android.messaging.datamodel.binding.BindableData;
+import com.android.messaging.datamodel.binding.BindingBase;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.Assert.RunsOnMainThread;
+import com.android.messaging.util.LogUtil;
+
+public class LaunchConversationData extends BindableData implements
+ GetOrCreateConversationActionListener {
+ public interface LaunchConversationDataListener {
+ void onGetOrCreateNewConversation(String conversationId);
+ void onGetOrCreateNewConversationFailed();
+ }
+
+ private LaunchConversationDataListener mListener;
+ private GetOrCreateConversationActionMonitor mMonitor;
+
+ public LaunchConversationData(final LaunchConversationDataListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ protected void unregisterListeners() {
+ mListener = null;
+ if (mMonitor != null) {
+ mMonitor.unregister();
+ }
+ mMonitor = null;
+ }
+
+ public void getOrCreateConversation(final BindingBase<LaunchConversationData> binding,
+ final String[] recipients) {
+ final String bindingId = binding.getBindingId();
+
+ // Start a new conversation from the list of contacts.
+ if (isBound(bindingId) && mMonitor == null) {
+ mMonitor = GetOrCreateConversationAction.getOrCreateConversation(recipients,
+ bindingId, this);
+ }
+ }
+
+ @Override
+ @RunsOnMainThread
+ public void onGetOrCreateConversationSucceeded(final ActionMonitor monitor,
+ final Object data, final String conversationId) {
+ Assert.isTrue(monitor == mMonitor);
+ Assert.isTrue(conversationId != null);
+
+ final String bindingId = (String) data;
+ if (isBound(bindingId) && mListener != null) {
+ mListener.onGetOrCreateNewConversation(conversationId);
+ }
+
+ mMonitor = null;
+ }
+
+ @Override
+ @RunsOnMainThread
+ public void onGetOrCreateConversationFailed(final ActionMonitor monitor,
+ final Object data) {
+ Assert.isTrue(monitor == mMonitor);
+ final String bindingId = (String) data;
+ if (isBound(bindingId) && mListener != null) {
+ mListener.onGetOrCreateNewConversationFailed();
+ }
+ LogUtil.e(LogUtil.BUGLE_TAG, "onGetOrCreateConversationFailed");
+ mMonitor = null;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/MediaPickerData.java b/src/com/android/messaging/datamodel/data/MediaPickerData.java
new file mode 100644
index 0000000..b0c8bf7
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/MediaPickerData.java
@@ -0,0 +1,175 @@
+/*
+ * 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);
+ }
+
+} \ No newline at end of file
diff --git a/src/com/android/messaging/datamodel/data/MediaPickerMessagePartData.java b/src/com/android/messaging/datamodel/data/MediaPickerMessagePartData.java
new file mode 100644
index 0000000..7de9166
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/MediaPickerMessagePartData.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.messaging.datamodel.data;
+
+import android.graphics.Rect;
+import android.net.Uri;
+
+public class MediaPickerMessagePartData extends MessagePartData {
+ private final Rect mStartRect;
+
+ public MediaPickerMessagePartData(final Rect startRect, final String contentType,
+ final Uri contentUri, final int width, final int height) {
+ this(startRect, null /* messageText */, contentType, contentUri, width, height);
+ }
+
+ public MediaPickerMessagePartData(final Rect startRect, final String messageText,
+ final String contentType, final Uri contentUri, final int width, final int height) {
+ this(startRect, messageText, contentType, contentUri, width, height,
+ false /*onlySingleAttachment*/);
+ }
+
+ public MediaPickerMessagePartData(final Rect startRect, final String contentType,
+ final Uri contentUri, final int width, final int height,
+ final boolean onlySingleAttachment) {
+ this(startRect, null /* messageText */, contentType, contentUri, width, height,
+ onlySingleAttachment);
+ }
+
+ public MediaPickerMessagePartData(final Rect startRect, final String messageText,
+ final String contentType, final Uri contentUri, final int width, final int height,
+ final boolean onlySingleAttachment) {
+ super(messageText, contentType, contentUri, width, height, onlySingleAttachment);
+ mStartRect = startRect;
+ }
+
+ /**
+ * @return The starting rect to animate the attachment preview from in order to perform a smooth
+ * transition
+ */
+ public Rect getStartRect() {
+ return mStartRect;
+ }
+
+ /**
+ * Modify the start rect of the attachment.
+ */
+ public void setStartRect(final Rect startRect) {
+ mStartRect.set(startRect);
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/MessageData.java b/src/com/android/messaging/datamodel/data/MessageData.java
new file mode 100644
index 0000000..a3698a9
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/MessageData.java
@@ -0,0 +1,922 @@
+/*
+ * 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();
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/MessagePartData.java b/src/com/android/messaging/datamodel/data/MessagePartData.java
new file mode 100644
index 0000000..fffaca8
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/MessagePartData.java
@@ -0,0 +1,534 @@
+/*
+ * 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;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/ParticipantData.java b/src/com/android/messaging/datamodel/data/ParticipantData.java
new file mode 100644
index 0000000..521c354
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/ParticipantData.java
@@ -0,0 +1,569 @@
+/*
+ * 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];
+ }
+ };
+}
diff --git a/src/com/android/messaging/datamodel/data/ParticipantListItemData.java b/src/com/android/messaging/datamodel/data/ParticipantListItemData.java
new file mode 100644
index 0000000..f6c9b5f
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/ParticipantListItemData.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.datamodel.data;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import com.android.messaging.datamodel.action.BugleActionToasts;
+import com.android.messaging.datamodel.action.UpdateDestinationBlockedAction;
+import com.android.messaging.util.AvatarUriUtil;
+
+/**
+ * Helps visualize a ParticipantData in a PersonItemView
+ */
+public class ParticipantListItemData extends PersonItemData {
+ private final Uri mAvatarUri;
+ private final String mDisplayName;
+ private final String mDetails;
+ private final long mContactId;
+ private final String mLookupKey;
+ private final String mNormalizedDestination;
+
+ /**
+ * Constructor. Takes necessary info from the incoming ParticipantData.
+ */
+ public ParticipantListItemData(final ParticipantData participant) {
+ mAvatarUri = AvatarUriUtil.createAvatarUri(participant);
+ mContactId = participant.getContactId();
+ mLookupKey = participant.getLookupKey();
+ mNormalizedDestination = participant.getNormalizedDestination();
+ if (TextUtils.isEmpty(participant.getFullName())) {
+ mDisplayName = participant.getSendDestination();
+ mDetails = null;
+ } else {
+ mDisplayName = participant.getFullName();
+ mDetails = (participant.isUnknownSender()) ? null : participant.getSendDestination();
+ }
+ }
+
+ @Override
+ public Uri getAvatarUri() {
+ return mAvatarUri;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return mDisplayName;
+ }
+
+ @Override
+ public String getDetails() {
+ return mDetails;
+ }
+
+ @Override
+ public Intent getClickIntent() {
+ return null;
+ }
+
+ @Override
+ public long getContactId() {
+ return mContactId;
+ }
+
+ @Override
+ public String getLookupKey() {
+ return mLookupKey;
+ }
+
+ @Override
+ public String getNormalizedDestination() {
+ return mNormalizedDestination;
+ }
+
+ public void unblock(final Context context) {
+ UpdateDestinationBlockedAction.updateDestinationBlocked(
+ mNormalizedDestination, false, null,
+ BugleActionToasts.makeUpdateDestinationBlockedActionListener(context));
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/PendingAttachmentData.java b/src/com/android/messaging/datamodel/data/PendingAttachmentData.java
new file mode 100644
index 0000000..5e079f8
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/PendingAttachmentData.java
@@ -0,0 +1,176 @@
+/*
+ * 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];
+ }
+ };
+}
diff --git a/src/com/android/messaging/datamodel/data/PeopleAndOptionsData.java b/src/com/android/messaging/datamodel/data/PeopleAndOptionsData.java
new file mode 100644
index 0000000..650a037
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/PeopleAndOptionsData.java
@@ -0,0 +1,210 @@
+/*
+ * 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));
+ }
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/PeopleOptionsItemData.java b/src/com/android/messaging/datamodel/data/PeopleOptionsItemData.java
new file mode 100644
index 0000000..5af6a30
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/PeopleOptionsItemData.java
@@ -0,0 +1,155 @@
+/*
+ * 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;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/PersonItemData.java b/src/com/android/messaging/datamodel/data/PersonItemData.java
new file mode 100644
index 0000000..a0a1ce8
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/PersonItemData.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.datamodel.data;
+
+import android.content.Intent;
+import android.net.Uri;
+
+import com.android.messaging.datamodel.binding.BindableData;
+
+/**
+ * Bridges between any particpant/contact related data and data displayed in the PersonItemView.
+ */
+public abstract class PersonItemData extends BindableData {
+ /**
+ * The UI component that listens for data change and update accordingly.
+ */
+ public interface PersonItemDataListener {
+ void onPersonDataUpdated(PersonItemData data);
+ void onPersonDataFailed(PersonItemData data, Exception exception);
+ }
+
+ private PersonItemDataListener mListener;
+
+ public abstract Uri getAvatarUri();
+ public abstract String getDisplayName();
+ public abstract String getDetails();
+ public abstract Intent getClickIntent();
+ public abstract long getContactId();
+ public abstract String getLookupKey();
+ public abstract String getNormalizedDestination();
+
+ public void setListener(final PersonItemDataListener listener) {
+ if (isBound()) {
+ mListener = listener;
+ }
+ }
+
+ protected void notifyDataUpdated() {
+ if (isBound() && mListener != null) {
+ mListener.onPersonDataUpdated(this);
+ }
+ }
+
+ protected void notifyDataFailed(final Exception exception) {
+ if (isBound() && mListener != null) {
+ mListener.onPersonDataFailed(this, exception);
+ }
+ }
+
+ @Override
+ protected void unregisterListeners() {
+ mListener = null;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/SelfParticipantsData.java b/src/com/android/messaging/datamodel/data/SelfParticipantsData.java
new file mode 100644
index 0000000..43302ed
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/SelfParticipantsData.java
@@ -0,0 +1,107 @@
+/*
+ * 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();
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/SettingsData.java b/src/com/android/messaging/datamodel/data/SettingsData.java
new file mode 100644
index 0000000..7474619
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/SettingsData.java
@@ -0,0 +1,223 @@
+/*
+ * 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;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/SubscriptionListData.java b/src/com/android/messaging/datamodel/data/SubscriptionListData.java
new file mode 100644
index 0000000..b5d4e4b
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/SubscriptionListData.java
@@ -0,0 +1,128 @@
+/*
+ * 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;
+ }
+}
diff --git a/src/com/android/messaging/datamodel/data/VCardContactItemData.java b/src/com/android/messaging/datamodel/data/VCardContactItemData.java
new file mode 100644
index 0000000..8abf493
--- /dev/null
+++ b/src/com/android/messaging/datamodel/data/VCardContactItemData.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.datamodel.data;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+import com.android.messaging.R;
+import com.android.messaging.datamodel.binding.Binding;
+import com.android.messaging.datamodel.binding.BindingBase;
+import com.android.messaging.datamodel.media.BindableMediaRequest;
+import com.android.messaging.datamodel.media.MediaRequest;
+import com.android.messaging.datamodel.media.MediaResourceManager;
+import com.android.messaging.datamodel.media.MediaResourceManager.MediaResourceLoadListener;
+import com.android.messaging.datamodel.media.VCardRequestDescriptor;
+import com.android.messaging.datamodel.media.VCardResource;
+import com.android.messaging.datamodel.media.VCardResourceEntry;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.AvatarUriUtil;
+import com.android.messaging.util.ContactUtil;
+
+import java.util.List;
+
+/**
+ * Data class for visualizing and loading data for a VCard contact.
+ */
+public class VCardContactItemData extends PersonItemData
+ implements MediaResourceLoadListener<VCardResource> {
+ private final Context mContext;
+ private final Uri mVCardUri;
+ private String mDetails;
+ private final Binding<BindableMediaRequest<VCardResource>> mBinding =
+ BindingBase.createBinding(this);
+ private VCardResource mVCardResource;
+
+ private static final Uri sDefaultAvatarUri =
+ AvatarUriUtil.createAvatarUri(null, null, null, null);
+
+ /**
+ * Constructor. This parses data from the given MessagePartData describing the vcard
+ */
+ public VCardContactItemData(final Context context, final MessagePartData messagePartData) {
+ this(context, messagePartData.getContentUri());
+ Assert.isTrue(messagePartData.isVCard());
+ }
+
+ /**
+ * Constructor. This parses data from the given VCard Uri
+ */
+ public VCardContactItemData(final Context context, final Uri vCardUri) {
+ mContext = context;
+ mDetails = mContext.getString(R.string.loading_vcard);
+ mVCardUri = vCardUri;
+ }
+
+ @Override
+ public Uri getAvatarUri() {
+ if (hasValidVCard()) {
+ final List<VCardResourceEntry> vcards = mVCardResource.getVCards();
+ Assert.isTrue(vcards.size() > 0);
+ if (vcards.size() == 1) {
+ return vcards.get(0).getAvatarUri();
+ }
+ }
+ return sDefaultAvatarUri;
+ }
+
+ @Override
+ public String getDisplayName() {
+ if (hasValidVCard()) {
+ final List<VCardResourceEntry> vcards = mVCardResource.getVCards();
+ Assert.isTrue(vcards.size() > 0);
+ if (vcards.size() == 1) {
+ return vcards.get(0).getDisplayName();
+ } else {
+ return mContext.getResources().getQuantityString(
+ R.plurals.vcard_multiple_display_name, vcards.size(), vcards.size());
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getDetails() {
+ return mDetails;
+ }
+
+ @Override
+ public Intent getClickIntent() {
+ return null;
+ }
+
+ @Override
+ public long getContactId() {
+ return ContactUtil.INVALID_CONTACT_ID;
+ }
+
+ @Override
+ public String getLookupKey() {
+ return null;
+ }
+
+ @Override
+ public String getNormalizedDestination() {
+ return null;
+ }
+
+ public VCardResource getVCardResource() {
+ return hasValidVCard() ? mVCardResource : null;
+ }
+
+ public Uri getVCardUri() {
+ return hasValidVCard() ? mVCardUri : null;
+ }
+
+ public boolean hasValidVCard() {
+ return isBound() && mVCardResource != null;
+ }
+
+ @Override
+ public void bind(final String bindingId) {
+ super.bind(bindingId);
+
+ // Bind and request the VCard from media resource manager.
+ mBinding.bind(new VCardRequestDescriptor(mVCardUri).buildAsyncMediaRequest(mContext, this));
+ MediaResourceManager.get().requestMediaResourceAsync(mBinding.getData());
+ }
+
+ @Override
+ public void unbind(final String bindingId) {
+ super.unbind(bindingId);
+ mBinding.unbind();
+ if (mVCardResource != null) {
+ mVCardResource.release();
+ mVCardResource = null;
+ }
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof VCardContactItemData)) {
+ return false;
+ }
+
+ final VCardContactItemData lhs = (VCardContactItemData) o;
+ return mVCardUri.equals(lhs.mVCardUri);
+ }
+
+ @Override
+ public void onMediaResourceLoaded(final MediaRequest<VCardResource> request,
+ final VCardResource resource, final boolean isCached) {
+ Assert.isTrue(mVCardResource == null);
+ mBinding.ensureBound();
+ mDetails = mContext.getString(R.string.vcard_tap_hint);
+ mVCardResource = resource;
+ mVCardResource.addRef();
+ notifyDataUpdated();
+ }
+
+ @Override
+ public void onMediaResourceLoadError(final MediaRequest<VCardResource> request,
+ final Exception exception) {
+ mBinding.ensureBound();
+ mDetails = mContext.getString(R.string.failed_loading_vcard);
+ notifyDataFailed(exception);
+ }
+}