Home | History | Annotate | Download | only in data
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.messaging.datamodel.data;
     18 
     19 import android.app.LoaderManager;
     20 import android.content.Context;
     21 import android.content.Loader;
     22 import android.database.Cursor;
     23 import android.database.CursorWrapper;
     24 import android.database.sqlite.SQLiteFullException;
     25 import android.net.Uri;
     26 import android.os.Bundle;
     27 import android.support.annotation.Nullable;
     28 import android.text.TextUtils;
     29 
     30 import com.android.common.contacts.DataUsageStatUpdater;
     31 import com.android.messaging.Factory;
     32 import com.android.messaging.R;
     33 import com.android.messaging.datamodel.BoundCursorLoader;
     34 import com.android.messaging.datamodel.BugleNotifications;
     35 import com.android.messaging.datamodel.DataModel;
     36 import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
     37 import com.android.messaging.datamodel.MessagingContentProvider;
     38 import com.android.messaging.datamodel.action.DeleteConversationAction;
     39 import com.android.messaging.datamodel.action.DeleteMessageAction;
     40 import com.android.messaging.datamodel.action.InsertNewMessageAction;
     41 import com.android.messaging.datamodel.action.RedownloadMmsAction;
     42 import com.android.messaging.datamodel.action.ResendMessageAction;
     43 import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
     44 import com.android.messaging.datamodel.binding.BindableData;
     45 import com.android.messaging.datamodel.binding.Binding;
     46 import com.android.messaging.datamodel.binding.BindingBase;
     47 import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
     48 import com.android.messaging.sms.MmsSmsUtils;
     49 import com.android.messaging.sms.MmsUtils;
     50 import com.android.messaging.util.Assert;
     51 import com.android.messaging.util.Assert.RunsOnMainThread;
     52 import com.android.messaging.util.ContactUtil;
     53 import com.android.messaging.util.LogUtil;
     54 import com.android.messaging.util.OsUtil;
     55 import com.android.messaging.util.PhoneUtils;
     56 import com.android.messaging.util.SafeAsyncTask;
     57 import com.android.messaging.widget.WidgetConversationProvider;
     58 
     59 import java.util.ArrayList;
     60 import java.util.Collections;
     61 import java.util.HashSet;
     62 import java.util.List;
     63 import java.util.Set;
     64 
     65 public class ConversationData extends BindableData {
     66 
     67     private static final String TAG = "bugle_datamodel";
     68     private static final String BINDING_ID = "bindingId";
     69     private static final long LAST_MESSAGE_TIMESTAMP_NaN = -1;
     70     private static final int MESSAGE_COUNT_NaN = -1;
     71 
     72     /**
     73      * Takes a conversation id and a list of message ids and computes the positions
     74      * for each message.
     75      */
     76     public List<Integer> getPositions(final String conversationId, final List<Long> ids) {
     77         final ArrayList<Integer> result = new ArrayList<Integer>();
     78 
     79         if (ids.isEmpty()) {
     80             return result;
     81         }
     82 
     83         final Cursor c = new ConversationData.ReversedCursor(
     84                 DataModel.get().getDatabase().rawQuery(
     85                         ConversationMessageData.getConversationMessageIdsQuerySql(),
     86                         new String [] { conversationId }));
     87         if (c != null) {
     88             try {
     89                 final Set<Long> idsSet = new HashSet<Long>(ids);
     90                 if (c.moveToLast()) {
     91                     do {
     92                         final long messageId = c.getLong(0);
     93                         if (idsSet.contains(messageId)) {
     94                             result.add(c.getPosition());
     95                         }
     96                     } while (c.moveToPrevious());
     97                 }
     98             } finally {
     99                 c.close();
    100             }
    101         }
    102         Collections.sort(result);
    103         return result;
    104     }
    105 
    106     public interface ConversationDataListener {
    107         public void onConversationMessagesCursorUpdated(ConversationData data, Cursor cursor,
    108                 @Nullable ConversationMessageData newestMessage, boolean isSync);
    109         public void onConversationMetadataUpdated(ConversationData data);
    110         public void closeConversation(String conversationId);
    111         public void onConversationParticipantDataLoaded(ConversationData data);
    112         public void onSubscriptionListDataLoaded(ConversationData data);
    113     }
    114 
    115     private static class ReversedCursor extends CursorWrapper {
    116         final int mCount;
    117 
    118         public ReversedCursor(final Cursor cursor) {
    119             super(cursor);
    120             mCount = cursor.getCount();
    121         }
    122 
    123         @Override
    124         public boolean moveToPosition(final int position) {
    125             return super.moveToPosition(mCount - position - 1);
    126         }
    127 
    128         @Override
    129         public int getPosition() {
    130             return mCount - super.getPosition() - 1;
    131         }
    132 
    133         @Override
    134         public boolean isAfterLast() {
    135             return super.isBeforeFirst();
    136         }
    137 
    138         @Override
    139         public boolean isBeforeFirst() {
    140             return super.isAfterLast();
    141         }
    142 
    143         @Override
    144         public boolean isFirst() {
    145             return super.isLast();
    146         }
    147 
    148         @Override
    149         public boolean isLast() {
    150             return super.isFirst();
    151         }
    152 
    153         @Override
    154         public boolean move(final int offset) {
    155             return super.move(-offset);
    156         }
    157 
    158         @Override
    159         public boolean moveToFirst() {
    160             return super.moveToLast();
    161         }
    162 
    163         @Override
    164         public boolean moveToLast() {
    165             return super.moveToFirst();
    166         }
    167 
    168         @Override
    169         public boolean moveToNext() {
    170             return super.moveToPrevious();
    171         }
    172 
    173         @Override
    174         public boolean moveToPrevious() {
    175             return super.moveToNext();
    176         }
    177     }
    178 
    179     /**
    180      * A trampoline class so that we can inherit from LoaderManager.LoaderCallbacks multiple times.
    181      */
    182     private class MetadataLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
    183         @Override
    184         public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
    185             Assert.equals(CONVERSATION_META_DATA_LOADER, id);
    186             Loader<Cursor> loader = null;
    187 
    188             final String bindingId = args.getString(BINDING_ID);
    189             // Check if data still bound to the requesting ui element
    190             if (isBound(bindingId)) {
    191                 final Uri uri =
    192                         MessagingContentProvider.buildConversationMetadataUri(mConversationId);
    193                 loader = new BoundCursorLoader(bindingId, mContext, uri,
    194                         ConversationListItemData.PROJECTION, null, null, null);
    195             } else {
    196                 LogUtil.w(TAG, "Creating messages loader after unbinding mConversationId = " +
    197                         mConversationId);
    198             }
    199             return loader;
    200         }
    201 
    202         @Override
    203         public void onLoadFinished(final Loader<Cursor> generic, final Cursor data) {
    204             final BoundCursorLoader loader = (BoundCursorLoader) generic;
    205 
    206             // Check if data still bound to the requesting ui element
    207             if (isBound(loader.getBindingId())) {
    208                 if (data.moveToNext()) {
    209                     Assert.isTrue(data.getCount() == 1);
    210                     mConversationMetadata.bind(data);
    211                     mListeners.onConversationMetadataUpdated(ConversationData.this);
    212                 } else {
    213                     // Close the conversation, no meta data means conversation was deleted
    214                     LogUtil.w(TAG, "Meta data loader returned nothing for mConversationId = " +
    215                             mConversationId);
    216                     mListeners.closeConversation(mConversationId);
    217                     // Notify the widget the conversation is deleted so it can go into its
    218                     // configure state.
    219                     WidgetConversationProvider.notifyConversationDeleted(
    220                             Factory.get().getApplicationContext(),
    221                             mConversationId);
    222                 }
    223             } else {
    224                 LogUtil.w(TAG, "Meta data loader finished after unbinding mConversationId = " +
    225                         mConversationId);
    226             }
    227         }
    228 
    229         @Override
    230         public void onLoaderReset(final Loader<Cursor> generic) {
    231             final BoundCursorLoader loader = (BoundCursorLoader) generic;
    232 
    233             // Check if data still bound to the requesting ui element
    234             if (isBound(loader.getBindingId())) {
    235                 // Clear the conversation meta data
    236                 mConversationMetadata = new ConversationListItemData();
    237                 mListeners.onConversationMetadataUpdated(ConversationData.this);
    238             } else {
    239                 LogUtil.w(TAG, "Meta data loader reset after unbinding mConversationId = " +
    240                         mConversationId);
    241             }
    242         }
    243     }
    244 
    245     /**
    246      * A trampoline class so that we can inherit from LoaderManager.LoaderCallbacks multiple times.
    247      */
    248     private class MessagesLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
    249         @Override
    250         public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
    251             Assert.equals(CONVERSATION_MESSAGES_LOADER, id);
    252             Loader<Cursor> loader = null;
    253 
    254             final String bindingId = args.getString(BINDING_ID);
    255             // Check if data still bound to the requesting ui element
    256             if (isBound(bindingId)) {
    257                 final Uri uri =
    258                         MessagingContentProvider.buildConversationMessagesUri(mConversationId);
    259                 loader = new BoundCursorLoader(bindingId, mContext, uri,
    260                         ConversationMessageData.getProjection(), null, null, null);
    261                 mLastMessageTimestamp = LAST_MESSAGE_TIMESTAMP_NaN;
    262                 mMessageCount = MESSAGE_COUNT_NaN;
    263             } else {
    264                 LogUtil.w(TAG, "Creating messages loader after unbinding mConversationId = " +
    265                         mConversationId);
    266             }
    267             return loader;
    268         }
    269 
    270         @Override
    271         public void onLoadFinished(final Loader<Cursor> generic, final Cursor rawData) {
    272             final BoundCursorLoader loader = (BoundCursorLoader) generic;
    273 
    274             // Check if data still bound to the requesting ui element
    275             if (isBound(loader.getBindingId())) {
    276                 // Check if we have a new message, or if we had a message sync.
    277                 ConversationMessageData newMessage = null;
    278                 boolean isSync = false;
    279                 Cursor data = null;
    280                 if (rawData != null) {
    281                     // Note that the cursor is sorted DESC so here we reverse it.
    282                     // This is a performance issue (improvement) for large cursors.
    283                     data = new ReversedCursor(rawData);
    284 
    285                     final int messageCountOld = mMessageCount;
    286                     mMessageCount = data.getCount();
    287                     final ConversationMessageData lastMessage = getLastMessage(data);
    288                     if (lastMessage != null) {
    289                         final long lastMessageTimestampOld = mLastMessageTimestamp;
    290                         mLastMessageTimestamp = lastMessage.getReceivedTimeStamp();
    291                         final String lastMessageIdOld = mLastMessageId;
    292                         mLastMessageId = lastMessage.getMessageId();
    293                         if (TextUtils.equals(lastMessageIdOld, mLastMessageId) &&
    294                                 messageCountOld < mMessageCount) {
    295                             // Last message stays the same (no incoming message) but message
    296                             // count increased, which means there has been a message sync.
    297                             isSync = true;
    298                         } else if (messageCountOld != MESSAGE_COUNT_NaN && // Ignore initial load
    299                                 mLastMessageTimestamp != LAST_MESSAGE_TIMESTAMP_NaN &&
    300                                 mLastMessageTimestamp > lastMessageTimestampOld) {
    301                             newMessage = lastMessage;
    302                         }
    303                     } else {
    304                         mLastMessageTimestamp = LAST_MESSAGE_TIMESTAMP_NaN;
    305                     }
    306                 } else {
    307                     mMessageCount = MESSAGE_COUNT_NaN;
    308                 }
    309 
    310                 mListeners.onConversationMessagesCursorUpdated(ConversationData.this, data,
    311                         newMessage, isSync);
    312             } else {
    313                 LogUtil.w(TAG, "Messages loader finished after unbinding mConversationId = " +
    314                         mConversationId);
    315             }
    316         }
    317 
    318         @Override
    319         public void onLoaderReset(final Loader<Cursor> generic) {
    320             final BoundCursorLoader loader = (BoundCursorLoader) generic;
    321 
    322             // Check if data still bound to the requesting ui element
    323             if (isBound(loader.getBindingId())) {
    324                 mListeners.onConversationMessagesCursorUpdated(ConversationData.this, null, null,
    325                         false);
    326                 mLastMessageTimestamp = LAST_MESSAGE_TIMESTAMP_NaN;
    327                 mMessageCount = MESSAGE_COUNT_NaN;
    328             } else {
    329                 LogUtil.w(TAG, "Messages loader reset after unbinding mConversationId = " +
    330                         mConversationId);
    331             }
    332         }
    333 
    334         private ConversationMessageData getLastMessage(final Cursor cursor) {
    335             if (cursor != null && cursor.getCount() > 0) {
    336                 final int position = cursor.getPosition();
    337                 if (cursor.moveToLast()) {
    338                     final ConversationMessageData messageData = new ConversationMessageData();
    339                     messageData.bind(cursor);
    340                     cursor.move(position);
    341                     return messageData;
    342                 }
    343             }
    344             return null;
    345         }
    346     }
    347 
    348     /**
    349      * A trampoline class so that we can inherit from LoaderManager.LoaderCallbacks multiple times.
    350      */
    351     private class ParticipantLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
    352         @Override
    353         public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
    354             Assert.equals(PARTICIPANT_LOADER, id);
    355             Loader<Cursor> loader = null;
    356 
    357             final String bindingId = args.getString(BINDING_ID);
    358             // Check if data still bound to the requesting ui element
    359             if (isBound(bindingId)) {
    360                 final Uri uri =
    361                         MessagingContentProvider.buildConversationParticipantsUri(mConversationId);
    362                 loader = new BoundCursorLoader(bindingId, mContext, uri,
    363                         ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
    364             } else {
    365                 LogUtil.w(TAG, "Creating participant loader after unbinding mConversationId = " +
    366                         mConversationId);
    367             }
    368             return loader;
    369         }
    370 
    371         @Override
    372         public void onLoadFinished(final Loader<Cursor> generic, final Cursor data) {
    373             final BoundCursorLoader loader = (BoundCursorLoader) generic;
    374 
    375             // Check if data still bound to the requesting ui element
    376             if (isBound(loader.getBindingId())) {
    377                 mParticipantData.bind(data);
    378                 mListeners.onConversationParticipantDataLoaded(ConversationData.this);
    379             } else {
    380                 LogUtil.w(TAG, "Participant loader finished after unbinding mConversationId = " +
    381                         mConversationId);
    382             }
    383         }
    384 
    385         @Override
    386         public void onLoaderReset(final Loader<Cursor> generic) {
    387             final BoundCursorLoader loader = (BoundCursorLoader) generic;
    388 
    389             // Check if data still bound to the requesting ui element
    390             if (isBound(loader.getBindingId())) {
    391                 mParticipantData.bind(null);
    392             } else {
    393                 LogUtil.w(TAG, "Participant loader reset after unbinding mConversationId = " +
    394                         mConversationId);
    395             }
    396         }
    397     }
    398 
    399     /**
    400      * A trampoline class so that we can inherit from LoaderManager.LoaderCallbacks multiple times.
    401      */
    402     private class SelfParticipantLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
    403         @Override
    404         public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
    405             Assert.equals(SELF_PARTICIPANT_LOADER, id);
    406             Loader<Cursor> loader = null;
    407 
    408             final String bindingId = args.getString(BINDING_ID);
    409             // Check if data still bound to the requesting ui element
    410             if (isBound(bindingId)) {
    411                 loader = new BoundCursorLoader(bindingId, mContext,
    412                         MessagingContentProvider.PARTICIPANTS_URI,
    413                         ParticipantData.ParticipantsQuery.PROJECTION,
    414                         ParticipantColumns.SUB_ID + " <> ?",
    415                         new String[] { String.valueOf(ParticipantData.OTHER_THAN_SELF_SUB_ID) },
    416                         null);
    417             } else {
    418                 LogUtil.w(TAG, "Creating self loader after unbinding mConversationId = " +
    419                         mConversationId);
    420             }
    421             return loader;
    422         }
    423 
    424         @Override
    425         public void onLoadFinished(final Loader<Cursor> generic, final Cursor data) {
    426             final BoundCursorLoader loader = (BoundCursorLoader) generic;
    427 
    428             // Check if data still bound to the requesting ui element
    429             if (isBound(loader.getBindingId())) {
    430                 mSelfParticipantsData.bind(data);
    431                 mSubscriptionListData.bind(mSelfParticipantsData.getSelfParticipants(true));
    432                 mListeners.onSubscriptionListDataLoaded(ConversationData.this);
    433             } else {
    434                 LogUtil.w(TAG, "Self loader finished after unbinding mConversationId = " +
    435                         mConversationId);
    436             }
    437         }
    438 
    439         @Override
    440         public void onLoaderReset(final Loader<Cursor> generic) {
    441             final BoundCursorLoader loader = (BoundCursorLoader) generic;
    442 
    443             // Check if data still bound to the requesting ui element
    444             if (isBound(loader.getBindingId())) {
    445                 mSelfParticipantsData.bind(null);
    446             } else {
    447                 LogUtil.w(TAG, "Self loader reset after unbinding mConversationId = " +
    448                         mConversationId);
    449             }
    450         }
    451     }
    452 
    453     private final ConversationDataEventDispatcher mListeners;
    454     private final MetadataLoaderCallbacks mMetadataLoaderCallbacks;
    455     private final MessagesLoaderCallbacks mMessagesLoaderCallbacks;
    456     private final ParticipantLoaderCallbacks mParticipantsLoaderCallbacks;
    457     private final SelfParticipantLoaderCallbacks mSelfParticipantLoaderCallbacks;
    458     private final Context mContext;
    459     private final String mConversationId;
    460     private final ConversationParticipantsData mParticipantData;
    461     private final SelfParticipantsData mSelfParticipantsData;
    462     private ConversationListItemData mConversationMetadata;
    463     private final SubscriptionListData mSubscriptionListData;
    464     private LoaderManager mLoaderManager;
    465     private long mLastMessageTimestamp = LAST_MESSAGE_TIMESTAMP_NaN;
    466     private int mMessageCount = MESSAGE_COUNT_NaN;
    467     private String mLastMessageId;
    468 
    469     public ConversationData(final Context context, final ConversationDataListener listener,
    470             final String conversationId) {
    471         Assert.isTrue(conversationId != null);
    472         mContext = context;
    473         mConversationId = conversationId;
    474         mMetadataLoaderCallbacks = new MetadataLoaderCallbacks();
    475         mMessagesLoaderCallbacks = new MessagesLoaderCallbacks();
    476         mParticipantsLoaderCallbacks = new ParticipantLoaderCallbacks();
    477         mSelfParticipantLoaderCallbacks = new SelfParticipantLoaderCallbacks();
    478         mParticipantData = new ConversationParticipantsData();
    479         mConversationMetadata = new ConversationListItemData();
    480         mSelfParticipantsData = new SelfParticipantsData();
    481         mSubscriptionListData = new SubscriptionListData(context);
    482 
    483         mListeners = new ConversationDataEventDispatcher();
    484         mListeners.add(listener);
    485     }
    486 
    487     @RunsOnMainThread
    488     public void addConversationDataListener(final ConversationDataListener listener) {
    489         Assert.isMainThread();
    490         mListeners.add(listener);
    491     }
    492 
    493     public String getConversationName() {
    494         return mConversationMetadata.getName();
    495     }
    496 
    497     public boolean getIsArchived() {
    498         return mConversationMetadata.getIsArchived();
    499     }
    500 
    501     public String getIcon() {
    502         return mConversationMetadata.getIcon();
    503     }
    504 
    505     public String getConversationId() {
    506         return mConversationId;
    507     }
    508 
    509     public void setFocus() {
    510         DataModel.get().setFocusedConversation(mConversationId);
    511         // As we are loading the conversation assume the user has read the messages...
    512         // Do this late though so that it doesn't get in the way of other actions
    513         BugleNotifications.markMessagesAsRead(mConversationId);
    514     }
    515 
    516     public void unsetFocus() {
    517         DataModel.get().setFocusedConversation(null);
    518     }
    519 
    520     public boolean isFocused() {
    521         return isBound() && DataModel.get().isFocusedConversation(mConversationId);
    522     }
    523 
    524     private static final int CONVERSATION_META_DATA_LOADER = 1;
    525     private static final int CONVERSATION_MESSAGES_LOADER = 2;
    526     private static final int PARTICIPANT_LOADER = 3;
    527     private static final int SELF_PARTICIPANT_LOADER = 4;
    528 
    529     public void init(final LoaderManager loaderManager,
    530             final BindingBase<ConversationData> binding) {
    531         // Remember the binding id so that loader callbacks can check if data is still bound
    532         // to same ui component
    533         final Bundle args = new Bundle();
    534         args.putString(BINDING_ID, binding.getBindingId());
    535         mLoaderManager = loaderManager;
    536         mLoaderManager.initLoader(CONVERSATION_META_DATA_LOADER, args, mMetadataLoaderCallbacks);
    537         mLoaderManager.initLoader(CONVERSATION_MESSAGES_LOADER, args, mMessagesLoaderCallbacks);
    538         mLoaderManager.initLoader(PARTICIPANT_LOADER, args, mParticipantsLoaderCallbacks);
    539         mLoaderManager.initLoader(SELF_PARTICIPANT_LOADER, args, mSelfParticipantLoaderCallbacks);
    540     }
    541 
    542     @Override
    543     protected void unregisterListeners() {
    544         mListeners.clear();
    545         // Make sure focus has moved away from this conversation
    546         // TODO: May false trigger if destroy happens after "new" conversation is focused.
    547         //        Assert.isTrue(!DataModel.get().isFocusedConversation(mConversationId));
    548 
    549         // This could be null if we bind but the caller doesn't init the BindableData
    550         if (mLoaderManager != null) {
    551             mLoaderManager.destroyLoader(CONVERSATION_META_DATA_LOADER);
    552             mLoaderManager.destroyLoader(CONVERSATION_MESSAGES_LOADER);
    553             mLoaderManager.destroyLoader(PARTICIPANT_LOADER);
    554             mLoaderManager.destroyLoader(SELF_PARTICIPANT_LOADER);
    555             mLoaderManager = null;
    556         }
    557     }
    558 
    559     /**
    560      * Gets the default self participant in the participant table (NOT the conversation's self).
    561      * This is available as soon as self participant data is loaded.
    562      */
    563     public ParticipantData getDefaultSelfParticipant() {
    564         return mSelfParticipantsData.getDefaultSelfParticipant();
    565     }
    566 
    567     public List<ParticipantData> getSelfParticipants(final boolean activeOnly) {
    568         return mSelfParticipantsData.getSelfParticipants(activeOnly);
    569     }
    570 
    571     public int getSelfParticipantsCountExcludingDefault(final boolean activeOnly) {
    572         return mSelfParticipantsData.getSelfParticipantsCountExcludingDefault(activeOnly);
    573     }
    574 
    575     public ParticipantData getSelfParticipantById(final String selfId) {
    576         return mSelfParticipantsData.getSelfParticipantById(selfId);
    577     }
    578 
    579     /**
    580      * For a 1:1 conversation return the other (not self) participant (else null)
    581      */
    582     public ParticipantData getOtherParticipant() {
    583         return mParticipantData.getOtherParticipant();
    584     }
    585 
    586     /**
    587      * Return true once the participants are loaded
    588      */
    589     public boolean getParticipantsLoaded() {
    590         return mParticipantData.isLoaded();
    591     }
    592 
    593     public void sendMessage(final BindingBase<ConversationData> binding,
    594             final MessageData message) {
    595         Assert.isTrue(TextUtils.equals(mConversationId, message.getConversationId()));
    596         Assert.isTrue(binding.getData() == this);
    597 
    598         if (!OsUtil.isAtLeastL_MR1() || message.getSelfId() == null) {
    599             InsertNewMessageAction.insertNewMessage(message);
    600         } else {
    601             final int systemDefaultSubId = PhoneUtils.getDefault().getDefaultSmsSubscriptionId();
    602             if (systemDefaultSubId != ParticipantData.DEFAULT_SELF_SUB_ID &&
    603                     mSelfParticipantsData.isDefaultSelf(message.getSelfId())) {
    604                 // Lock the sub selection to the system default SIM as soon as the user clicks on
    605                 // the send button to avoid races between this and when InsertNewMessageAction is
    606                 // actually executed on the data model thread, during which the user can potentially
    607                 // change the system default SIM in Settings.
    608                 InsertNewMessageAction.insertNewMessage(message, systemDefaultSubId);
    609             } else {
    610                 InsertNewMessageAction.insertNewMessage(message);
    611             }
    612         }
    613         // Update contacts so Frequents will reflect messaging activity.
    614         if (!getParticipantsLoaded()) {
    615             return;  // oh well, not critical
    616         }
    617         final ArrayList<String> phones = new ArrayList<>();
    618         final ArrayList<String> emails = new ArrayList<>();
    619         for (final ParticipantData participant : mParticipantData) {
    620             if (!participant.isSelf()) {
    621                 if (participant.isEmail()) {
    622                     emails.add(participant.getSendDestination());
    623                 } else {
    624                     phones.add(participant.getSendDestination());
    625                 }
    626             }
    627         }
    628 
    629         if (ContactUtil.hasReadContactsPermission()) {
    630             SafeAsyncTask.executeOnThreadPool(new Runnable() {
    631                 @Override
    632                 public void run() {
    633                     final DataUsageStatUpdater updater = new DataUsageStatUpdater(
    634                             Factory.get().getApplicationContext());
    635                     try {
    636                         if (!phones.isEmpty()) {
    637                             updater.updateWithPhoneNumber(phones);
    638                         }
    639                         if (!emails.isEmpty()) {
    640                             updater.updateWithAddress(emails);
    641                         }
    642                     } catch (final SQLiteFullException ex) {
    643                         LogUtil.w(TAG, "Unable to update contact", ex);
    644                     }
    645                 }
    646             });
    647         }
    648     }
    649 
    650     public void downloadMessage(final BindingBase<ConversationData> binding,
    651             final String messageId) {
    652         Assert.isTrue(binding.getData() == this);
    653         Assert.notNull(messageId);
    654         RedownloadMmsAction.redownloadMessage(messageId);
    655     }
    656 
    657     public void resendMessage(final BindingBase<ConversationData> binding, final String messageId) {
    658         Assert.isTrue(binding.getData() == this);
    659         Assert.notNull(messageId);
    660         ResendMessageAction.resendMessage(messageId);
    661     }
    662 
    663     public void deleteMessage(final BindingBase<ConversationData> binding, final String messageId) {
    664         Assert.isTrue(binding.getData() == this);
    665         Assert.notNull(messageId);
    666         DeleteMessageAction.deleteMessage(messageId);
    667     }
    668 
    669     public void deleteConversation(final Binding<ConversationData> binding) {
    670         Assert.isTrue(binding.getData() == this);
    671         // If possible use timestamp of last message shown to delete only messages user is aware of
    672         if (mConversationMetadata == null) {
    673             DeleteConversationAction.deleteConversation(mConversationId,
    674                     System.currentTimeMillis());
    675         } else {
    676             mConversationMetadata.deleteConversation();
    677         }
    678     }
    679 
    680     public void archiveConversation(final BindingBase<ConversationData> binding) {
    681         Assert.isTrue(binding.getData() == this);
    682         UpdateConversationArchiveStatusAction.archiveConversation(mConversationId);
    683     }
    684 
    685     public void unarchiveConversation(final BindingBase<ConversationData> binding) {
    686         Assert.isTrue(binding.getData() == this);
    687         UpdateConversationArchiveStatusAction.unarchiveConversation(mConversationId);
    688     }
    689 
    690     public ConversationParticipantsData getParticipants() {
    691         return mParticipantData;
    692     }
    693 
    694     /**
    695      * Returns a dialable phone number for the participant if we are in a 1-1 conversation.
    696      * @return the participant phone number, or null if the phone number is not valid or if there
    697      *         are more than one participant.
    698      */
    699     public String getParticipantPhoneNumber() {
    700         final ParticipantData participant = this.getOtherParticipant();
    701         if (participant != null) {
    702             final String phoneNumber = participant.getSendDestination();
    703             if (!TextUtils.isEmpty(phoneNumber) && MmsSmsUtils.isPhoneNumber(phoneNumber)) {
    704                 return phoneNumber;
    705             }
    706         }
    707         return null;
    708     }
    709 
    710     /**
    711      * Create a message to be forwarded from an existing message.
    712      */
    713     public MessageData createForwardedMessage(final ConversationMessageData message) {
    714         final MessageData forwardedMessage = new MessageData();
    715 
    716         final String originalSubject =
    717                 MmsUtils.cleanseMmsSubject(mContext.getResources(), message.getMmsSubject());
    718         if (!TextUtils.isEmpty(originalSubject)) {
    719             forwardedMessage.setMmsSubject(
    720                     mContext.getResources().getString(R.string.message_fwd, originalSubject));
    721         }
    722 
    723         for (final MessagePartData part : message.getParts()) {
    724             MessagePartData forwardedPart;
    725 
    726             // Depending on the part type, if it is text, we can directly create a text part;
    727             // if it is attachment, then we need to create a pending attachment data out of it, so
    728             // that we may persist the attachment locally in the scratch folder when the user picks
    729             // a conversation to forward to.
    730             if (part.isText()) {
    731                 forwardedPart = MessagePartData.createTextMessagePart(part.getText());
    732             } else {
    733                 final PendingAttachmentData pendingAttachmentData = PendingAttachmentData
    734                         .createPendingAttachmentData(part.getContentType(), part.getContentUri());
    735                 forwardedPart = pendingAttachmentData;
    736             }
    737             forwardedMessage.addPart(forwardedPart);
    738         }
    739         return forwardedMessage;
    740     }
    741 
    742     public int getNumberOfParticipantsExcludingSelf() {
    743         return mParticipantData.getNumberOfParticipantsExcludingSelf();
    744     }
    745 
    746     /**
    747      * Returns {@link com.android.messaging.datamodel.data.SubscriptionListData
    748      * .SubscriptionListEntry} for a given self participant so UI can display SIM-related info
    749      * (icon, name etc.) for multi-SIM.
    750      */
    751     public SubscriptionListEntry getSubscriptionEntryForSelfParticipant(
    752             final String selfParticipantId, final boolean excludeDefault) {
    753         return getSubscriptionEntryForSelfParticipant(selfParticipantId, excludeDefault,
    754                 mSubscriptionListData, mSelfParticipantsData);
    755     }
    756 
    757     /**
    758      * Returns {@link com.android.messaging.datamodel.data.SubscriptionListData
    759      * .SubscriptionListEntry} for a given self participant so UI can display SIM-related info
    760      * (icon, name etc.) for multi-SIM.
    761      */
    762     public static SubscriptionListEntry getSubscriptionEntryForSelfParticipant(
    763             final String selfParticipantId, final boolean excludeDefault,
    764             final SubscriptionListData subscriptionListData,
    765             final SelfParticipantsData selfParticipantsData) {
    766         // SIM indicators are shown in the UI only if:
    767         // 1. Framework has MSIM support AND
    768         // 2. The device has had multiple *active* subscriptions. AND
    769         // 3. The message's subscription is active.
    770         if (OsUtil.isAtLeastL_MR1() &&
    771                 selfParticipantsData.getSelfParticipantsCountExcludingDefault(true) > 1) {
    772             return subscriptionListData.getActiveSubscriptionEntryBySelfId(selfParticipantId,
    773                     excludeDefault);
    774         }
    775         return null;
    776     }
    777 
    778     public SubscriptionListData getSubscriptionListData() {
    779         return mSubscriptionListData;
    780     }
    781 
    782     /**
    783      * A dummy implementation of {@link ConversationDataListener} so that subclasses may opt to
    784      * implement some, but not all, of the interface methods.
    785      */
    786     public static class SimpleConversationDataListener implements ConversationDataListener {
    787 
    788         @Override
    789         public void onConversationMessagesCursorUpdated(final ConversationData data, final Cursor cursor,
    790                 @Nullable
    791                         final
    792                 ConversationMessageData newestMessage, final boolean isSync) {}
    793 
    794         @Override
    795         public void onConversationMetadataUpdated(final ConversationData data) {}
    796 
    797         @Override
    798         public void closeConversation(final String conversationId) {}
    799 
    800         @Override
    801         public void onConversationParticipantDataLoaded(final ConversationData data) {}
    802 
    803         @Override
    804         public void onSubscriptionListDataLoaded(final ConversationData data) {}
    805 
    806     }
    807 
    808     private class ConversationDataEventDispatcher
    809             extends ArrayList<ConversationDataListener>
    810             implements ConversationDataListener {
    811 
    812         @Override
    813         public void onConversationMessagesCursorUpdated(final ConversationData data, final Cursor cursor,
    814                 @Nullable
    815                         final ConversationMessageData newestMessage, final boolean isSync) {
    816             for (final ConversationDataListener listener : this) {
    817                 listener.onConversationMessagesCursorUpdated(data, cursor, newestMessage, isSync);
    818             }
    819         }
    820 
    821         @Override
    822         public void onConversationMetadataUpdated(final ConversationData data) {
    823             for (final ConversationDataListener listener : this) {
    824                 listener.onConversationMetadataUpdated(data);
    825             }
    826         }
    827 
    828         @Override
    829         public void closeConversation(final String conversationId) {
    830             for (final ConversationDataListener listener : this) {
    831                 listener.closeConversation(conversationId);
    832             }
    833         }
    834 
    835         @Override
    836         public void onConversationParticipantDataLoaded(final ConversationData data) {
    837             for (final ConversationDataListener listener : this) {
    838                 listener.onConversationParticipantDataLoaded(data);
    839             }
    840         }
    841 
    842         @Override
    843         public void onSubscriptionListDataLoaded(final ConversationData data) {
    844             for (final ConversationDataListener listener : this) {
    845                 listener.onSubscriptionListDataLoaded(data);
    846             }
    847         }
    848     }
    849 }
    850