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
17package com.android.messaging.datamodel.data;
18
19import android.net.Uri;
20import android.text.TextUtils;
21
22import com.android.messaging.datamodel.MessageTextStats;
23import com.android.messaging.datamodel.action.ReadDraftDataAction;
24import com.android.messaging.datamodel.action.ReadDraftDataAction.ReadDraftDataActionListener;
25import com.android.messaging.datamodel.action.ReadDraftDataAction.ReadDraftDataActionMonitor;
26import com.android.messaging.datamodel.action.WriteDraftMessageAction;
27import com.android.messaging.datamodel.binding.BindableData;
28import com.android.messaging.datamodel.binding.Binding;
29import com.android.messaging.datamodel.binding.BindingBase;
30import com.android.messaging.sms.MmsConfig;
31import com.android.messaging.sms.MmsSmsUtils;
32import com.android.messaging.sms.MmsUtils;
33import com.android.messaging.util.Assert;
34import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
35import com.android.messaging.util.Assert.RunsOnMainThread;
36import com.android.messaging.util.BugleGservices;
37import com.android.messaging.util.BugleGservicesKeys;
38import com.android.messaging.util.LogUtil;
39import com.android.messaging.util.PhoneUtils;
40import com.android.messaging.util.SafeAsyncTask;
41
42import java.util.ArrayList;
43import java.util.Collection;
44import java.util.Collections;
45import java.util.Iterator;
46import java.util.List;
47import java.util.Set;
48
49public 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