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 androidx.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      */
getPositions(final String conversationId, final List<Long> ids)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 {
onConversationMessagesCursorUpdated(ConversationData data, Cursor cursor, @Nullable ConversationMessageData newestMessage, boolean isSync)107         public void onConversationMessagesCursorUpdated(ConversationData data, Cursor cursor,
108                 @Nullable ConversationMessageData newestMessage, boolean isSync);
onConversationMetadataUpdated(ConversationData data)109         public void onConversationMetadataUpdated(ConversationData data);
closeConversation(String conversationId)110         public void closeConversation(String conversationId);
onConversationParticipantDataLoaded(ConversationData data)111         public void onConversationParticipantDataLoaded(ConversationData data);
onSubscriptionListDataLoaded(ConversationData data)112         public void onSubscriptionListDataLoaded(ConversationData data);
113     }
114 
115     private static class ReversedCursor extends CursorWrapper {
116         final int mCount;
117 
ReversedCursor(final Cursor cursor)118         public ReversedCursor(final Cursor cursor) {
119             super(cursor);
120             mCount = cursor.getCount();
121         }
122 
123         @Override
moveToPosition(final int position)124         public boolean moveToPosition(final int position) {
125             return super.moveToPosition(mCount - position - 1);
126         }
127 
128         @Override
getPosition()129         public int getPosition() {
130             return mCount - super.getPosition() - 1;
131         }
132 
133         @Override
isAfterLast()134         public boolean isAfterLast() {
135             return super.isBeforeFirst();
136         }
137 
138         @Override
isBeforeFirst()139         public boolean isBeforeFirst() {
140             return super.isAfterLast();
141         }
142 
143         @Override
isFirst()144         public boolean isFirst() {
145             return super.isLast();
146         }
147 
148         @Override
isLast()149         public boolean isLast() {
150             return super.isFirst();
151         }
152 
153         @Override
move(final int offset)154         public boolean move(final int offset) {
155             return super.move(-offset);
156         }
157 
158         @Override
moveToFirst()159         public boolean moveToFirst() {
160             return super.moveToLast();
161         }
162 
163         @Override
moveToLast()164         public boolean moveToLast() {
165             return super.moveToFirst();
166         }
167 
168         @Override
moveToNext()169         public boolean moveToNext() {
170             return super.moveToPrevious();
171         }
172 
173         @Override
moveToPrevious()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
onCreateLoader(final int id, final Bundle args)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
onLoadFinished(final Loader<Cursor> generic, final Cursor data)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
onLoaderReset(final Loader<Cursor> generic)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
onCreateLoader(final int id, final Bundle args)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
onLoadFinished(final Loader<Cursor> generic, final Cursor rawData)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
onLoaderReset(final Loader<Cursor> generic)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 
getLastMessage(final Cursor cursor)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
onCreateLoader(final int id, final Bundle args)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
onLoadFinished(final Loader<Cursor> generic, final Cursor data)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
onLoaderReset(final Loader<Cursor> generic)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
onCreateLoader(final int id, final Bundle args)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
onLoadFinished(final Loader<Cursor> generic, final Cursor data)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
onLoaderReset(final Loader<Cursor> generic)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 
ConversationData(final Context context, final ConversationDataListener listener, final String conversationId)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
addConversationDataListener(final ConversationDataListener listener)488     public void addConversationDataListener(final ConversationDataListener listener) {
489         Assert.isMainThread();
490         mListeners.add(listener);
491     }
492 
getConversationName()493     public String getConversationName() {
494         return mConversationMetadata.getName();
495     }
496 
getIsArchived()497     public boolean getIsArchived() {
498         return mConversationMetadata.getIsArchived();
499     }
500 
getIcon()501     public String getIcon() {
502         return mConversationMetadata.getIcon();
503     }
504 
getConversationId()505     public String getConversationId() {
506         return mConversationId;
507     }
508 
setFocus()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 
unsetFocus()516     public void unsetFocus() {
517         DataModel.get().setFocusedConversation(null);
518     }
519 
isFocused()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 
init(final LoaderManager loaderManager, final BindingBase<ConversationData> binding)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
unregisterListeners()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      */
getDefaultSelfParticipant()563     public ParticipantData getDefaultSelfParticipant() {
564         return mSelfParticipantsData.getDefaultSelfParticipant();
565     }
566 
getSelfParticipants(final boolean activeOnly)567     public List<ParticipantData> getSelfParticipants(final boolean activeOnly) {
568         return mSelfParticipantsData.getSelfParticipants(activeOnly);
569     }
570 
getSelfParticipantsCountExcludingDefault(final boolean activeOnly)571     public int getSelfParticipantsCountExcludingDefault(final boolean activeOnly) {
572         return mSelfParticipantsData.getSelfParticipantsCountExcludingDefault(activeOnly);
573     }
574 
getSelfParticipantById(final String selfId)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      */
getOtherParticipant()582     public ParticipantData getOtherParticipant() {
583         return mParticipantData.getOtherParticipant();
584     }
585 
586     /**
587      * Return true once the participants are loaded
588      */
getParticipantsLoaded()589     public boolean getParticipantsLoaded() {
590         return mParticipantData.isLoaded();
591     }
592 
sendMessage(final BindingBase<ConversationData> binding, final MessageData message)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 
downloadMessage(final BindingBase<ConversationData> binding, final String messageId)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 
resendMessage(final BindingBase<ConversationData> binding, final String messageId)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 
deleteMessage(final BindingBase<ConversationData> binding, final String messageId)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 
deleteConversation(final Binding<ConversationData> binding)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 
archiveConversation(final BindingBase<ConversationData> binding)680     public void archiveConversation(final BindingBase<ConversationData> binding) {
681         Assert.isTrue(binding.getData() == this);
682         UpdateConversationArchiveStatusAction.archiveConversation(mConversationId);
683     }
684 
unarchiveConversation(final BindingBase<ConversationData> binding)685     public void unarchiveConversation(final BindingBase<ConversationData> binding) {
686         Assert.isTrue(binding.getData() == this);
687         UpdateConversationArchiveStatusAction.unarchiveConversation(mConversationId);
688     }
689 
getParticipants()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      */
getParticipantPhoneNumber()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      */
createForwardedMessage(final ConversationMessageData message)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 
getNumberOfParticipantsExcludingSelf()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      */
getSubscriptionEntryForSelfParticipant( final String selfParticipantId, final boolean excludeDefault)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      */
getSubscriptionEntryForSelfParticipant( final String selfParticipantId, final boolean excludeDefault, final SubscriptionListData subscriptionListData, final SelfParticipantsData selfParticipantsData)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 
getSubscriptionListData()778     public SubscriptionListData getSubscriptionListData() {
779         return mSubscriptionListData;
780     }
781 
782     /**
783      * A placeholder implementation of {@link ConversationDataListener} so that subclasses may opt
784      * to implement some, but not all, of the interface methods.
785      */
786     public static class SimpleConversationDataListener implements ConversationDataListener {
787 
788         @Override
onConversationMessagesCursorUpdated(final ConversationData data, final Cursor cursor, @Nullable final ConversationMessageData newestMessage, final boolean isSync)789         public void onConversationMessagesCursorUpdated(final ConversationData data, final Cursor cursor,
790                 @Nullable
791                         final
792                 ConversationMessageData newestMessage, final boolean isSync) {}
793 
794         @Override
onConversationMetadataUpdated(final ConversationData data)795         public void onConversationMetadataUpdated(final ConversationData data) {}
796 
797         @Override
closeConversation(final String conversationId)798         public void closeConversation(final String conversationId) {}
799 
800         @Override
onConversationParticipantDataLoaded(final ConversationData data)801         public void onConversationParticipantDataLoaded(final ConversationData data) {}
802 
803         @Override
onSubscriptionListDataLoaded(final ConversationData data)804         public void onSubscriptionListDataLoaded(final ConversationData data) {}
805 
806     }
807 
808     private class ConversationDataEventDispatcher
809             extends ArrayList<ConversationDataListener>
810             implements ConversationDataListener {
811 
812         @Override
onConversationMessagesCursorUpdated(final ConversationData data, final Cursor cursor, @Nullable final ConversationMessageData newestMessage, final boolean isSync)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
onConversationMetadataUpdated(final ConversationData data)822         public void onConversationMetadataUpdated(final ConversationData data) {
823             for (final ConversationDataListener listener : this) {
824                 listener.onConversationMetadataUpdated(data);
825             }
826         }
827 
828         @Override
closeConversation(final String conversationId)829         public void closeConversation(final String conversationId) {
830             for (final ConversationDataListener listener : this) {
831                 listener.closeConversation(conversationId);
832             }
833         }
834 
835         @Override
onConversationParticipantDataLoaded(final ConversationData data)836         public void onConversationParticipantDataLoaded(final ConversationData data) {
837             for (final ConversationDataListener listener : this) {
838                 listener.onConversationParticipantDataLoaded(data);
839             }
840         }
841 
842         @Override
onSubscriptionListDataLoaded(final ConversationData data)843         public void onSubscriptionListDataLoaded(final ConversationData data) {
844             for (final ConversationDataListener listener : this) {
845                 listener.onSubscriptionListDataLoaded(data);
846             }
847         }
848     }
849 }
850