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