/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.messaging.datamodel.data; import android.net.Uri; import android.text.TextUtils; import com.android.messaging.datamodel.MessageTextStats; import com.android.messaging.datamodel.action.ReadDraftDataAction; import com.android.messaging.datamodel.action.ReadDraftDataAction.ReadDraftDataActionListener; import com.android.messaging.datamodel.action.ReadDraftDataAction.ReadDraftDataActionMonitor; import com.android.messaging.datamodel.action.WriteDraftMessageAction; import com.android.messaging.datamodel.binding.BindableData; import com.android.messaging.datamodel.binding.Binding; import com.android.messaging.datamodel.binding.BindingBase; import com.android.messaging.sms.MmsConfig; import com.android.messaging.sms.MmsSmsUtils; import com.android.messaging.sms.MmsUtils; import com.android.messaging.util.Assert; import com.android.messaging.util.Assert.DoesNotRunOnMainThread; import com.android.messaging.util.Assert.RunsOnMainThread; import com.android.messaging.util.BugleGservices; import com.android.messaging.util.BugleGservicesKeys; import com.android.messaging.util.LogUtil; import com.android.messaging.util.PhoneUtils; import com.android.messaging.util.SafeAsyncTask; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; public class DraftMessageData extends BindableData implements ReadDraftDataActionListener { /** * Interface for DraftMessageData listeners */ public interface DraftMessageDataListener { @RunsOnMainThread void onDraftChanged(DraftMessageData data, int changeFlags); @RunsOnMainThread void onDraftAttachmentLimitReached(DraftMessageData data); @RunsOnMainThread void onDraftAttachmentLoadFailed(); } /** * Interface for providing subscription-related data to DraftMessageData */ public interface DraftMessageSubscriptionDataProvider { int getConversationSelfSubId(); } // Flags sent to onDraftChanged to help the receiver limit the amount of work done public static int ATTACHMENTS_CHANGED = 0x0001; public static int MESSAGE_TEXT_CHANGED = 0x0002; public static int MESSAGE_SUBJECT_CHANGED = 0x0004; // Whether the self participant data has been loaded public static int SELF_CHANGED = 0x0008; public static int ALL_CHANGED = 0x00FF; // ALL_CHANGED intentionally doesn't include WIDGET_CHANGED. ConversationFragment needs to // be notified if the draft it is looking at is changed externally (by a desktop widget) so it // can reload the draft. public static int WIDGET_CHANGED = 0x0100; private final String mConversationId; private ReadDraftDataActionMonitor mMonitor; private final DraftMessageDataEventDispatcher mListeners; private DraftMessageSubscriptionDataProvider mSubscriptionDataProvider; private boolean mIncludeEmailAddress; private boolean mIsGroupConversation; private String mMessageText; private String mMessageSubject; private String mSelfId; private MessageTextStats mMessageTextStats; private boolean mSending; /** Keeps track of completed attachments in the message draft. This data is persisted to db */ private final List mAttachments; /** A read-only wrapper on mAttachments surfaced to the UI layer for rendering */ private final List mReadOnlyAttachments; /** Keeps track of pending attachments that are being loaded. The pending attachments are * transient, because they are not persisted to the database and are dropped once we go * to the background (after the UI calls saveToStorage) */ private final List mPendingAttachments; /** A read-only wrapper on mPendingAttachments surfaced to the UI layer for rendering */ private final List mReadOnlyPendingAttachments; /** Is the current draft a cached copy of what's been saved to the database. If so, we * may skip loading from database if we are still bound */ private boolean mIsDraftCachedCopy; /** Whether we are currently asynchronously validating the draft before sending. */ private CheckDraftForSendTask mCheckDraftForSendTask; public DraftMessageData(final String conversationId) { mConversationId = conversationId; mAttachments = new ArrayList(); mReadOnlyAttachments = Collections.unmodifiableList(mAttachments); mPendingAttachments = new ArrayList(); mReadOnlyPendingAttachments = Collections.unmodifiableList(mPendingAttachments); mListeners = new DraftMessageDataEventDispatcher(); mMessageTextStats = new MessageTextStats(); } public void addListener(final DraftMessageDataListener listener) { mListeners.add(listener); } public void setSubscriptionDataProvider(final DraftMessageSubscriptionDataProvider provider) { mSubscriptionDataProvider = provider; } public void updateFromMessageData(final MessageData message, final String bindingId) { // New attachments have arrived - only update if the user hasn't already edited Assert.notNull(bindingId); // The draft is now synced with actual MessageData and no longer a cached copy. mIsDraftCachedCopy = false; // Do not use the loaded draft if the user began composing a message before the draft loaded // During config changes (orientation), the text fields preserve their data, so allow them // to be the same and still consider the draft unchanged by the user if (isDraftEmpty() || (TextUtils.equals(mMessageText, message.getMessageText()) && TextUtils.equals(mMessageSubject, message.getMmsSubject()) && mAttachments.isEmpty())) { // No need to clear as just checked it was empty or a subset setMessageText(message.getMessageText(), false /* notify */); setMessageSubject(message.getMmsSubject(), false /* notify */); for (final MessagePartData part : message.getParts()) { if (part.isAttachment() && getAttachmentCount() >= getAttachmentLimit()) { dispatchAttachmentLimitReached(); break; } if (part instanceof PendingAttachmentData) { // This is a pending attachment data from share intent (e.g. an shared image // that we need to persist locally). final PendingAttachmentData data = (PendingAttachmentData) part; Assert.equals(PendingAttachmentData.STATE_PENDING, data.getCurrentState()); addOnePendingAttachmentNoNotify(data, bindingId); } else if (part.isAttachment()) { addOneAttachmentNoNotify(part); } } dispatchChanged(ALL_CHANGED); } else { // The user has started a new message so we throw out the draft message data if there // is one but we also loaded the self metadata and need to let our listeners know. dispatchChanged(SELF_CHANGED); } } /** * Create a MessageData object containing a copy of all the parts in this DraftMessageData. * * @param clearLocalCopy whether we should clear out the in-memory copy of the parts. If we * are simply pausing/resuming and not sending the message, then we can keep * @return the MessageData for the draft, null if self id is not set */ public MessageData createMessageWithCurrentAttachments(final boolean clearLocalCopy) { MessageData message = null; if (getIsMms()) { message = MessageData.createDraftMmsMessage(mConversationId, mSelfId, mMessageText, mMessageSubject); for (final MessagePartData attachment : mAttachments) { message.addPart(attachment); } } else { message = MessageData.createDraftSmsMessage(mConversationId, mSelfId, mMessageText); } if (clearLocalCopy) { // The message now owns all the attachments and the text... clearLocalDraftCopy(); dispatchChanged(ALL_CHANGED); } else { // The draft message becomes a cached copy for UI. mIsDraftCachedCopy = true; } return message; } private void clearLocalDraftCopy() { mIsDraftCachedCopy = false; mAttachments.clear(); setMessageText(""); setMessageSubject(""); } public String getConversationId() { return mConversationId; } public String getMessageText() { return mMessageText; } public String getMessageSubject() { return mMessageSubject; } public boolean getIsMms() { final int selfSubId = getSelfSubId(); return MmsSmsUtils.getRequireMmsForEmailAddress(mIncludeEmailAddress, selfSubId) || (mIsGroupConversation && MmsUtils.groupMmsEnabled(selfSubId)) || mMessageTextStats.getMessageLengthRequiresMms() || !mAttachments.isEmpty() || !TextUtils.isEmpty(mMessageSubject); } public boolean getIsGroupMmsConversation() { return getIsMms() && mIsGroupConversation; } public String getSelfId() { return mSelfId; } public int getNumMessagesToBeSent() { return mMessageTextStats.getNumMessagesToBeSent(); } public int getCodePointsRemainingInCurrentMessage() { return mMessageTextStats.getCodePointsRemainingInCurrentMessage(); } public int getSelfSubId() { return mSubscriptionDataProvider == null ? ParticipantData.DEFAULT_SELF_SUB_ID : mSubscriptionDataProvider.getConversationSelfSubId(); } private void setMessageText(final String messageText, final boolean notify) { mMessageText = messageText; mMessageTextStats.updateMessageTextStats(getSelfSubId(), mMessageText); if (notify) { dispatchChanged(MESSAGE_TEXT_CHANGED); } } private void setMessageSubject(final String subject, final boolean notify) { mMessageSubject = subject; if (notify) { dispatchChanged(MESSAGE_SUBJECT_CHANGED); } } public void setMessageText(final String messageText) { setMessageText(messageText, false); } public void setMessageSubject(final String subject) { setMessageSubject(subject, false); } public void addAttachments(final Collection attachments) { // If the incoming attachments contains a single-only attachment, we need to clear // the existing attachments. for (final MessagePartData data : attachments) { if (data.isSinglePartOnly()) { // clear any existing attachments because the attachment we're adding can only // exist by itself. destroyAttachments(); break; } } // If the existing attachments contain a single-only attachment, we need to clear the // existing attachments to make room for the incoming attachment. for (final MessagePartData data : mAttachments) { if (data.isSinglePartOnly()) { // clear any existing attachments because the single attachment can only exist // by itself destroyAttachments(); break; } } // If any of the pending attachments contain a single-only attachment, we need to clear the // existing attachments to make room for the incoming attachment. for (final MessagePartData data : mPendingAttachments) { if (data.isSinglePartOnly()) { // clear any existing attachments because the single attachment can only exist // by itself destroyAttachments(); break; } } boolean reachedLimit = false; for (final MessagePartData data : attachments) { // Don't break out of loop even if limit has been reached so we can destroy all // of the over-limit attachments. reachedLimit |= addOneAttachmentNoNotify(data); } if (reachedLimit) { dispatchAttachmentLimitReached(); } dispatchChanged(ATTACHMENTS_CHANGED); } public boolean containsAttachment(final Uri contentUri) { for (final MessagePartData existingAttachment : mAttachments) { if (existingAttachment.getContentUri().equals(contentUri)) { return true; } } for (final PendingAttachmentData pendingAttachment : mPendingAttachments) { if (pendingAttachment.getContentUri().equals(contentUri)) { return true; } } return false; } /** * Try to add one attachment to the attachment list, while guarding against duplicates and * going over the limit. * @return true if the attachment limit was reached, false otherwise */ private boolean addOneAttachmentNoNotify(final MessagePartData attachment) { Assert.isTrue(attachment.isAttachment()); final boolean reachedLimit = getAttachmentCount() >= getAttachmentLimit(); if (reachedLimit || containsAttachment(attachment.getContentUri())) { // Never go over the limit. Never add duplicated attachments. attachment.destroyAsync(); return reachedLimit; } else { addAttachment(attachment, null /*pendingAttachment*/); return false; } } private void addAttachment(final MessagePartData attachment, final PendingAttachmentData pendingAttachment) { if (attachment != null && attachment.isSinglePartOnly()) { // clear any existing attachments because the attachment we're adding can only // exist by itself. destroyAttachments(); } if (pendingAttachment != null && pendingAttachment.isSinglePartOnly()) { // clear any existing attachments because the attachment we're adding can only // exist by itself. destroyAttachments(); } // If the existing attachments contain a single-only attachment, we need to clear the // existing attachments to make room for the incoming attachment. for (final MessagePartData data : mAttachments) { if (data.isSinglePartOnly()) { // clear any existing attachments because the single attachment can only exist // by itself destroyAttachments(); break; } } // If any of the pending attachments contain a single-only attachment, we need to clear the // existing attachments to make room for the incoming attachment. for (final MessagePartData data : mPendingAttachments) { if (data.isSinglePartOnly()) { // clear any existing attachments because the single attachment can only exist // by itself destroyAttachments(); break; } } if (attachment != null) { mAttachments.add(attachment); } else if (pendingAttachment != null) { mPendingAttachments.add(pendingAttachment); } } public void addPendingAttachment(final PendingAttachmentData pendingAttachment, final BindingBase binding) { final boolean reachedLimit = addOnePendingAttachmentNoNotify(pendingAttachment, binding.getBindingId()); if (reachedLimit) { dispatchAttachmentLimitReached(); } dispatchChanged(ATTACHMENTS_CHANGED); } /** * Try to add one pending attachment, while guarding against duplicates and * going over the limit. * @return true if the attachment limit was reached, false otherwise */ private boolean addOnePendingAttachmentNoNotify(final PendingAttachmentData pendingAttachment, final String bindingId) { final boolean reachedLimit = getAttachmentCount() >= getAttachmentLimit(); if (reachedLimit || containsAttachment(pendingAttachment.getContentUri())) { // Never go over the limit. Never add duplicated attachments. pendingAttachment.destroyAsync(); return reachedLimit; } else { Assert.isTrue(!mPendingAttachments.contains(pendingAttachment)); Assert.equals(PendingAttachmentData.STATE_PENDING, pendingAttachment.getCurrentState()); addAttachment(null /*attachment*/, pendingAttachment); pendingAttachment.loadAttachmentForDraft(this, bindingId); return false; } } public void setSelfId(final String selfId, final boolean notify) { LogUtil.d(LogUtil.BUGLE_TAG, "DraftMessageData: set selfId=" + selfId + " for conversationId=" + mConversationId); mSelfId = selfId; if (notify) { dispatchChanged(SELF_CHANGED); } } public boolean hasAttachments() { return !mAttachments.isEmpty(); } public boolean hasPendingAttachments() { return !mPendingAttachments.isEmpty(); } private int getAttachmentCount() { return mAttachments.size() + mPendingAttachments.size(); } private int getVideoAttachmentCount() { int count = 0; for (MessagePartData part : mAttachments) { if (part.isVideo()) { count++; } } for (MessagePartData part : mPendingAttachments) { if (part.isVideo()) { count++; } } return count; } private int getAttachmentLimit() { return BugleGservices.get().getInt( BugleGservicesKeys.MMS_ATTACHMENT_LIMIT, BugleGservicesKeys.MMS_ATTACHMENT_LIMIT_DEFAULT); } public void removeAttachment(final MessagePartData attachment) { for (final MessagePartData existingAttachment : mAttachments) { if (existingAttachment.getContentUri().equals(attachment.getContentUri())) { mAttachments.remove(existingAttachment); existingAttachment.destroyAsync(); dispatchChanged(ATTACHMENTS_CHANGED); break; } } } public void removeExistingAttachments(final Set attachmentsToRemove) { boolean removed = false; final Iterator iterator = mAttachments.iterator(); while (iterator.hasNext()) { final MessagePartData existingAttachment = iterator.next(); if (attachmentsToRemove.contains(existingAttachment)) { iterator.remove(); existingAttachment.destroyAsync(); removed = true; } } if (removed) { dispatchChanged(ATTACHMENTS_CHANGED); } } public void removePendingAttachment(final PendingAttachmentData pendingAttachment) { for (final PendingAttachmentData existingAttachment : mPendingAttachments) { if (existingAttachment.getContentUri().equals(pendingAttachment.getContentUri())) { mPendingAttachments.remove(pendingAttachment); pendingAttachment.destroyAsync(); dispatchChanged(ATTACHMENTS_CHANGED); break; } } } public void updatePendingAttachment(final MessagePartData updatedAttachment, final PendingAttachmentData pendingAttachment) { for (final PendingAttachmentData existingAttachment : mPendingAttachments) { if (existingAttachment.getContentUri().equals(pendingAttachment.getContentUri())) { mPendingAttachments.remove(pendingAttachment); if (pendingAttachment.isSinglePartOnly()) { updatedAttachment.setSinglePartOnly(true); } mAttachments.add(updatedAttachment); dispatchChanged(ATTACHMENTS_CHANGED); return; } } // If we are here, this means the pending attachment has been dropped before the task // to load it was completed. In this case destroy the temporarily staged file since it // is no longer needed. updatedAttachment.destroyAsync(); } /** * Remove the attachments from the draft and notify any listeners. * @param flags typically this will be ATTACHMENTS_CHANGED. When attachments are cleared in a * widget, flags will also contain WIDGET_CHANGED. */ public void clearAttachments(final int flags) { destroyAttachments(); dispatchChanged(flags); } public List getReadOnlyAttachments() { return mReadOnlyAttachments; } public List getReadOnlyPendingAttachments() { return mReadOnlyPendingAttachments; } public boolean loadFromStorage(final BindingBase binding, final MessageData optionalIncomingDraft, boolean clearLocalDraft) { LogUtil.d(LogUtil.BUGLE_TAG, "DraftMessageData: " + (optionalIncomingDraft == null ? "loading" : "setting") + " for conversationId=" + mConversationId); if (clearLocalDraft) { clearLocalDraftCopy(); } final boolean isDraftCachedCopy = mIsDraftCachedCopy; mIsDraftCachedCopy = false; // Before reading message from db ensure the caller is bound to us (and knows the id) if (mMonitor == null && !isDraftCachedCopy && isBound(binding.getBindingId())) { mMonitor = ReadDraftDataAction.readDraftData(mConversationId, optionalIncomingDraft, binding.getBindingId(), this); return true; } return false; } /** * Saves the current draft to db. This will save the draft and drop any pending attachments * we have. The UI typically goes into the background when this is called, and instead of * trying to persist the state of the pending attachments (the app may be killed, the activity * may be destroyed), we simply drop the pending attachments for consistency. */ public void saveToStorage(final BindingBase binding) { saveToStorageInternal(binding); dropPendingAttachments(); } private void saveToStorageInternal(final BindingBase binding) { // Create MessageData to store to db, but don't clear the in-memory copy so UI will // continue to display it. // If self id is null then we'll not attempt to change the conversation's self id. final MessageData message = createMessageWithCurrentAttachments(false /* clearLocalCopy */); // Before writing message to db ensure the caller is bound to us (and knows the id) if (isBound(binding.getBindingId())){ WriteDraftMessageAction.writeDraftMessage(mConversationId, message); } } /** * Called when we are ready to send the message. This will assemble/return the MessageData for * sending and clear the local draft data, both from memory and from DB. This will also bind * the message data with a self Id through which the message will be sent. * * @param binding the binding object from our consumer. We need to make sure we are still bound * to that binding before saving to storage. */ public MessageData prepareMessageForSending(final BindingBase binding) { // We can't send the message while there's still stuff pending. Assert.isTrue(!hasPendingAttachments()); mSending = true; // Assembles the message to send and empty working draft data. // If self id is null then message is sent with conversation's self id. final MessageData messageToSend = createMessageWithCurrentAttachments(true /* clearLocalCopy */); // Note sending message will empty the draft data in DB. mSending = false; return messageToSend; } public boolean isSending() { return mSending; } @Override // ReadDraftMessageActionListener.onReadDraftMessageSucceeded public void onReadDraftDataSucceeded(final ReadDraftDataAction action, final Object data, final MessageData message, final ConversationListItemData conversation) { final String bindingId = (String) data; // Before passing draft message on to ui ensure the data is bound to the same bindingid if (isBound(bindingId)) { mSelfId = message.getSelfId(); mIsGroupConversation = conversation.getIsGroup(); mIncludeEmailAddress = conversation.getIncludeEmailAddress(); updateFromMessageData(message, bindingId); LogUtil.d(LogUtil.BUGLE_TAG, "DraftMessageData: draft loaded. " + "conversationId=" + mConversationId + " selfId=" + mSelfId); } else { LogUtil.w(LogUtil.BUGLE_TAG, "DraftMessageData: draft loaded but not bound. " + "conversationId=" + mConversationId); } mMonitor = null; } @Override // ReadDraftMessageActionListener.onReadDraftDataFailed public void onReadDraftDataFailed(final ReadDraftDataAction action, final Object data) { LogUtil.w(LogUtil.BUGLE_TAG, "DraftMessageData: draft not loaded. " + "conversationId=" + mConversationId); // The draft is now synced with actual MessageData and no longer a cached copy. mIsDraftCachedCopy = false; // Just clear the monitor - no update to draft data mMonitor = null; } /** * Check if Bugle is default sms app * @return */ public boolean getIsDefaultSmsApp() { return PhoneUtils.getDefault().isDefaultSmsApp(); } @Override //BindableData.unregisterListeners protected void unregisterListeners() { if (mMonitor != null) { mMonitor.unregister(); } mMonitor = null; mListeners.clear(); } private void destroyAttachments() { for (final MessagePartData attachment : mAttachments) { attachment.destroyAsync(); } mAttachments.clear(); mPendingAttachments.clear(); } private void dispatchChanged(final int changeFlags) { // No change is expected to be made to the draft if it is in cached copy state. if (mIsDraftCachedCopy) { return; } // Any change in the draft will cancel any pending draft checking task, since the // size/status of the draft may have changed. if (mCheckDraftForSendTask != null) { mCheckDraftForSendTask.cancel(true /* mayInterruptIfRunning */); mCheckDraftForSendTask = null; } mListeners.onDraftChanged(this, changeFlags); } private void dispatchAttachmentLimitReached() { mListeners.onDraftAttachmentLimitReached(this); } /** * Drop any pending attachments that haven't finished. This is called after the UI goes to * the background and we persist the draft data to the database. */ private void dropPendingAttachments() { mPendingAttachments.clear(); } private boolean isDraftEmpty() { return TextUtils.isEmpty(mMessageText) && mAttachments.isEmpty() && TextUtils.isEmpty(mMessageSubject); } public boolean isCheckingDraft() { return mCheckDraftForSendTask != null && !mCheckDraftForSendTask.isCancelled(); } public void checkDraftForAction(final boolean checkMessageSize, final int selfSubId, final CheckDraftTaskCallback callback, final Binding binding) { new CheckDraftForSendTask(checkMessageSize, selfSubId, callback, binding) .executeOnThreadPool((Void) null); } /** * Allows us to have multiple data listeners for DraftMessageData */ private class DraftMessageDataEventDispatcher extends ArrayList implements DraftMessageDataListener { @Override @RunsOnMainThread public void onDraftChanged(DraftMessageData data, int changeFlags) { Assert.isMainThread(); for (final DraftMessageDataListener listener : this) { listener.onDraftChanged(data, changeFlags); } } @Override @RunsOnMainThread public void onDraftAttachmentLimitReached(DraftMessageData data) { Assert.isMainThread(); for (final DraftMessageDataListener listener : this) { listener.onDraftAttachmentLimitReached(data); } } @Override @RunsOnMainThread public void onDraftAttachmentLoadFailed() { Assert.isMainThread(); for (final DraftMessageDataListener listener : this) { listener.onDraftAttachmentLoadFailed(); } } } public interface CheckDraftTaskCallback { void onDraftChecked(DraftMessageData data, int result); } public class CheckDraftForSendTask extends SafeAsyncTask { public static final int RESULT_PASSED = 0; public static final int RESULT_HAS_PENDING_ATTACHMENTS = 1; public static final int RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS = 2; public static final int RESULT_MESSAGE_OVER_LIMIT = 3; public static final int RESULT_VIDEO_ATTACHMENT_LIMIT_EXCEEDED = 4; public static final int RESULT_SIM_NOT_READY = 5; private final boolean mCheckMessageSize; private final int mSelfSubId; private final CheckDraftTaskCallback mCallback; private final String mBindingId; private final List mAttachmentsCopy; private int mPreExecuteResult = RESULT_PASSED; public CheckDraftForSendTask(final boolean checkMessageSize, final int selfSubId, final CheckDraftTaskCallback callback, final Binding binding) { mCheckMessageSize = checkMessageSize; mSelfSubId = selfSubId; mCallback = callback; mBindingId = binding.getBindingId(); // Obtain an immutable copy of the attachment list so we can operate on it in the // background thread. mAttachmentsCopy = new ArrayList(mAttachments); mCheckDraftForSendTask = this; } @Override protected void onPreExecute() { // Perform checking work that can happen on the main thread. if (hasPendingAttachments()) { mPreExecuteResult = RESULT_HAS_PENDING_ATTACHMENTS; return; } if (getIsGroupMmsConversation()) { try { if (TextUtils.isEmpty(PhoneUtils.get(mSelfSubId).getSelfRawNumber(true))) { mPreExecuteResult = RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS; return; } } catch (IllegalStateException e) { // This happens when there is no active subscription, e.g. on Nova // when the phone switches carrier. mPreExecuteResult = RESULT_SIM_NOT_READY; return; } } if (getVideoAttachmentCount() > MmsUtils.MAX_VIDEO_ATTACHMENT_COUNT) { mPreExecuteResult = RESULT_VIDEO_ATTACHMENT_LIMIT_EXCEEDED; return; } } @Override protected Integer doInBackgroundTimed(Void... params) { if (mPreExecuteResult != RESULT_PASSED) { return mPreExecuteResult; } if (mCheckMessageSize && getIsMessageOverLimit()) { return RESULT_MESSAGE_OVER_LIMIT; } return RESULT_PASSED; } @Override protected void onPostExecute(Integer result) { mCheckDraftForSendTask = null; // Only call back if we are bound to the original binding. if (isBound(mBindingId) && !isCancelled()) { mCallback.onDraftChecked(DraftMessageData.this, result); } else { if (!isBound(mBindingId)) { LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: draft not bound"); } if (isCancelled()) { LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: draft is cancelled"); } } } @Override protected void onCancelled() { mCheckDraftForSendTask = null; } /** * 1. Check if the draft message contains too many attachments to send * 2. Computes the minimum size that this message could be compressed/downsampled/encoded * before sending and check if it meets the carrier max size for sending. * @see MessagePartData#getMinimumSizeInBytesForSending() */ @DoesNotRunOnMainThread private boolean getIsMessageOverLimit() { Assert.isNotMainThread(); if (mAttachmentsCopy.size() > getAttachmentLimit()) { return true; } // Aggregate the size from all the attachments. long totalSize = 0; for (final MessagePartData attachment : mAttachmentsCopy) { totalSize += attachment.getMinimumSizeInBytesForSending(); } return totalSize > MmsConfig.get(mSelfSubId).getMaxMessageSize(); } } public void onPendingAttachmentLoadFailed(PendingAttachmentData data) { mListeners.onDraftAttachmentLoadFailed(); } }