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