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