| /* |
| * 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<MessagePartData> mAttachments; |
| |
| /** A read-only wrapper on mAttachments surfaced to the UI layer for rendering */ |
| private final List<MessagePartData> 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<PendingAttachmentData> mPendingAttachments; |
| |
| /** A read-only wrapper on mPendingAttachments surfaced to the UI layer for rendering */ |
| private final List<PendingAttachmentData> 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<MessagePartData>(); |
| mReadOnlyAttachments = Collections.unmodifiableList(mAttachments); |
| mPendingAttachments = new ArrayList<PendingAttachmentData>(); |
| 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<? extends MessagePartData> 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()); |
| // Check duplication. |
| for (final MessagePartData existingAttachment : mAttachments) { |
| if (existingAttachment.getContentUri().equals(attachment.getContentUri())) { |
| // Destroy existing attachment and replace with new attachment instead of destroying |
| // new one so that mSelectedImages in GalleryGridView could be maintained correctly. |
| mAttachments.remove(existingAttachment); |
| existingAttachment.destroyAsync(); |
| addAttachment(attachment, null /*pendingAttachment*/); |
| return false; |
| } |
| } |
| |
| final boolean reachedLimit = getAttachmentCount() >= getAttachmentLimit(); |
| if (reachedLimit) { |
| // Never go over the limit. |
| attachment.destroyAsync(); |
| return true; |
| } 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<DraftMessageData> 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<MessagePartData> attachmentsToRemove) { |
| boolean removed = false; |
| final Iterator<MessagePartData> 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<MessagePartData> getReadOnlyAttachments() { |
| return mReadOnlyAttachments; |
| } |
| |
| public List<PendingAttachmentData> getReadOnlyPendingAttachments() { |
| return mReadOnlyPendingAttachments; |
| } |
| |
| public boolean loadFromStorage(final BindingBase<DraftMessageData> 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<DraftMessageData> binding) { |
| saveToStorageInternal(binding); |
| dropPendingAttachments(); |
| } |
| |
| private void saveToStorageInternal(final BindingBase<DraftMessageData> 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<DraftMessageData> 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<DraftMessageData> binding) { |
| new CheckDraftForSendTask(checkMessageSize, selfSubId, callback, binding) |
| .executeOnThreadPool((Void) null); |
| } |
| |
| /** |
| * Allows us to have multiple data listeners for DraftMessageData |
| */ |
| private class DraftMessageDataEventDispatcher |
| extends ArrayList<DraftMessageDataListener> |
| 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<Void, Void, Integer> { |
| 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<MessagePartData> mAttachmentsCopy; |
| private int mPreExecuteResult = RESULT_PASSED; |
| |
| public CheckDraftForSendTask(final boolean checkMessageSize, final int selfSubId, |
| final CheckDraftTaskCallback callback, final Binding<DraftMessageData> 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<MessagePartData>(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(); |
| } |
| } |