• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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