blob: f63c27fc7b63dd64ea9b54d1045e9c962ced58bb [file] [log] [blame]
/*
* 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();
}
}