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.net.Uri; 20 import android.text.TextUtils; 21 22 import com.android.messaging.datamodel.MessageTextStats; 23 import com.android.messaging.datamodel.action.ReadDraftDataAction; 24 import com.android.messaging.datamodel.action.ReadDraftDataAction.ReadDraftDataActionListener; 25 import com.android.messaging.datamodel.action.ReadDraftDataAction.ReadDraftDataActionMonitor; 26 import com.android.messaging.datamodel.action.WriteDraftMessageAction; 27 import com.android.messaging.datamodel.binding.BindableData; 28 import com.android.messaging.datamodel.binding.Binding; 29 import com.android.messaging.datamodel.binding.BindingBase; 30 import com.android.messaging.sms.MmsConfig; 31 import com.android.messaging.sms.MmsSmsUtils; 32 import com.android.messaging.sms.MmsUtils; 33 import com.android.messaging.util.Assert; 34 import com.android.messaging.util.Assert.DoesNotRunOnMainThread; 35 import com.android.messaging.util.Assert.RunsOnMainThread; 36 import com.android.messaging.util.BugleGservices; 37 import com.android.messaging.util.BugleGservicesKeys; 38 import com.android.messaging.util.LogUtil; 39 import com.android.messaging.util.PhoneUtils; 40 import com.android.messaging.util.SafeAsyncTask; 41 42 import java.util.ArrayList; 43 import java.util.Collection; 44 import java.util.Collections; 45 import java.util.Iterator; 46 import java.util.List; 47 import java.util.Set; 48 49 public class DraftMessageData extends BindableData implements ReadDraftDataActionListener { 50 51 /** 52 * Interface for DraftMessageData listeners 53 */ 54 public interface DraftMessageDataListener { 55 @RunsOnMainThread onDraftChanged(DraftMessageData data, int changeFlags)56 void onDraftChanged(DraftMessageData data, int changeFlags); 57 58 @RunsOnMainThread onDraftAttachmentLimitReached(DraftMessageData data)59 void onDraftAttachmentLimitReached(DraftMessageData data); 60 61 @RunsOnMainThread onDraftAttachmentLoadFailed()62 void onDraftAttachmentLoadFailed(); 63 } 64 65 /** 66 * Interface for providing subscription-related data to DraftMessageData 67 */ 68 public interface DraftMessageSubscriptionDataProvider { getConversationSelfSubId()69 int getConversationSelfSubId(); 70 } 71 72 // Flags sent to onDraftChanged to help the receiver limit the amount of work done 73 public static int ATTACHMENTS_CHANGED = 0x0001; 74 public static int MESSAGE_TEXT_CHANGED = 0x0002; 75 public static int MESSAGE_SUBJECT_CHANGED = 0x0004; 76 // Whether the self participant data has been loaded 77 public static int SELF_CHANGED = 0x0008; 78 public static int ALL_CHANGED = 0x00FF; 79 // ALL_CHANGED intentionally doesn't include WIDGET_CHANGED. ConversationFragment needs to 80 // be notified if the draft it is looking at is changed externally (by a desktop widget) so it 81 // can reload the draft. 82 public static int WIDGET_CHANGED = 0x0100; 83 84 private final String mConversationId; 85 private ReadDraftDataActionMonitor mMonitor; 86 private final DraftMessageDataEventDispatcher mListeners; 87 private DraftMessageSubscriptionDataProvider mSubscriptionDataProvider; 88 89 private boolean mIncludeEmailAddress; 90 private boolean mIsGroupConversation; 91 private String mMessageText; 92 private String mMessageSubject; 93 private String mSelfId; 94 private MessageTextStats mMessageTextStats; 95 private boolean mSending; 96 97 /** Keeps track of completed attachments in the message draft. This data is persisted to db */ 98 private final List<MessagePartData> mAttachments; 99 100 /** A read-only wrapper on mAttachments surfaced to the UI layer for rendering */ 101 private final List<MessagePartData> mReadOnlyAttachments; 102 103 /** Keeps track of pending attachments that are being loaded. The pending attachments are 104 * transient, because they are not persisted to the database and are dropped once we go 105 * to the background (after the UI calls saveToStorage) */ 106 private final List<PendingAttachmentData> mPendingAttachments; 107 108 /** A read-only wrapper on mPendingAttachments surfaced to the UI layer for rendering */ 109 private final List<PendingAttachmentData> mReadOnlyPendingAttachments; 110 111 /** Is the current draft a cached copy of what's been saved to the database. If so, we 112 * may skip loading from database if we are still bound */ 113 private boolean mIsDraftCachedCopy; 114 115 /** Whether we are currently asynchronously validating the draft before sending. */ 116 private CheckDraftForSendTask mCheckDraftForSendTask; 117 DraftMessageData(final String conversationId)118 public DraftMessageData(final String conversationId) { 119 mConversationId = conversationId; 120 mAttachments = new ArrayList<MessagePartData>(); 121 mReadOnlyAttachments = Collections.unmodifiableList(mAttachments); 122 mPendingAttachments = new ArrayList<PendingAttachmentData>(); 123 mReadOnlyPendingAttachments = Collections.unmodifiableList(mPendingAttachments); 124 mListeners = new DraftMessageDataEventDispatcher(); 125 mMessageTextStats = new MessageTextStats(); 126 } 127 addListener(final DraftMessageDataListener listener)128 public void addListener(final DraftMessageDataListener listener) { 129 mListeners.add(listener); 130 } 131 setSubscriptionDataProvider(final DraftMessageSubscriptionDataProvider provider)132 public void setSubscriptionDataProvider(final DraftMessageSubscriptionDataProvider provider) { 133 mSubscriptionDataProvider = provider; 134 } 135 updateFromMessageData(final MessageData message, final String bindingId)136 public void updateFromMessageData(final MessageData message, final String bindingId) { 137 // New attachments have arrived - only update if the user hasn't already edited 138 Assert.notNull(bindingId); 139 // The draft is now synced with actual MessageData and no longer a cached copy. 140 mIsDraftCachedCopy = false; 141 // Do not use the loaded draft if the user began composing a message before the draft loaded 142 // During config changes (orientation), the text fields preserve their data, so allow them 143 // to be the same and still consider the draft unchanged by the user 144 if (isDraftEmpty() || (TextUtils.equals(mMessageText, message.getMessageText()) && 145 TextUtils.equals(mMessageSubject, message.getMmsSubject()) && 146 mAttachments.isEmpty())) { 147 // No need to clear as just checked it was empty or a subset 148 setMessageText(message.getMessageText(), false /* notify */); 149 setMessageSubject(message.getMmsSubject(), false /* notify */); 150 for (final MessagePartData part : message.getParts()) { 151 if (part.isAttachment() && getAttachmentCount() >= getAttachmentLimit()) { 152 dispatchAttachmentLimitReached(); 153 break; 154 } 155 156 if (part instanceof PendingAttachmentData) { 157 // This is a pending attachment data from share intent (e.g. an shared image 158 // that we need to persist locally). 159 final PendingAttachmentData data = (PendingAttachmentData) part; 160 Assert.equals(PendingAttachmentData.STATE_PENDING, data.getCurrentState()); 161 addOnePendingAttachmentNoNotify(data, bindingId); 162 } else if (part.isAttachment()) { 163 addOneAttachmentNoNotify(part); 164 } 165 } 166 dispatchChanged(ALL_CHANGED); 167 } else { 168 // The user has started a new message so we throw out the draft message data if there 169 // is one but we also loaded the self metadata and need to let our listeners know. 170 dispatchChanged(SELF_CHANGED); 171 } 172 } 173 174 /** 175 * Create a MessageData object containing a copy of all the parts in this DraftMessageData. 176 * 177 * @param clearLocalCopy whether we should clear out the in-memory copy of the parts. If we 178 * are simply pausing/resuming and not sending the message, then we can keep 179 * @return the MessageData for the draft, null if self id is not set 180 */ createMessageWithCurrentAttachments(final boolean clearLocalCopy)181 public MessageData createMessageWithCurrentAttachments(final boolean clearLocalCopy) { 182 MessageData message = null; 183 if (getIsMms()) { 184 message = MessageData.createDraftMmsMessage(mConversationId, mSelfId, 185 mMessageText, mMessageSubject); 186 for (final MessagePartData attachment : mAttachments) { 187 message.addPart(attachment); 188 } 189 } else { 190 message = MessageData.createDraftSmsMessage(mConversationId, mSelfId, 191 mMessageText); 192 } 193 194 if (clearLocalCopy) { 195 // The message now owns all the attachments and the text... 196 clearLocalDraftCopy(); 197 dispatchChanged(ALL_CHANGED); 198 } else { 199 // The draft message becomes a cached copy for UI. 200 mIsDraftCachedCopy = true; 201 } 202 return message; 203 } 204 clearLocalDraftCopy()205 private void clearLocalDraftCopy() { 206 mIsDraftCachedCopy = false; 207 mAttachments.clear(); 208 setMessageText(""); 209 setMessageSubject(""); 210 } 211 getConversationId()212 public String getConversationId() { 213 return mConversationId; 214 } 215 getMessageText()216 public String getMessageText() { 217 return mMessageText; 218 } 219 getMessageSubject()220 public String getMessageSubject() { 221 return mMessageSubject; 222 } 223 getIsMms()224 public boolean getIsMms() { 225 final int selfSubId = getSelfSubId(); 226 return MmsSmsUtils.getRequireMmsForEmailAddress(mIncludeEmailAddress, selfSubId) || 227 (mIsGroupConversation && MmsUtils.groupMmsEnabled(selfSubId)) || 228 mMessageTextStats.getMessageLengthRequiresMms() || !mAttachments.isEmpty() || 229 !TextUtils.isEmpty(mMessageSubject); 230 } 231 getIsGroupMmsConversation()232 public boolean getIsGroupMmsConversation() { 233 return getIsMms() && mIsGroupConversation; 234 } 235 getSelfId()236 public String getSelfId() { 237 return mSelfId; 238 } 239 getNumMessagesToBeSent()240 public int getNumMessagesToBeSent() { 241 return mMessageTextStats.getNumMessagesToBeSent(); 242 } 243 getCodePointsRemainingInCurrentMessage()244 public int getCodePointsRemainingInCurrentMessage() { 245 return mMessageTextStats.getCodePointsRemainingInCurrentMessage(); 246 } 247 getSelfSubId()248 public int getSelfSubId() { 249 return mSubscriptionDataProvider == null ? ParticipantData.DEFAULT_SELF_SUB_ID : 250 mSubscriptionDataProvider.getConversationSelfSubId(); 251 } 252 setMessageText(final String messageText, final boolean notify)253 private void setMessageText(final String messageText, final boolean notify) { 254 mMessageText = messageText; 255 mMessageTextStats.updateMessageTextStats(getSelfSubId(), mMessageText); 256 if (notify) { 257 dispatchChanged(MESSAGE_TEXT_CHANGED); 258 } 259 } 260 setMessageSubject(final String subject, final boolean notify)261 private void setMessageSubject(final String subject, final boolean notify) { 262 mMessageSubject = subject; 263 if (notify) { 264 dispatchChanged(MESSAGE_SUBJECT_CHANGED); 265 } 266 } 267 setMessageText(final String messageText)268 public void setMessageText(final String messageText) { 269 setMessageText(messageText, false); 270 } 271 setMessageSubject(final String subject)272 public void setMessageSubject(final String subject) { 273 setMessageSubject(subject, false); 274 } 275 addAttachments(final Collection<? extends MessagePartData> attachments)276 public void addAttachments(final Collection<? extends MessagePartData> attachments) { 277 // If the incoming attachments contains a single-only attachment, we need to clear 278 // the existing attachments. 279 for (final MessagePartData data : attachments) { 280 if (data.isSinglePartOnly()) { 281 // clear any existing attachments because the attachment we're adding can only 282 // exist by itself. 283 destroyAttachments(); 284 break; 285 } 286 } 287 // If the existing attachments contain a single-only attachment, we need to clear the 288 // existing attachments to make room for the incoming attachment. 289 for (final MessagePartData data : mAttachments) { 290 if (data.isSinglePartOnly()) { 291 // clear any existing attachments because the single attachment can only exist 292 // by itself 293 destroyAttachments(); 294 break; 295 } 296 } 297 // If any of the pending attachments contain a single-only attachment, we need to clear the 298 // existing attachments to make room for the incoming attachment. 299 for (final MessagePartData data : mPendingAttachments) { 300 if (data.isSinglePartOnly()) { 301 // clear any existing attachments because the single attachment can only exist 302 // by itself 303 destroyAttachments(); 304 break; 305 } 306 } 307 308 boolean reachedLimit = false; 309 for (final MessagePartData data : attachments) { 310 // Don't break out of loop even if limit has been reached so we can destroy all 311 // of the over-limit attachments. 312 reachedLimit |= addOneAttachmentNoNotify(data); 313 } 314 if (reachedLimit) { 315 dispatchAttachmentLimitReached(); 316 } 317 dispatchChanged(ATTACHMENTS_CHANGED); 318 } 319 containsAttachment(final Uri contentUri)320 public boolean containsAttachment(final Uri contentUri) { 321 for (final MessagePartData existingAttachment : mAttachments) { 322 if (existingAttachment.getContentUri().equals(contentUri)) { 323 return true; 324 } 325 } 326 327 for (final PendingAttachmentData pendingAttachment : mPendingAttachments) { 328 if (pendingAttachment.getContentUri().equals(contentUri)) { 329 return true; 330 } 331 } 332 return false; 333 } 334 335 /** 336 * Try to add one attachment to the attachment list, while guarding against duplicates and 337 * going over the limit. 338 * @return true if the attachment limit was reached, false otherwise 339 */ addOneAttachmentNoNotify(final MessagePartData attachment)340 private boolean addOneAttachmentNoNotify(final MessagePartData attachment) { 341 Assert.isTrue(attachment.isAttachment()); 342 // Check duplication. 343 for (final MessagePartData existingAttachment : mAttachments) { 344 if (existingAttachment.getContentUri().equals(attachment.getContentUri())) { 345 // Destroy existing attachment and replace with new attachment instead of destroying 346 // new one so that mSelectedImages in GalleryGridView could be maintained correctly. 347 mAttachments.remove(existingAttachment); 348 existingAttachment.destroyAsync(); 349 addAttachment(attachment, null /*pendingAttachment*/); 350 return false; 351 } 352 } 353 354 final boolean reachedLimit = getAttachmentCount() >= getAttachmentLimit(); 355 if (reachedLimit) { 356 // Never go over the limit. 357 attachment.destroyAsync(); 358 return true; 359 } else { 360 addAttachment(attachment, null /*pendingAttachment*/); 361 return false; 362 } 363 } 364 addAttachment(final MessagePartData attachment, final PendingAttachmentData pendingAttachment)365 private void addAttachment(final MessagePartData attachment, 366 final PendingAttachmentData pendingAttachment) { 367 if (attachment != null && attachment.isSinglePartOnly()) { 368 // clear any existing attachments because the attachment we're adding can only 369 // exist by itself. 370 destroyAttachments(); 371 } 372 if (pendingAttachment != null && pendingAttachment.isSinglePartOnly()) { 373 // clear any existing attachments because the attachment we're adding can only 374 // exist by itself. 375 destroyAttachments(); 376 } 377 // If the existing attachments contain a single-only attachment, we need to clear the 378 // existing attachments to make room for the incoming attachment. 379 for (final MessagePartData data : mAttachments) { 380 if (data.isSinglePartOnly()) { 381 // clear any existing attachments because the single attachment can only exist 382 // by itself 383 destroyAttachments(); 384 break; 385 } 386 } 387 // If any of the pending attachments contain a single-only attachment, we need to clear the 388 // existing attachments to make room for the incoming attachment. 389 for (final MessagePartData data : mPendingAttachments) { 390 if (data.isSinglePartOnly()) { 391 // clear any existing attachments because the single attachment can only exist 392 // by itself 393 destroyAttachments(); 394 break; 395 } 396 } 397 if (attachment != null) { 398 mAttachments.add(attachment); 399 } else if (pendingAttachment != null) { 400 mPendingAttachments.add(pendingAttachment); 401 } 402 } 403 addPendingAttachment(final PendingAttachmentData pendingAttachment, final BindingBase<DraftMessageData> binding)404 public void addPendingAttachment(final PendingAttachmentData pendingAttachment, 405 final BindingBase<DraftMessageData> binding) { 406 final boolean reachedLimit = addOnePendingAttachmentNoNotify(pendingAttachment, 407 binding.getBindingId()); 408 if (reachedLimit) { 409 dispatchAttachmentLimitReached(); 410 } 411 dispatchChanged(ATTACHMENTS_CHANGED); 412 } 413 414 /** 415 * Try to add one pending attachment, while guarding against duplicates and 416 * going over the limit. 417 * @return true if the attachment limit was reached, false otherwise 418 */ addOnePendingAttachmentNoNotify(final PendingAttachmentData pendingAttachment, final String bindingId)419 private boolean addOnePendingAttachmentNoNotify(final PendingAttachmentData pendingAttachment, 420 final String bindingId) { 421 final boolean reachedLimit = getAttachmentCount() >= getAttachmentLimit(); 422 if (reachedLimit || containsAttachment(pendingAttachment.getContentUri())) { 423 // Never go over the limit. Never add duplicated attachments. 424 pendingAttachment.destroyAsync(); 425 return reachedLimit; 426 } else { 427 Assert.isTrue(!mPendingAttachments.contains(pendingAttachment)); 428 Assert.equals(PendingAttachmentData.STATE_PENDING, pendingAttachment.getCurrentState()); 429 addAttachment(null /*attachment*/, pendingAttachment); 430 431 pendingAttachment.loadAttachmentForDraft(this, bindingId); 432 return false; 433 } 434 } 435 setSelfId(final String selfId, final boolean notify)436 public void setSelfId(final String selfId, final boolean notify) { 437 LogUtil.d(LogUtil.BUGLE_TAG, "DraftMessageData: set selfId=" + selfId 438 + " for conversationId=" + mConversationId); 439 mSelfId = selfId; 440 if (notify) { 441 dispatchChanged(SELF_CHANGED); 442 } 443 } 444 hasAttachments()445 public boolean hasAttachments() { 446 return !mAttachments.isEmpty(); 447 } 448 hasPendingAttachments()449 public boolean hasPendingAttachments() { 450 return !mPendingAttachments.isEmpty(); 451 } 452 getAttachmentCount()453 private int getAttachmentCount() { 454 return mAttachments.size() + mPendingAttachments.size(); 455 } 456 getVideoAttachmentCount()457 private int getVideoAttachmentCount() { 458 int count = 0; 459 for (MessagePartData part : mAttachments) { 460 if (part.isVideo()) { 461 count++; 462 } 463 } 464 for (MessagePartData part : mPendingAttachments) { 465 if (part.isVideo()) { 466 count++; 467 } 468 } 469 return count; 470 } 471 getAttachmentLimit()472 private int getAttachmentLimit() { 473 return BugleGservices.get().getInt( 474 BugleGservicesKeys.MMS_ATTACHMENT_LIMIT, 475 BugleGservicesKeys.MMS_ATTACHMENT_LIMIT_DEFAULT); 476 } 477 removeAttachment(final MessagePartData attachment)478 public void removeAttachment(final MessagePartData attachment) { 479 for (final MessagePartData existingAttachment : mAttachments) { 480 if (existingAttachment.getContentUri().equals(attachment.getContentUri())) { 481 mAttachments.remove(existingAttachment); 482 existingAttachment.destroyAsync(); 483 dispatchChanged(ATTACHMENTS_CHANGED); 484 break; 485 } 486 } 487 } 488 removeExistingAttachments(final Set<MessagePartData> attachmentsToRemove)489 public void removeExistingAttachments(final Set<MessagePartData> attachmentsToRemove) { 490 boolean removed = false; 491 final Iterator<MessagePartData> iterator = mAttachments.iterator(); 492 while (iterator.hasNext()) { 493 final MessagePartData existingAttachment = iterator.next(); 494 if (attachmentsToRemove.contains(existingAttachment)) { 495 iterator.remove(); 496 existingAttachment.destroyAsync(); 497 removed = true; 498 } 499 } 500 501 if (removed) { 502 dispatchChanged(ATTACHMENTS_CHANGED); 503 } 504 } 505 removePendingAttachment(final PendingAttachmentData pendingAttachment)506 public void removePendingAttachment(final PendingAttachmentData pendingAttachment) { 507 for (final PendingAttachmentData existingAttachment : mPendingAttachments) { 508 if (existingAttachment.getContentUri().equals(pendingAttachment.getContentUri())) { 509 mPendingAttachments.remove(pendingAttachment); 510 pendingAttachment.destroyAsync(); 511 dispatchChanged(ATTACHMENTS_CHANGED); 512 break; 513 } 514 } 515 } 516 updatePendingAttachment(final MessagePartData updatedAttachment, final PendingAttachmentData pendingAttachment)517 public void updatePendingAttachment(final MessagePartData updatedAttachment, 518 final PendingAttachmentData pendingAttachment) { 519 for (final PendingAttachmentData existingAttachment : mPendingAttachments) { 520 if (existingAttachment.getContentUri().equals(pendingAttachment.getContentUri())) { 521 mPendingAttachments.remove(pendingAttachment); 522 if (pendingAttachment.isSinglePartOnly()) { 523 updatedAttachment.setSinglePartOnly(true); 524 } 525 mAttachments.add(updatedAttachment); 526 dispatchChanged(ATTACHMENTS_CHANGED); 527 return; 528 } 529 } 530 531 // If we are here, this means the pending attachment has been dropped before the task 532 // to load it was completed. In this case destroy the temporarily staged file since it 533 // is no longer needed. 534 updatedAttachment.destroyAsync(); 535 } 536 537 /** 538 * Remove the attachments from the draft and notify any listeners. 539 * @param flags typically this will be ATTACHMENTS_CHANGED. When attachments are cleared in a 540 * widget, flags will also contain WIDGET_CHANGED. 541 */ clearAttachments(final int flags)542 public void clearAttachments(final int flags) { 543 destroyAttachments(); 544 dispatchChanged(flags); 545 } 546 getReadOnlyAttachments()547 public List<MessagePartData> getReadOnlyAttachments() { 548 return mReadOnlyAttachments; 549 } 550 getReadOnlyPendingAttachments()551 public List<PendingAttachmentData> getReadOnlyPendingAttachments() { 552 return mReadOnlyPendingAttachments; 553 } 554 loadFromStorage(final BindingBase<DraftMessageData> binding, final MessageData optionalIncomingDraft, boolean clearLocalDraft)555 public boolean loadFromStorage(final BindingBase<DraftMessageData> binding, 556 final MessageData optionalIncomingDraft, boolean clearLocalDraft) { 557 LogUtil.d(LogUtil.BUGLE_TAG, "DraftMessageData: " 558 + (optionalIncomingDraft == null ? "loading" : "setting") 559 + " for conversationId=" + mConversationId); 560 if (clearLocalDraft) { 561 clearLocalDraftCopy(); 562 } 563 final boolean isDraftCachedCopy = mIsDraftCachedCopy; 564 mIsDraftCachedCopy = false; 565 // Before reading message from db ensure the caller is bound to us (and knows the id) 566 if (mMonitor == null && !isDraftCachedCopy && isBound(binding.getBindingId())) { 567 mMonitor = ReadDraftDataAction.readDraftData(mConversationId, 568 optionalIncomingDraft, binding.getBindingId(), this); 569 return true; 570 } 571 return false; 572 } 573 574 /** 575 * Saves the current draft to db. This will save the draft and drop any pending attachments 576 * we have. The UI typically goes into the background when this is called, and instead of 577 * trying to persist the state of the pending attachments (the app may be killed, the activity 578 * may be destroyed), we simply drop the pending attachments for consistency. 579 */ saveToStorage(final BindingBase<DraftMessageData> binding)580 public void saveToStorage(final BindingBase<DraftMessageData> binding) { 581 saveToStorageInternal(binding); 582 dropPendingAttachments(); 583 } 584 saveToStorageInternal(final BindingBase<DraftMessageData> binding)585 private void saveToStorageInternal(final BindingBase<DraftMessageData> binding) { 586 // Create MessageData to store to db, but don't clear the in-memory copy so UI will 587 // continue to display it. 588 // If self id is null then we'll not attempt to change the conversation's self id. 589 final MessageData message = createMessageWithCurrentAttachments(false /* clearLocalCopy */); 590 // Before writing message to db ensure the caller is bound to us (and knows the id) 591 if (isBound(binding.getBindingId())){ 592 WriteDraftMessageAction.writeDraftMessage(mConversationId, message); 593 } 594 } 595 596 /** 597 * Called when we are ready to send the message. This will assemble/return the MessageData for 598 * sending and clear the local draft data, both from memory and from DB. This will also bind 599 * the message data with a self Id through which the message will be sent. 600 * 601 * @param binding the binding object from our consumer. We need to make sure we are still bound 602 * to that binding before saving to storage. 603 */ prepareMessageForSending(final BindingBase<DraftMessageData> binding)604 public MessageData prepareMessageForSending(final BindingBase<DraftMessageData> binding) { 605 // We can't send the message while there's still stuff pending. 606 Assert.isTrue(!hasPendingAttachments()); 607 mSending = true; 608 // Assembles the message to send and empty working draft data. 609 // If self id is null then message is sent with conversation's self id. 610 final MessageData messageToSend = 611 createMessageWithCurrentAttachments(true /* clearLocalCopy */); 612 // Note sending message will empty the draft data in DB. 613 mSending = false; 614 return messageToSend; 615 } 616 isSending()617 public boolean isSending() { 618 return mSending; 619 } 620 621 @Override // ReadDraftMessageActionListener.onReadDraftMessageSucceeded onReadDraftDataSucceeded(final ReadDraftDataAction action, final Object data, final MessageData message, final ConversationListItemData conversation)622 public void onReadDraftDataSucceeded(final ReadDraftDataAction action, final Object data, 623 final MessageData message, final ConversationListItemData conversation) { 624 final String bindingId = (String) data; 625 626 // Before passing draft message on to ui ensure the data is bound to the same bindingid 627 if (isBound(bindingId)) { 628 mSelfId = message.getSelfId(); 629 mIsGroupConversation = conversation.getIsGroup(); 630 mIncludeEmailAddress = conversation.getIncludeEmailAddress(); 631 updateFromMessageData(message, bindingId); 632 LogUtil.d(LogUtil.BUGLE_TAG, "DraftMessageData: draft loaded. " 633 + "conversationId=" + mConversationId + " selfId=" + mSelfId); 634 } else { 635 LogUtil.w(LogUtil.BUGLE_TAG, "DraftMessageData: draft loaded but not bound. " 636 + "conversationId=" + mConversationId); 637 } 638 mMonitor = null; 639 } 640 641 @Override // ReadDraftMessageActionListener.onReadDraftDataFailed onReadDraftDataFailed(final ReadDraftDataAction action, final Object data)642 public void onReadDraftDataFailed(final ReadDraftDataAction action, final Object data) { 643 LogUtil.w(LogUtil.BUGLE_TAG, "DraftMessageData: draft not loaded. " 644 + "conversationId=" + mConversationId); 645 // The draft is now synced with actual MessageData and no longer a cached copy. 646 mIsDraftCachedCopy = false; 647 // Just clear the monitor - no update to draft data 648 mMonitor = null; 649 } 650 651 /** 652 * Check if Bugle is default sms app 653 * @return 654 */ getIsDefaultSmsApp()655 public boolean getIsDefaultSmsApp() { 656 return PhoneUtils.getDefault().isDefaultSmsApp(); 657 } 658 659 @Override //BindableData.unregisterListeners unregisterListeners()660 protected void unregisterListeners() { 661 if (mMonitor != null) { 662 mMonitor.unregister(); 663 } 664 mMonitor = null; 665 mListeners.clear(); 666 } 667 destroyAttachments()668 private void destroyAttachments() { 669 for (final MessagePartData attachment : mAttachments) { 670 attachment.destroyAsync(); 671 } 672 mAttachments.clear(); 673 mPendingAttachments.clear(); 674 } 675 dispatchChanged(final int changeFlags)676 private void dispatchChanged(final int changeFlags) { 677 // No change is expected to be made to the draft if it is in cached copy state. 678 if (mIsDraftCachedCopy) { 679 return; 680 } 681 // Any change in the draft will cancel any pending draft checking task, since the 682 // size/status of the draft may have changed. 683 if (mCheckDraftForSendTask != null) { 684 mCheckDraftForSendTask.cancel(true /* mayInterruptIfRunning */); 685 mCheckDraftForSendTask = null; 686 } 687 mListeners.onDraftChanged(this, changeFlags); 688 } 689 dispatchAttachmentLimitReached()690 private void dispatchAttachmentLimitReached() { 691 mListeners.onDraftAttachmentLimitReached(this); 692 } 693 694 /** 695 * Drop any pending attachments that haven't finished. This is called after the UI goes to 696 * the background and we persist the draft data to the database. 697 */ dropPendingAttachments()698 private void dropPendingAttachments() { 699 mPendingAttachments.clear(); 700 } 701 isDraftEmpty()702 private boolean isDraftEmpty() { 703 return TextUtils.isEmpty(mMessageText) && mAttachments.isEmpty() && 704 TextUtils.isEmpty(mMessageSubject); 705 } 706 isCheckingDraft()707 public boolean isCheckingDraft() { 708 return mCheckDraftForSendTask != null && !mCheckDraftForSendTask.isCancelled(); 709 } 710 checkDraftForAction(final boolean checkMessageSize, final int selfSubId, final CheckDraftTaskCallback callback, final Binding<DraftMessageData> binding)711 public void checkDraftForAction(final boolean checkMessageSize, final int selfSubId, 712 final CheckDraftTaskCallback callback, final Binding<DraftMessageData> binding) { 713 new CheckDraftForSendTask(checkMessageSize, selfSubId, callback, binding) 714 .executeOnThreadPool((Void) null); 715 } 716 717 /** 718 * Allows us to have multiple data listeners for DraftMessageData 719 */ 720 private class DraftMessageDataEventDispatcher 721 extends ArrayList<DraftMessageDataListener> 722 implements DraftMessageDataListener { 723 724 @Override 725 @RunsOnMainThread onDraftChanged(DraftMessageData data, int changeFlags)726 public void onDraftChanged(DraftMessageData data, int changeFlags) { 727 Assert.isMainThread(); 728 for (final DraftMessageDataListener listener : this) { 729 listener.onDraftChanged(data, changeFlags); 730 } 731 } 732 733 @Override 734 @RunsOnMainThread onDraftAttachmentLimitReached(DraftMessageData data)735 public void onDraftAttachmentLimitReached(DraftMessageData data) { 736 Assert.isMainThread(); 737 for (final DraftMessageDataListener listener : this) { 738 listener.onDraftAttachmentLimitReached(data); 739 } 740 } 741 742 @Override 743 @RunsOnMainThread onDraftAttachmentLoadFailed()744 public void onDraftAttachmentLoadFailed() { 745 Assert.isMainThread(); 746 for (final DraftMessageDataListener listener : this) { 747 listener.onDraftAttachmentLoadFailed(); 748 } 749 } 750 } 751 752 public interface CheckDraftTaskCallback { onDraftChecked(DraftMessageData data, int result)753 void onDraftChecked(DraftMessageData data, int result); 754 } 755 756 public class CheckDraftForSendTask extends SafeAsyncTask<Void, Void, Integer> { 757 public static final int RESULT_PASSED = 0; 758 public static final int RESULT_HAS_PENDING_ATTACHMENTS = 1; 759 public static final int RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS = 2; 760 public static final int RESULT_MESSAGE_OVER_LIMIT = 3; 761 public static final int RESULT_VIDEO_ATTACHMENT_LIMIT_EXCEEDED = 4; 762 public static final int RESULT_SIM_NOT_READY = 5; 763 private final boolean mCheckMessageSize; 764 private final int mSelfSubId; 765 private final CheckDraftTaskCallback mCallback; 766 private final String mBindingId; 767 private final List<MessagePartData> mAttachmentsCopy; 768 private int mPreExecuteResult = RESULT_PASSED; 769 CheckDraftForSendTask(final boolean checkMessageSize, final int selfSubId, final CheckDraftTaskCallback callback, final Binding<DraftMessageData> binding)770 public CheckDraftForSendTask(final boolean checkMessageSize, final int selfSubId, 771 final CheckDraftTaskCallback callback, final Binding<DraftMessageData> binding) { 772 mCheckMessageSize = checkMessageSize; 773 mSelfSubId = selfSubId; 774 mCallback = callback; 775 mBindingId = binding.getBindingId(); 776 // Obtain an immutable copy of the attachment list so we can operate on it in the 777 // background thread. 778 mAttachmentsCopy = new ArrayList<MessagePartData>(mAttachments); 779 780 mCheckDraftForSendTask = this; 781 } 782 783 @Override onPreExecute()784 protected void onPreExecute() { 785 // Perform checking work that can happen on the main thread. 786 if (hasPendingAttachments()) { 787 mPreExecuteResult = RESULT_HAS_PENDING_ATTACHMENTS; 788 return; 789 } 790 if (getIsGroupMmsConversation()) { 791 try { 792 if (TextUtils.isEmpty(PhoneUtils.get(mSelfSubId).getSelfRawNumber(true))) { 793 mPreExecuteResult = RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS; 794 return; 795 } 796 } catch (IllegalStateException e) { 797 // This happens when there is no active subscription, e.g. on Nova 798 // when the phone switches carrier. 799 mPreExecuteResult = RESULT_SIM_NOT_READY; 800 return; 801 } 802 } 803 if (getVideoAttachmentCount() > MmsUtils.MAX_VIDEO_ATTACHMENT_COUNT) { 804 mPreExecuteResult = RESULT_VIDEO_ATTACHMENT_LIMIT_EXCEEDED; 805 return; 806 } 807 } 808 809 @Override doInBackgroundTimed(Void... params)810 protected Integer doInBackgroundTimed(Void... params) { 811 if (mPreExecuteResult != RESULT_PASSED) { 812 return mPreExecuteResult; 813 } 814 815 if (mCheckMessageSize && getIsMessageOverLimit()) { 816 return RESULT_MESSAGE_OVER_LIMIT; 817 } 818 return RESULT_PASSED; 819 } 820 821 @Override onPostExecute(Integer result)822 protected void onPostExecute(Integer result) { 823 mCheckDraftForSendTask = null; 824 // Only call back if we are bound to the original binding. 825 if (isBound(mBindingId) && !isCancelled()) { 826 mCallback.onDraftChecked(DraftMessageData.this, result); 827 } else { 828 if (!isBound(mBindingId)) { 829 LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: draft not bound"); 830 } 831 if (isCancelled()) { 832 LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: draft is cancelled"); 833 } 834 } 835 } 836 837 @Override onCancelled()838 protected void onCancelled() { 839 mCheckDraftForSendTask = null; 840 } 841 842 /** 843 * 1. Check if the draft message contains too many attachments to send 844 * 2. Computes the minimum size that this message could be compressed/downsampled/encoded 845 * before sending and check if it meets the carrier max size for sending. 846 * @see MessagePartData#getMinimumSizeInBytesForSending() 847 */ 848 @DoesNotRunOnMainThread getIsMessageOverLimit()849 private boolean getIsMessageOverLimit() { 850 Assert.isNotMainThread(); 851 if (mAttachmentsCopy.size() > getAttachmentLimit()) { 852 return true; 853 } 854 855 // Aggregate the size from all the attachments. 856 long totalSize = 0; 857 for (final MessagePartData attachment : mAttachmentsCopy) { 858 totalSize += attachment.getMinimumSizeInBytesForSending(); 859 } 860 return totalSize > MmsConfig.get(mSelfSubId).getMaxMessageSize(); 861 } 862 } 863 onPendingAttachmentLoadFailed(PendingAttachmentData data)864 public void onPendingAttachmentLoadFailed(PendingAttachmentData data) { 865 mListeners.onDraftAttachmentLoadFailed(); 866 } 867 } 868