Home | History | Annotate | Download | only in data
      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
     56         void onDraftChanged(DraftMessageData data, int changeFlags);
     57 
     58         @RunsOnMainThread
     59         void onDraftAttachmentLimitReached(DraftMessageData data);
     60 
     61         @RunsOnMainThread
     62         void onDraftAttachmentLoadFailed();
     63     }
     64 
     65     /**
     66      * Interface for providing subscription-related data to DraftMessageData
     67      */
     68     public interface DraftMessageSubscriptionDataProvider {
     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 
    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 
    128     public void addListener(final DraftMessageDataListener listener) {
    129         mListeners.add(listener);
    130     }
    131 
    132     public void setSubscriptionDataProvider(final DraftMessageSubscriptionDataProvider provider) {
    133         mSubscriptionDataProvider = provider;
    134     }
    135 
    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      */
    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 
    205     private void clearLocalDraftCopy() {
    206         mIsDraftCachedCopy = false;
    207         mAttachments.clear();
    208         setMessageText("");
    209         setMessageSubject("");
    210     }
    211 
    212     public String getConversationId() {
    213         return mConversationId;
    214     }
    215 
    216     public String getMessageText() {
    217         return mMessageText;
    218     }
    219 
    220     public String getMessageSubject() {
    221         return mMessageSubject;
    222     }
    223 
    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 
    232     public boolean getIsGroupMmsConversation() {
    233         return getIsMms() && mIsGroupConversation;
    234     }
    235 
    236     public String getSelfId() {
    237         return mSelfId;
    238     }
    239 
    240     public int getNumMessagesToBeSent() {
    241         return mMessageTextStats.getNumMessagesToBeSent();
    242     }
    243 
    244     public int getCodePointsRemainingInCurrentMessage() {
    245         return mMessageTextStats.getCodePointsRemainingInCurrentMessage();
    246     }
    247 
    248     public int getSelfSubId() {
    249         return mSubscriptionDataProvider == null ? ParticipantData.DEFAULT_SELF_SUB_ID :
    250                 mSubscriptionDataProvider.getConversationSelfSubId();
    251     }
    252 
    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 
    261     private void setMessageSubject(final String subject, final boolean notify) {
    262         mMessageSubject = subject;
    263         if (notify) {
    264             dispatchChanged(MESSAGE_SUBJECT_CHANGED);
    265         }
    266     }
    267 
    268     public void setMessageText(final String messageText) {
    269         setMessageText(messageText, false);
    270     }
    271 
    272     public void setMessageSubject(final String subject) {
    273         setMessageSubject(subject, false);
    274     }
    275 
    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 
    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      */
    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 
    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 
    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      */
    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 
    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 
    433     public boolean hasAttachments() {
    434         return !mAttachments.isEmpty();
    435     }
    436 
    437     public boolean hasPendingAttachments() {
    438         return !mPendingAttachments.isEmpty();
    439     }
    440 
    441     private int getAttachmentCount() {
    442         return mAttachments.size() + mPendingAttachments.size();
    443     }
    444 
    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 
    460     private int getAttachmentLimit() {
    461         return BugleGservices.get().getInt(
    462                 BugleGservicesKeys.MMS_ATTACHMENT_LIMIT,
    463                 BugleGservicesKeys.MMS_ATTACHMENT_LIMIT_DEFAULT);
    464     }
    465 
    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 
    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 
    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 
    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      */
    530     public void clearAttachments(final int flags) {
    531         destroyAttachments();
    532         dispatchChanged(flags);
    533     }
    534 
    535     public List<MessagePartData> getReadOnlyAttachments() {
    536         return mReadOnlyAttachments;
    537     }
    538 
    539     public List<PendingAttachmentData> getReadOnlyPendingAttachments() {
    540         return mReadOnlyPendingAttachments;
    541     }
    542 
    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      */
    568     public void saveToStorage(final BindingBase<DraftMessageData> binding) {
    569         saveToStorageInternal(binding);
    570         dropPendingAttachments();
    571     }
    572 
    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      */
    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 
    605     public boolean isSending() {
    606         return mSending;
    607     }
    608 
    609     @Override // ReadDraftMessageActionListener.onReadDraftMessageSucceeded
    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
    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      */
    643     public boolean getIsDefaultSmsApp() {
    644         return PhoneUtils.getDefault().isDefaultSmsApp();
    645     }
    646 
    647     @Override //BindableData.unregisterListeners
    648     protected void unregisterListeners() {
    649         if (mMonitor != null) {
    650             mMonitor.unregister();
    651         }
    652         mMonitor = null;
    653         mListeners.clear();
    654     }
    655 
    656     private void destroyAttachments() {
    657         for (final MessagePartData attachment : mAttachments) {
    658             attachment.destroyAsync();
    659         }
    660         mAttachments.clear();
    661         mPendingAttachments.clear();
    662     }
    663 
    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 
    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      */
    686     private void dropPendingAttachments() {
    687         mPendingAttachments.clear();
    688     }
    689 
    690     private boolean isDraftEmpty() {
    691         return TextUtils.isEmpty(mMessageText) && mAttachments.isEmpty() &&
    692                 TextUtils.isEmpty(mMessageSubject);
    693     }
    694 
    695     public boolean isCheckingDraft() {
    696         return mCheckDraftForSendTask != null && !mCheckDraftForSendTask.isCancelled();
    697     }
    698 
    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
    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
    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
    732         public void onDraftAttachmentLoadFailed() {
    733             Assert.isMainThread();
    734             for (final DraftMessageDataListener listener : this) {
    735                 listener.onDraftAttachmentLoadFailed();
    736             }
    737         }
    738     }
    739 
    740     public interface CheckDraftTaskCallback {
    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 
    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
    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
    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
    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
    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
    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 
    852     public void onPendingAttachmentLoadFailed(PendingAttachmentData data) {
    853         mListeners.onDraftAttachmentLoadFailed();
    854     }
    855 }
    856