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 final boolean reachedLimit = getAttachmentCount() >= getAttachmentLimit(); 343 if (reachedLimit || containsAttachment(attachment.getContentUri())) { 344 // Never go over the limit. Never add duplicated attachments. 345 attachment.destroyAsync(); 346 return reachedLimit; 347 } else { 348 addAttachment(attachment, null /*pendingAttachment*/); 349 return false; 350 } 351 } 352 addAttachment(final MessagePartData attachment, final PendingAttachmentData pendingAttachment)353 private void addAttachment(final MessagePartData attachment, 354 final PendingAttachmentData pendingAttachment) { 355 if (attachment != null && attachment.isSinglePartOnly()) { 356 // clear any existing attachments because the attachment we're adding can only 357 // exist by itself. 358 destroyAttachments(); 359 } 360 if (pendingAttachment != null && pendingAttachment.isSinglePartOnly()) { 361 // clear any existing attachments because the attachment we're adding can only 362 // exist by itself. 363 destroyAttachments(); 364 } 365 // If the existing attachments contain a single-only attachment, we need to clear the 366 // existing attachments to make room for the incoming attachment. 367 for (final MessagePartData data : mAttachments) { 368 if (data.isSinglePartOnly()) { 369 // clear any existing attachments because the single attachment can only exist 370 // by itself 371 destroyAttachments(); 372 break; 373 } 374 } 375 // If any of the pending attachments contain a single-only attachment, we need to clear the 376 // existing attachments to make room for the incoming attachment. 377 for (final MessagePartData data : mPendingAttachments) { 378 if (data.isSinglePartOnly()) { 379 // clear any existing attachments because the single attachment can only exist 380 // by itself 381 destroyAttachments(); 382 break; 383 } 384 } 385 if (attachment != null) { 386 mAttachments.add(attachment); 387 } else if (pendingAttachment != null) { 388 mPendingAttachments.add(pendingAttachment); 389 } 390 } 391 addPendingAttachment(final PendingAttachmentData pendingAttachment, final BindingBase<DraftMessageData> binding)392 public void addPendingAttachment(final PendingAttachmentData pendingAttachment, 393 final BindingBase<DraftMessageData> binding) { 394 final boolean reachedLimit = addOnePendingAttachmentNoNotify(pendingAttachment, 395 binding.getBindingId()); 396 if (reachedLimit) { 397 dispatchAttachmentLimitReached(); 398 } 399 dispatchChanged(ATTACHMENTS_CHANGED); 400 } 401 402 /** 403 * Try to add one pending attachment, while guarding against duplicates and 404 * going over the limit. 405 * @return true if the attachment limit was reached, false otherwise 406 */ addOnePendingAttachmentNoNotify(final PendingAttachmentData pendingAttachment, final String bindingId)407 private boolean addOnePendingAttachmentNoNotify(final PendingAttachmentData pendingAttachment, 408 final String bindingId) { 409 final boolean reachedLimit = getAttachmentCount() >= getAttachmentLimit(); 410 if (reachedLimit || containsAttachment(pendingAttachment.getContentUri())) { 411 // Never go over the limit. Never add duplicated attachments. 412 pendingAttachment.destroyAsync(); 413 return reachedLimit; 414 } else { 415 Assert.isTrue(!mPendingAttachments.contains(pendingAttachment)); 416 Assert.equals(PendingAttachmentData.STATE_PENDING, pendingAttachment.getCurrentState()); 417 addAttachment(null /*attachment*/, pendingAttachment); 418 419 pendingAttachment.loadAttachmentForDraft(this, bindingId); 420 return false; 421 } 422 } 423 setSelfId(final String selfId, final boolean notify)424 public void setSelfId(final String selfId, final boolean notify) { 425 LogUtil.d(LogUtil.BUGLE_TAG, "DraftMessageData: set selfId=" + selfId 426 + " for conversationId=" + mConversationId); 427 mSelfId = selfId; 428 if (notify) { 429 dispatchChanged(SELF_CHANGED); 430 } 431 } 432 hasAttachments()433 public boolean hasAttachments() { 434 return !mAttachments.isEmpty(); 435 } 436 hasPendingAttachments()437 public boolean hasPendingAttachments() { 438 return !mPendingAttachments.isEmpty(); 439 } 440 getAttachmentCount()441 private int getAttachmentCount() { 442 return mAttachments.size() + mPendingAttachments.size(); 443 } 444 getVideoAttachmentCount()445 private int getVideoAttachmentCount() { 446 int count = 0; 447 for (MessagePartData part : mAttachments) { 448 if (part.isVideo()) { 449 count++; 450 } 451 } 452 for (MessagePartData part : mPendingAttachments) { 453 if (part.isVideo()) { 454 count++; 455 } 456 } 457 return count; 458 } 459 getAttachmentLimit()460 private int getAttachmentLimit() { 461 return BugleGservices.get().getInt( 462 BugleGservicesKeys.MMS_ATTACHMENT_LIMIT, 463 BugleGservicesKeys.MMS_ATTACHMENT_LIMIT_DEFAULT); 464 } 465 removeAttachment(final MessagePartData attachment)466 public void removeAttachment(final MessagePartData attachment) { 467 for (final MessagePartData existingAttachment : mAttachments) { 468 if (existingAttachment.getContentUri().equals(attachment.getContentUri())) { 469 mAttachments.remove(existingAttachment); 470 existingAttachment.destroyAsync(); 471 dispatchChanged(ATTACHMENTS_CHANGED); 472 break; 473 } 474 } 475 } 476 removeExistingAttachments(final Set<MessagePartData> attachmentsToRemove)477 public void removeExistingAttachments(final Set<MessagePartData> attachmentsToRemove) { 478 boolean removed = false; 479 final Iterator<MessagePartData> iterator = mAttachments.iterator(); 480 while (iterator.hasNext()) { 481 final MessagePartData existingAttachment = iterator.next(); 482 if (attachmentsToRemove.contains(existingAttachment)) { 483 iterator.remove(); 484 existingAttachment.destroyAsync(); 485 removed = true; 486 } 487 } 488 489 if (removed) { 490 dispatchChanged(ATTACHMENTS_CHANGED); 491 } 492 } 493 removePendingAttachment(final PendingAttachmentData pendingAttachment)494 public void removePendingAttachment(final PendingAttachmentData pendingAttachment) { 495 for (final PendingAttachmentData existingAttachment : mPendingAttachments) { 496 if (existingAttachment.getContentUri().equals(pendingAttachment.getContentUri())) { 497 mPendingAttachments.remove(pendingAttachment); 498 pendingAttachment.destroyAsync(); 499 dispatchChanged(ATTACHMENTS_CHANGED); 500 break; 501 } 502 } 503 } 504 updatePendingAttachment(final MessagePartData updatedAttachment, final PendingAttachmentData pendingAttachment)505 public void updatePendingAttachment(final MessagePartData updatedAttachment, 506 final PendingAttachmentData pendingAttachment) { 507 for (final PendingAttachmentData existingAttachment : mPendingAttachments) { 508 if (existingAttachment.getContentUri().equals(pendingAttachment.getContentUri())) { 509 mPendingAttachments.remove(pendingAttachment); 510 if (pendingAttachment.isSinglePartOnly()) { 511 updatedAttachment.setSinglePartOnly(true); 512 } 513 mAttachments.add(updatedAttachment); 514 dispatchChanged(ATTACHMENTS_CHANGED); 515 return; 516 } 517 } 518 519 // If we are here, this means the pending attachment has been dropped before the task 520 // to load it was completed. In this case destroy the temporarily staged file since it 521 // is no longer needed. 522 updatedAttachment.destroyAsync(); 523 } 524 525 /** 526 * Remove the attachments from the draft and notify any listeners. 527 * @param flags typically this will be ATTACHMENTS_CHANGED. When attachments are cleared in a 528 * widget, flags will also contain WIDGET_CHANGED. 529 */ clearAttachments(final int flags)530 public void clearAttachments(final int flags) { 531 destroyAttachments(); 532 dispatchChanged(flags); 533 } 534 getReadOnlyAttachments()535 public List<MessagePartData> getReadOnlyAttachments() { 536 return mReadOnlyAttachments; 537 } 538 getReadOnlyPendingAttachments()539 public List<PendingAttachmentData> getReadOnlyPendingAttachments() { 540 return mReadOnlyPendingAttachments; 541 } 542 loadFromStorage(final BindingBase<DraftMessageData> binding, final MessageData optionalIncomingDraft, boolean clearLocalDraft)543 public boolean loadFromStorage(final BindingBase<DraftMessageData> binding, 544 final MessageData optionalIncomingDraft, boolean clearLocalDraft) { 545 LogUtil.d(LogUtil.BUGLE_TAG, "DraftMessageData: " 546 + (optionalIncomingDraft == null ? "loading" : "setting") 547 + " for conversationId=" + mConversationId); 548 if (clearLocalDraft) { 549 clearLocalDraftCopy(); 550 } 551 final boolean isDraftCachedCopy = mIsDraftCachedCopy; 552 mIsDraftCachedCopy = false; 553 // Before reading message from db ensure the caller is bound to us (and knows the id) 554 if (mMonitor == null && !isDraftCachedCopy && isBound(binding.getBindingId())) { 555 mMonitor = ReadDraftDataAction.readDraftData(mConversationId, 556 optionalIncomingDraft, binding.getBindingId(), this); 557 return true; 558 } 559 return false; 560 } 561 562 /** 563 * Saves the current draft to db. This will save the draft and drop any pending attachments 564 * we have. The UI typically goes into the background when this is called, and instead of 565 * trying to persist the state of the pending attachments (the app may be killed, the activity 566 * may be destroyed), we simply drop the pending attachments for consistency. 567 */ saveToStorage(final BindingBase<DraftMessageData> binding)568 public void saveToStorage(final BindingBase<DraftMessageData> binding) { 569 saveToStorageInternal(binding); 570 dropPendingAttachments(); 571 } 572 saveToStorageInternal(final BindingBase<DraftMessageData> binding)573 private void saveToStorageInternal(final BindingBase<DraftMessageData> binding) { 574 // Create MessageData to store to db, but don't clear the in-memory copy so UI will 575 // continue to display it. 576 // If self id is null then we'll not attempt to change the conversation's self id. 577 final MessageData message = createMessageWithCurrentAttachments(false /* clearLocalCopy */); 578 // Before writing message to db ensure the caller is bound to us (and knows the id) 579 if (isBound(binding.getBindingId())){ 580 WriteDraftMessageAction.writeDraftMessage(mConversationId, message); 581 } 582 } 583 584 /** 585 * Called when we are ready to send the message. This will assemble/return the MessageData for 586 * sending and clear the local draft data, both from memory and from DB. This will also bind 587 * the message data with a self Id through which the message will be sent. 588 * 589 * @param binding the binding object from our consumer. We need to make sure we are still bound 590 * to that binding before saving to storage. 591 */ prepareMessageForSending(final BindingBase<DraftMessageData> binding)592 public MessageData prepareMessageForSending(final BindingBase<DraftMessageData> binding) { 593 // We can't send the message while there's still stuff pending. 594 Assert.isTrue(!hasPendingAttachments()); 595 mSending = true; 596 // Assembles the message to send and empty working draft data. 597 // If self id is null then message is sent with conversation's self id. 598 final MessageData messageToSend = 599 createMessageWithCurrentAttachments(true /* clearLocalCopy */); 600 // Note sending message will empty the draft data in DB. 601 mSending = false; 602 return messageToSend; 603 } 604 isSending()605 public boolean isSending() { 606 return mSending; 607 } 608 609 @Override // ReadDraftMessageActionListener.onReadDraftMessageSucceeded onReadDraftDataSucceeded(final ReadDraftDataAction action, final Object data, final MessageData message, final ConversationListItemData conversation)610 public void onReadDraftDataSucceeded(final ReadDraftDataAction action, final Object data, 611 final MessageData message, final ConversationListItemData conversation) { 612 final String bindingId = (String) data; 613 614 // Before passing draft message on to ui ensure the data is bound to the same bindingid 615 if (isBound(bindingId)) { 616 mSelfId = message.getSelfId(); 617 mIsGroupConversation = conversation.getIsGroup(); 618 mIncludeEmailAddress = conversation.getIncludeEmailAddress(); 619 updateFromMessageData(message, bindingId); 620 LogUtil.d(LogUtil.BUGLE_TAG, "DraftMessageData: draft loaded. " 621 + "conversationId=" + mConversationId + " selfId=" + mSelfId); 622 } else { 623 LogUtil.w(LogUtil.BUGLE_TAG, "DraftMessageData: draft loaded but not bound. " 624 + "conversationId=" + mConversationId); 625 } 626 mMonitor = null; 627 } 628 629 @Override // ReadDraftMessageActionListener.onReadDraftDataFailed onReadDraftDataFailed(final ReadDraftDataAction action, final Object data)630 public void onReadDraftDataFailed(final ReadDraftDataAction action, final Object data) { 631 LogUtil.w(LogUtil.BUGLE_TAG, "DraftMessageData: draft not loaded. " 632 + "conversationId=" + mConversationId); 633 // The draft is now synced with actual MessageData and no longer a cached copy. 634 mIsDraftCachedCopy = false; 635 // Just clear the monitor - no update to draft data 636 mMonitor = null; 637 } 638 639 /** 640 * Check if Bugle is default sms app 641 * @return 642 */ getIsDefaultSmsApp()643 public boolean getIsDefaultSmsApp() { 644 return PhoneUtils.getDefault().isDefaultSmsApp(); 645 } 646 647 @Override //BindableData.unregisterListeners unregisterListeners()648 protected void unregisterListeners() { 649 if (mMonitor != null) { 650 mMonitor.unregister(); 651 } 652 mMonitor = null; 653 mListeners.clear(); 654 } 655 destroyAttachments()656 private void destroyAttachments() { 657 for (final MessagePartData attachment : mAttachments) { 658 attachment.destroyAsync(); 659 } 660 mAttachments.clear(); 661 mPendingAttachments.clear(); 662 } 663 dispatchChanged(final int changeFlags)664 private void dispatchChanged(final int changeFlags) { 665 // No change is expected to be made to the draft if it is in cached copy state. 666 if (mIsDraftCachedCopy) { 667 return; 668 } 669 // Any change in the draft will cancel any pending draft checking task, since the 670 // size/status of the draft may have changed. 671 if (mCheckDraftForSendTask != null) { 672 mCheckDraftForSendTask.cancel(true /* mayInterruptIfRunning */); 673 mCheckDraftForSendTask = null; 674 } 675 mListeners.onDraftChanged(this, changeFlags); 676 } 677 dispatchAttachmentLimitReached()678 private void dispatchAttachmentLimitReached() { 679 mListeners.onDraftAttachmentLimitReached(this); 680 } 681 682 /** 683 * Drop any pending attachments that haven't finished. This is called after the UI goes to 684 * the background and we persist the draft data to the database. 685 */ dropPendingAttachments()686 private void dropPendingAttachments() { 687 mPendingAttachments.clear(); 688 } 689 isDraftEmpty()690 private boolean isDraftEmpty() { 691 return TextUtils.isEmpty(mMessageText) && mAttachments.isEmpty() && 692 TextUtils.isEmpty(mMessageSubject); 693 } 694 isCheckingDraft()695 public boolean isCheckingDraft() { 696 return mCheckDraftForSendTask != null && !mCheckDraftForSendTask.isCancelled(); 697 } 698 checkDraftForAction(final boolean checkMessageSize, final int selfSubId, final CheckDraftTaskCallback callback, final Binding<DraftMessageData> binding)699 public void checkDraftForAction(final boolean checkMessageSize, final int selfSubId, 700 final CheckDraftTaskCallback callback, final Binding<DraftMessageData> binding) { 701 new CheckDraftForSendTask(checkMessageSize, selfSubId, callback, binding) 702 .executeOnThreadPool((Void) null); 703 } 704 705 /** 706 * Allows us to have multiple data listeners for DraftMessageData 707 */ 708 private class DraftMessageDataEventDispatcher 709 extends ArrayList<DraftMessageDataListener> 710 implements DraftMessageDataListener { 711 712 @Override 713 @RunsOnMainThread onDraftChanged(DraftMessageData data, int changeFlags)714 public void onDraftChanged(DraftMessageData data, int changeFlags) { 715 Assert.isMainThread(); 716 for (final DraftMessageDataListener listener : this) { 717 listener.onDraftChanged(data, changeFlags); 718 } 719 } 720 721 @Override 722 @RunsOnMainThread onDraftAttachmentLimitReached(DraftMessageData data)723 public void onDraftAttachmentLimitReached(DraftMessageData data) { 724 Assert.isMainThread(); 725 for (final DraftMessageDataListener listener : this) { 726 listener.onDraftAttachmentLimitReached(data); 727 } 728 } 729 730 @Override 731 @RunsOnMainThread onDraftAttachmentLoadFailed()732 public void onDraftAttachmentLoadFailed() { 733 Assert.isMainThread(); 734 for (final DraftMessageDataListener listener : this) { 735 listener.onDraftAttachmentLoadFailed(); 736 } 737 } 738 } 739 740 public interface CheckDraftTaskCallback { onDraftChecked(DraftMessageData data, int result)741 void onDraftChecked(DraftMessageData data, int result); 742 } 743 744 public class CheckDraftForSendTask extends SafeAsyncTask<Void, Void, Integer> { 745 public static final int RESULT_PASSED = 0; 746 public static final int RESULT_HAS_PENDING_ATTACHMENTS = 1; 747 public static final int RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS = 2; 748 public static final int RESULT_MESSAGE_OVER_LIMIT = 3; 749 public static final int RESULT_VIDEO_ATTACHMENT_LIMIT_EXCEEDED = 4; 750 public static final int RESULT_SIM_NOT_READY = 5; 751 private final boolean mCheckMessageSize; 752 private final int mSelfSubId; 753 private final CheckDraftTaskCallback mCallback; 754 private final String mBindingId; 755 private final List<MessagePartData> mAttachmentsCopy; 756 private int mPreExecuteResult = RESULT_PASSED; 757 CheckDraftForSendTask(final boolean checkMessageSize, final int selfSubId, final CheckDraftTaskCallback callback, final Binding<DraftMessageData> binding)758 public CheckDraftForSendTask(final boolean checkMessageSize, final int selfSubId, 759 final CheckDraftTaskCallback callback, final Binding<DraftMessageData> binding) { 760 mCheckMessageSize = checkMessageSize; 761 mSelfSubId = selfSubId; 762 mCallback = callback; 763 mBindingId = binding.getBindingId(); 764 // Obtain an immutable copy of the attachment list so we can operate on it in the 765 // background thread. 766 mAttachmentsCopy = new ArrayList<MessagePartData>(mAttachments); 767 768 mCheckDraftForSendTask = this; 769 } 770 771 @Override onPreExecute()772 protected void onPreExecute() { 773 // Perform checking work that can happen on the main thread. 774 if (hasPendingAttachments()) { 775 mPreExecuteResult = RESULT_HAS_PENDING_ATTACHMENTS; 776 return; 777 } 778 if (getIsGroupMmsConversation()) { 779 try { 780 if (TextUtils.isEmpty(PhoneUtils.get(mSelfSubId).getSelfRawNumber(true))) { 781 mPreExecuteResult = RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS; 782 return; 783 } 784 } catch (IllegalStateException e) { 785 // This happens when there is no active subscription, e.g. on Nova 786 // when the phone switches carrier. 787 mPreExecuteResult = RESULT_SIM_NOT_READY; 788 return; 789 } 790 } 791 if (getVideoAttachmentCount() > MmsUtils.MAX_VIDEO_ATTACHMENT_COUNT) { 792 mPreExecuteResult = RESULT_VIDEO_ATTACHMENT_LIMIT_EXCEEDED; 793 return; 794 } 795 } 796 797 @Override doInBackgroundTimed(Void... params)798 protected Integer doInBackgroundTimed(Void... params) { 799 if (mPreExecuteResult != RESULT_PASSED) { 800 return mPreExecuteResult; 801 } 802 803 if (mCheckMessageSize && getIsMessageOverLimit()) { 804 return RESULT_MESSAGE_OVER_LIMIT; 805 } 806 return RESULT_PASSED; 807 } 808 809 @Override onPostExecute(Integer result)810 protected void onPostExecute(Integer result) { 811 mCheckDraftForSendTask = null; 812 // Only call back if we are bound to the original binding. 813 if (isBound(mBindingId) && !isCancelled()) { 814 mCallback.onDraftChecked(DraftMessageData.this, result); 815 } else { 816 if (!isBound(mBindingId)) { 817 LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: draft not bound"); 818 } 819 if (isCancelled()) { 820 LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: draft is cancelled"); 821 } 822 } 823 } 824 825 @Override onCancelled()826 protected void onCancelled() { 827 mCheckDraftForSendTask = null; 828 } 829 830 /** 831 * 1. Check if the draft message contains too many attachments to send 832 * 2. Computes the minimum size that this message could be compressed/downsampled/encoded 833 * before sending and check if it meets the carrier max size for sending. 834 * @see MessagePartData#getMinimumSizeInBytesForSending() 835 */ 836 @DoesNotRunOnMainThread getIsMessageOverLimit()837 private boolean getIsMessageOverLimit() { 838 Assert.isNotMainThread(); 839 if (mAttachmentsCopy.size() > getAttachmentLimit()) { 840 return true; 841 } 842 843 // Aggregate the size from all the attachments. 844 long totalSize = 0; 845 for (final MessagePartData attachment : mAttachmentsCopy) { 846 totalSize += attachment.getMinimumSizeInBytesForSending(); 847 } 848 return totalSize > MmsConfig.get(mSelfSubId).getMaxMessageSize(); 849 } 850 } 851 onPendingAttachmentLoadFailed(PendingAttachmentData data)852 public void onPendingAttachmentLoadFailed(PendingAttachmentData data) { 853 mListeners.onDraftAttachmentLoadFailed(); 854 } 855 } 856