blob: c35927e26ec35be4ebac363532177cf302fad499 [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.ui.conversation;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import android.text.Editable;
import android.text.Html;
import android.text.InputFilter;
import android.text.InputFilter.LengthFilter;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.format.Formatter;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.KeyEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.EditorInfo;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.datamodel.binding.Binding;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.datamodel.binding.ImmutableBindingRef;
import com.android.messaging.datamodel.data.ConversationData;
import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener;
import com.android.messaging.datamodel.data.ConversationData.SimpleConversationDataListener;
import com.android.messaging.datamodel.data.DraftMessageData;
import com.android.messaging.datamodel.data.DraftMessageData.CheckDraftForSendTask;
import com.android.messaging.datamodel.data.DraftMessageData.CheckDraftTaskCallback;
import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageDataListener;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.MessagePartData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.datamodel.data.PendingAttachmentData;
import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
import com.android.messaging.sms.MmsConfig;
import com.android.messaging.ui.AttachmentPreview;
import com.android.messaging.ui.BugleActionBarActivity;
import com.android.messaging.ui.PlainTextEditText;
import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputSink;
import com.android.messaging.util.AccessibilityUtil;
import com.android.messaging.util.Assert;
import com.android.messaging.util.AvatarUriUtil;
import com.android.messaging.util.BuglePrefs;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.MediaUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.SafeAsyncTask;
import com.android.messaging.util.UiUtils;
import com.android.messaging.util.UriUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* This view contains the UI required to generate and send messages.
*/
public class ComposeMessageView extends LinearLayout
implements TextView.OnEditorActionListener, DraftMessageDataListener, TextWatcher,
ConversationInputSink {
public interface IComposeMessageViewHost extends
DraftMessageData.DraftMessageSubscriptionDataProvider {
void sendMessage(MessageData message);
void onComposeEditTextFocused();
void onAttachmentsCleared();
void onAttachmentsChanged(final boolean haveAttachments);
void displayPhoto(Uri photoUri, Rect imageBounds, boolean isDraft);
void promptForSelfPhoneNumber();
boolean isReadyForAction();
void warnOfMissingActionConditions(final boolean sending,
final Runnable commandToRunAfterActionConditionResolved);
void warnOfExceedingMessageLimit(final boolean showAttachmentChooser,
boolean tooManyVideos);
void notifyOfAttachmentLoadFailed();
void showAttachmentChooser();
boolean shouldShowSubjectEditor();
boolean shouldHideAttachmentsWhenSimSelectorShown();
Uri getSelfSendButtonIconUri();
int overrideCounterColor();
int getAttachmentsClearedFlags();
}
public static final int CODEPOINTS_REMAINING_BEFORE_COUNTER_SHOWN = 10;
// There is no draft and there is no need for the SIM selector
private static final int SEND_WIDGET_MODE_SELF_AVATAR = 1;
// There is no draft but we need to show the SIM selector
private static final int SEND_WIDGET_MODE_SIM_SELECTOR = 2;
// There is a draft
private static final int SEND_WIDGET_MODE_SEND_BUTTON = 3;
private PlainTextEditText mComposeEditText;
private PlainTextEditText mComposeSubjectText;
private TextView mMessageBodySize;
private TextView mMmsIndicator;
private SimIconView mSelfSendIcon;
private ImageButton mSendButton;
private View mSubjectView;
private ImageButton mDeleteSubjectButton;
private AttachmentPreview mAttachmentPreview;
private ImageButton mAttachMediaButton;
private final Binding<DraftMessageData> mBinding;
private IComposeMessageViewHost mHost;
private final Context mOriginalContext;
private int mSendWidgetMode = SEND_WIDGET_MODE_SELF_AVATAR;
// Shared data model object binding from the conversation.
private ImmutableBindingRef<ConversationData> mConversationDataModel;
// Centrally manages all the mutual exclusive UI components accepting user input, i.e.
// media picker, IME keyboard and SIM selector.
private ConversationInputManager mInputManager;
private final ConversationDataListener mDataListener = new SimpleConversationDataListener() {
@Override
public void onConversationMetadataUpdated(ConversationData data) {
mConversationDataModel.ensureBound(data);
updateVisualsOnDraftChanged();
}
@Override
public void onConversationParticipantDataLoaded(ConversationData data) {
mConversationDataModel.ensureBound(data);
updateVisualsOnDraftChanged();
}
@Override
public void onSubscriptionListDataLoaded(ConversationData data) {
mConversationDataModel.ensureBound(data);
updateOnSelfSubscriptionChange();
updateVisualsOnDraftChanged();
}
};
public ComposeMessageView(final Context context, final AttributeSet attrs) {
super(new ContextThemeWrapper(context, R.style.ColorAccentBlueOverrideStyle), attrs);
mOriginalContext = context;
mBinding = BindingBase.createBinding(this);
}
/**
* Host calls this to bind view to DraftMessageData object
*/
public void bind(final DraftMessageData data, final IComposeMessageViewHost host) {
mHost = host;
mBinding.bind(data);
data.addListener(this);
data.setSubscriptionDataProvider(host);
final int counterColor = mHost.overrideCounterColor();
if (counterColor != -1) {
mMessageBodySize.setTextColor(counterColor);
}
}
/**
* Host calls this to unbind view
*/
public void unbind() {
mBinding.unbind();
mHost = null;
mInputManager.onDetach();
}
@Override
protected void onFinishInflate() {
mComposeEditText = (PlainTextEditText) findViewById(
R.id.compose_message_text);
mComposeEditText.setOnEditorActionListener(this);
mComposeEditText.addTextChangedListener(this);
mComposeEditText.setOnFocusChangeListener(new OnFocusChangeListener() {
@Override
public void onFocusChange(final View v, final boolean hasFocus) {
if (v == mComposeEditText && hasFocus) {
mHost.onComposeEditTextFocused();
}
}
});
mComposeEditText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
if (mHost.shouldHideAttachmentsWhenSimSelectorShown()) {
hideSimSelector();
}
}
});
// onFinishInflate() is called before self is loaded from db. We set the default text
// limit here, and apply the real limit later in updateOnSelfSubscriptionChange().
mComposeEditText.setFilters(new InputFilter[] {
new LengthFilter(MmsConfig.get(ParticipantData.DEFAULT_SELF_SUB_ID)
.getMaxTextLimit()) });
mSelfSendIcon = (SimIconView) findViewById(R.id.self_send_icon);
mSelfSendIcon.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
boolean shown = mInputManager.toggleSimSelector(true /* animate */,
getSelfSubscriptionListEntry());
hideAttachmentsWhenShowingSims(shown);
}
});
mSelfSendIcon.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(final View v) {
if (mHost.shouldShowSubjectEditor()) {
showSubjectEditor();
} else {
boolean shown = mInputManager.toggleSimSelector(true /* animate */,
getSelfSubscriptionListEntry());
hideAttachmentsWhenShowingSims(shown);
}
return true;
}
});
mComposeSubjectText = (PlainTextEditText) findViewById(
R.id.compose_subject_text);
// We need the listener to change the avatar to the send button when the user starts
// typing a subject without a message.
mComposeSubjectText.addTextChangedListener(this);
// onFinishInflate() is called before self is loaded from db. We set the default text
// limit here, and apply the real limit later in updateOnSelfSubscriptionChange().
mComposeSubjectText.setFilters(new InputFilter[] {
new LengthFilter(MmsConfig.get(ParticipantData.DEFAULT_SELF_SUB_ID)
.getMaxSubjectLength())});
mDeleteSubjectButton = (ImageButton) findViewById(R.id.delete_subject_button);
mDeleteSubjectButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View clickView) {
hideSubjectEditor();
mComposeSubjectText.setText(null);
mBinding.getData().setMessageSubject(null);
}
});
mSubjectView = findViewById(R.id.subject_view);
mSendButton = (ImageButton) findViewById(R.id.send_message_button);
mSendButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View clickView) {
sendMessageInternal(true /* checkMessageSize */);
}
});
mSendButton.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(final View arg0) {
boolean shown = mInputManager.toggleSimSelector(true /* animate */,
getSelfSubscriptionListEntry());
hideAttachmentsWhenShowingSims(shown);
if (mHost.shouldShowSubjectEditor()) {
showSubjectEditor();
}
return true;
}
});
mSendButton.setAccessibilityDelegate(new AccessibilityDelegate() {
@Override
public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
super.onPopulateAccessibilityEvent(host, event);
// When the send button is long clicked, we want TalkBack to announce the real
// action (select SIM or edit subject), as opposed to "long press send button."
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_LONG_CLICKED) {
event.getText().clear();
event.getText().add(getResources()
.getText(shouldShowSimSelector(mConversationDataModel.getData()) ?
R.string.send_button_long_click_description_with_sim_selector :
R.string.send_button_long_click_description_no_sim_selector));
// Make this an announcement so TalkBack will read our custom message.
event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
}
}
});
mAttachMediaButton =
(ImageButton) findViewById(R.id.attach_media_button);
mAttachMediaButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View clickView) {
// Showing the media picker is treated as starting to compose the message.
mInputManager.showHideMediaPicker(true /* show */, true /* animate */);
}
});
mAttachmentPreview = (AttachmentPreview) findViewById(R.id.attachment_draft_view);
mAttachmentPreview.setComposeMessageView(this);
mMessageBodySize = (TextView) findViewById(R.id.message_body_size);
mMmsIndicator = (TextView) findViewById(R.id.mms_indicator);
}
private void hideAttachmentsWhenShowingSims(final boolean simPickerVisible) {
if (!mHost.shouldHideAttachmentsWhenSimSelectorShown()) {
return;
}
final boolean haveAttachments = mBinding.getData().hasAttachments();
if (simPickerVisible && haveAttachments) {
mHost.onAttachmentsChanged(false);
mAttachmentPreview.hideAttachmentPreview();
} else {
mHost.onAttachmentsChanged(haveAttachments);
mAttachmentPreview.onAttachmentsChanged(mBinding.getData());
}
}
public void setInputManager(final ConversationInputManager inputManager) {
mInputManager = inputManager;
}
public void setConversationDataModel(final ImmutableBindingRef<ConversationData> refDataModel) {
mConversationDataModel = refDataModel;
mConversationDataModel.getData().addConversationDataListener(mDataListener);
}
ImmutableBindingRef<DraftMessageData> getDraftDataModel() {
return BindingBase.createBindingReference(mBinding);
}
// returns true if it actually shows the subject editor and false if already showing
private boolean showSubjectEditor() {
// show the subject editor
if (mSubjectView.getVisibility() == View.GONE) {
mSubjectView.setVisibility(View.VISIBLE);
mSubjectView.requestFocus();
return true;
}
return false;
}
private void hideSubjectEditor() {
mSubjectView.setVisibility(View.GONE);
mComposeEditText.requestFocus();
}
/**
* {@inheritDoc} from TextView.OnEditorActionListener
*/
@Override // TextView.OnEditorActionListener.onEditorAction
public boolean onEditorAction(final TextView view, final int actionId, final KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_SEND) {
sendMessageInternal(true /* checkMessageSize */);
return true;
}
return false;
}
private void sendMessageInternal(final boolean checkMessageSize) {
LogUtil.i(LogUtil.BUGLE_TAG, "UI initiated message sending in conversation " +
mBinding.getData().getConversationId());
if (mBinding.getData().isCheckingDraft()) {
// Don't send message if we are currently checking draft for sending.
LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: still checking draft");
return;
}
// Check the host for pre-conditions about any action.
if (mHost.isReadyForAction()) {
mInputManager.showHideSimSelector(false /* show */, true /* animate */);
final String messageToSend = mComposeEditText.getText().toString();
mBinding.getData().setMessageText(messageToSend);
final String subject = mComposeSubjectText.getText().toString();
mBinding.getData().setMessageSubject(subject);
// Asynchronously check the draft against various requirements before sending.
mBinding.getData().checkDraftForAction(checkMessageSize,
mHost.getConversationSelfSubId(), new CheckDraftTaskCallback() {
@Override
public void onDraftChecked(DraftMessageData data, int result) {
mBinding.ensureBound(data);
switch (result) {
case CheckDraftForSendTask.RESULT_PASSED:
// Continue sending after check succeeded.
final MessageData message = mBinding.getData()
.prepareMessageForSending(mBinding);
if (message != null && message.hasContent()) {
playSentSound();
mHost.sendMessage(message);
hideSubjectEditor();
if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
AccessibilityUtil.announceForAccessibilityCompat(
ComposeMessageView.this, null,
R.string.sending_message);
}
}
break;
case CheckDraftForSendTask.RESULT_HAS_PENDING_ATTACHMENTS:
// Cannot send while there's still attachment(s) being loaded.
UiUtils.showToastAtBottom(
R.string.cant_send_message_while_loading_attachments);
break;
case CheckDraftForSendTask.RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS:
mHost.promptForSelfPhoneNumber();
break;
case CheckDraftForSendTask.RESULT_MESSAGE_OVER_LIMIT:
Assert.isTrue(checkMessageSize);
mHost.warnOfExceedingMessageLimit(
true /*sending*/, false /* tooManyVideos */);
break;
case CheckDraftForSendTask.RESULT_VIDEO_ATTACHMENT_LIMIT_EXCEEDED:
Assert.isTrue(checkMessageSize);
mHost.warnOfExceedingMessageLimit(
true /*sending*/, true /* tooManyVideos */);
break;
case CheckDraftForSendTask.RESULT_SIM_NOT_READY:
// Cannot send if there is no active subscription
UiUtils.showToastAtBottom(
R.string.cant_send_message_without_active_subscription);
break;
default:
break;
}
}
}, mBinding);
} else {
mHost.warnOfMissingActionConditions(true /*sending*/,
new Runnable() {
@Override
public void run() {
sendMessageInternal(checkMessageSize);
}
});
}
}
public static void playSentSound() {
// Check if this setting is enabled before playing
final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
final Context context = Factory.get().getApplicationContext();
final String prefKey = context.getString(R.string.send_sound_pref_key);
final boolean defaultValue = context.getResources().getBoolean(
R.bool.send_sound_pref_default);
if (!prefs.getBoolean(prefKey, defaultValue)) {
return;
}
MediaUtil.get().playSound(context, R.raw.message_sent, null /* completionListener */);
}
/**
* {@inheritDoc} from DraftMessageDataListener
*/
@Override // From DraftMessageDataListener
public void onDraftChanged(final DraftMessageData data, final int changeFlags) {
// As this is called asynchronously when message read check bound before updating text
mBinding.ensureBound(data);
// We have to cache the values of the DraftMessageData because when we set
// mComposeEditText, its onTextChanged calls updateVisualsOnDraftChanged,
// which immediately reloads the text from the subject and message fields and replaces
// what's in the DraftMessageData.
final String subject = data.getMessageSubject();
final String message = data.getMessageText();
boolean hasAttachmentsChanged = false;
if ((changeFlags & DraftMessageData.MESSAGE_SUBJECT_CHANGED) ==
DraftMessageData.MESSAGE_SUBJECT_CHANGED) {
mComposeSubjectText.setText(subject);
// Set the cursor selection to the end since setText resets it to the start
mComposeSubjectText.setSelection(mComposeSubjectText.getText().length());
}
if ((changeFlags & DraftMessageData.MESSAGE_TEXT_CHANGED) ==
DraftMessageData.MESSAGE_TEXT_CHANGED) {
mComposeEditText.setText(message);
// Set the cursor selection to the end since setText resets it to the start
mComposeEditText.setSelection(mComposeEditText.getText().length());
}
if ((changeFlags & DraftMessageData.ATTACHMENTS_CHANGED) ==
DraftMessageData.ATTACHMENTS_CHANGED) {
final boolean haveAttachments = mAttachmentPreview.onAttachmentsChanged(data);
mHost.onAttachmentsChanged(haveAttachments);
hasAttachmentsChanged = true;
}
if ((changeFlags & DraftMessageData.SELF_CHANGED) == DraftMessageData.SELF_CHANGED) {
updateOnSelfSubscriptionChange();
}
updateVisualsOnDraftChanged(hasAttachmentsChanged);
}
@Override // From DraftMessageDataListener
public void onDraftAttachmentLimitReached(final DraftMessageData data) {
mBinding.ensureBound(data);
mHost.warnOfExceedingMessageLimit(false /* sending */, false /* tooManyVideos */);
}
private void updateOnSelfSubscriptionChange() {
// Refresh the length filters according to the selected self's MmsConfig.
mComposeEditText.setFilters(new InputFilter[] {
new LengthFilter(MmsConfig.get(mBinding.getData().getSelfSubId())
.getMaxTextLimit()) });
mComposeSubjectText.setFilters(new InputFilter[] {
new LengthFilter(MmsConfig.get(mBinding.getData().getSelfSubId())
.getMaxSubjectLength())});
}
@Override
public void onMediaItemsSelected(final Collection<MessagePartData> items) {
mBinding.getData().addAttachments(items);
announceMediaItemState(true /*isSelected*/);
}
@Override
public void onMediaItemsUnselected(final MessagePartData item) {
mBinding.getData().removeAttachment(item);
announceMediaItemState(false /*isSelected*/);
}
@Override
public void onPendingAttachmentAdded(final PendingAttachmentData pendingItem) {
mBinding.getData().addPendingAttachment(pendingItem, mBinding);
resumeComposeMessage();
}
private void announceMediaItemState(final boolean isSelected) {
final Resources res = getContext().getResources();
final String announcement = isSelected ? res.getString(
R.string.mediapicker_gallery_item_selected_content_description) :
res.getString(R.string.mediapicker_gallery_item_unselected_content_description);
AccessibilityUtil.announceForAccessibilityCompat(
this, null, announcement);
}
private void announceAttachmentState() {
if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
int attachmentCount = mBinding.getData().getReadOnlyAttachments().size()
+ mBinding.getData().getReadOnlyPendingAttachments().size();
final String announcement = getContext().getResources().getQuantityString(
R.plurals.attachment_changed_accessibility_announcement,
attachmentCount, attachmentCount);
AccessibilityUtil.announceForAccessibilityCompat(
this, null, announcement);
}
}
@Override
public void resumeComposeMessage() {
mComposeEditText.requestFocus();
mInputManager.showHideImeKeyboard(true, true);
announceAttachmentState();
}
public void clearAttachments() {
mBinding.getData().clearAttachments(mHost.getAttachmentsClearedFlags());
mHost.onAttachmentsCleared();
}
public void requestDraftMessage(boolean clearLocalDraft) {
mBinding.getData().loadFromStorage(mBinding, null, clearLocalDraft);
}
public void setDraftMessage(final MessageData message) {
mBinding.getData().loadFromStorage(mBinding, message, false);
}
public void writeDraftMessage() {
final String messageText = mComposeEditText.getText().toString();
mBinding.getData().setMessageText(messageText);
final String subject = mComposeSubjectText.getText().toString();
mBinding.getData().setMessageSubject(subject);
mBinding.getData().saveToStorage(mBinding);
}
private void updateConversationSelfId(final String selfId, final boolean notify) {
mBinding.getData().setSelfId(selfId, notify);
}
private Uri getSelfSendButtonIconUri() {
final Uri overridenSelfUri = mHost.getSelfSendButtonIconUri();
if (overridenSelfUri != null) {
return overridenSelfUri;
}
final SubscriptionListEntry subscriptionListEntry = getSelfSubscriptionListEntry();
if (subscriptionListEntry != null) {
return subscriptionListEntry.selectedIconUri;
}
// Fall back to default self-avatar in the base case.
final ParticipantData self = mConversationDataModel.getData().getDefaultSelfParticipant();
return self == null ? null : AvatarUriUtil.createAvatarUri(self);
}
private SubscriptionListEntry getSelfSubscriptionListEntry() {
return mConversationDataModel.getData().getSubscriptionEntryForSelfParticipant(
mBinding.getData().getSelfId(), false /* excludeDefault */);
}
private boolean isDataLoadedForMessageSend() {
// Check data loading prerequisites for sending a message.
return mConversationDataModel != null && mConversationDataModel.isBound() &&
mConversationDataModel.getData().getParticipantsLoaded();
}
private static class AsyncUpdateMessageBodySizeTask
extends SafeAsyncTask<List<MessagePartData>, Void, Long> {
private final Context mContext;
private final TextView mSizeTextView;
public AsyncUpdateMessageBodySizeTask(final Context context, final TextView tv) {
mContext = context;
mSizeTextView = tv;
}
@Override
protected Long doInBackgroundTimed(final List<MessagePartData>... params) {
final List<MessagePartData> attachments = params[0];
long totalSize = 0;
for (final MessagePartData attachment : attachments) {
final Uri contentUri = attachment.getContentUri();
if (contentUri != null) {
totalSize += UriUtil.getContentSize(attachment.getContentUri());
}
}
return totalSize;
}
@Override
protected void onPostExecute(Long size) {
if (mSizeTextView != null) {
mSizeTextView.setText(Formatter.formatFileSize(mContext, size));
mSizeTextView.setVisibility(View.VISIBLE);
}
}
}
private void updateVisualsOnDraftChanged() {
updateVisualsOnDraftChanged(false);
}
private void updateVisualsOnDraftChanged(boolean hasAttachmentsChanged) {
final String messageText = mComposeEditText.getText().toString();
final DraftMessageData draftMessageData = mBinding.getData();
draftMessageData.setMessageText(messageText);
final String subject = mComposeSubjectText.getText().toString();
draftMessageData.setMessageSubject(subject);
if (!TextUtils.isEmpty(subject)) {
mSubjectView.setVisibility(View.VISIBLE);
}
final boolean hasMessageText = (TextUtils.getTrimmedLength(messageText) > 0);
final boolean hasSubject = (TextUtils.getTrimmedLength(subject) > 0);
final boolean hasWorkingDraft = hasMessageText || hasSubject ||
mBinding.getData().hasAttachments();
final List<MessagePartData> attachments =
new ArrayList<MessagePartData>(draftMessageData.getReadOnlyAttachments());
if (draftMessageData.getIsMms()) { // MMS case
if (draftMessageData.hasAttachments()) {
if (hasAttachmentsChanged) {
// Calculate message attachments size and show it.
new AsyncUpdateMessageBodySizeTask(getContext(), mMessageBodySize)
.executeOnThreadPool(attachments, null, null);
} else {
// No update. Just show previous size.
mMessageBodySize.setVisibility(View.VISIBLE);
}
} else {
mMessageBodySize.setVisibility(View.INVISIBLE);
}
} else { // SMS case
// Update the SMS text counter.
final int messageCount = draftMessageData.getNumMessagesToBeSent();
final int codePointsRemaining =
draftMessageData.getCodePointsRemainingInCurrentMessage();
// Show the counter only if we are going to send more than one message OR we are getting
// close.
if (messageCount > 1
|| codePointsRemaining <= CODEPOINTS_REMAINING_BEFORE_COUNTER_SHOWN) {
// Update the remaining characters and number of messages required.
final String counterText =
messageCount > 1
? codePointsRemaining + " / " + messageCount
: String.valueOf(codePointsRemaining);
mMessageBodySize.setText(counterText);
mMessageBodySize.setVisibility(View.VISIBLE);
} else {
mMessageBodySize.setVisibility(View.INVISIBLE);
}
}
// Update the send message button. Self icon uri might be null if self participant data
// and/or conversation metadata hasn't been loaded by the host.
final Uri selfSendButtonUri = getSelfSendButtonIconUri();
int sendWidgetMode = SEND_WIDGET_MODE_SELF_AVATAR;
if (selfSendButtonUri != null) {
if (hasWorkingDraft && isDataLoadedForMessageSend()) {
UiUtils.revealOrHideViewWithAnimation(mSendButton, VISIBLE, null);
if (isOverriddenAvatarAGroup()) {
// If the host has overriden the avatar to show a group avatar where the
// send button sits, we have to hide the group avatar because it can be larger
// than the send button and pieces of the avatar will stick out from behind
// the send button.
UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, GONE, null);
}
mMmsIndicator.setVisibility(draftMessageData.getIsMms() ? VISIBLE : INVISIBLE);
sendWidgetMode = SEND_WIDGET_MODE_SEND_BUTTON;
} else {
mSelfSendIcon.setImageResourceUri(selfSendButtonUri);
if (isOverriddenAvatarAGroup()) {
UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, VISIBLE, null);
}
UiUtils.revealOrHideViewWithAnimation(mSendButton, GONE, null);
mMmsIndicator.setVisibility(INVISIBLE);
if (shouldShowSimSelector(mConversationDataModel.getData())) {
sendWidgetMode = SEND_WIDGET_MODE_SIM_SELECTOR;
}
}
} else {
mSelfSendIcon.setImageResourceUri(null);
}
if (mSendWidgetMode != sendWidgetMode || sendWidgetMode == SEND_WIDGET_MODE_SIM_SELECTOR) {
setSendButtonAccessibility(sendWidgetMode);
mSendWidgetMode = sendWidgetMode;
}
// Update the text hint on the message box depending on the attachment type.
final int attachmentCount = attachments.size();
if (attachmentCount == 0) {
final SubscriptionListEntry subscriptionListEntry =
mConversationDataModel.getData().getSubscriptionEntryForSelfParticipant(
mBinding.getData().getSelfId(), false /* excludeDefault */);
if (subscriptionListEntry == null) {
mComposeEditText.setHint(R.string.compose_message_view_hint_text);
} else {
mComposeEditText.setHint(Html.fromHtml(getResources().getString(
R.string.compose_message_view_hint_text_multi_sim,
subscriptionListEntry.displayName)));
}
} else {
int type = -1;
for (final MessagePartData attachment : attachments) {
int newType;
if (attachment.isImage()) {
newType = ContentType.TYPE_IMAGE;
} else if (attachment.isAudio()) {
newType = ContentType.TYPE_AUDIO;
} else if (attachment.isVideo()) {
newType = ContentType.TYPE_VIDEO;
} else if (attachment.isVCard()) {
newType = ContentType.TYPE_VCARD;
} else {
newType = ContentType.TYPE_OTHER;
}
if (type == -1) {
type = newType;
} else if (type != newType || type == ContentType.TYPE_OTHER) {
type = ContentType.TYPE_OTHER;
break;
}
}
switch (type) {
case ContentType.TYPE_IMAGE:
mComposeEditText.setHint(getResources().getQuantityString(
R.plurals.compose_message_view_hint_text_photo, attachmentCount));
break;
case ContentType.TYPE_AUDIO:
mComposeEditText.setHint(getResources().getQuantityString(
R.plurals.compose_message_view_hint_text_audio, attachmentCount));
break;
case ContentType.TYPE_VIDEO:
mComposeEditText.setHint(getResources().getQuantityString(
R.plurals.compose_message_view_hint_text_video, attachmentCount));
break;
case ContentType.TYPE_VCARD:
mComposeEditText.setHint(getResources().getQuantityString(
R.plurals.compose_message_view_hint_text_vcard, attachmentCount));
break;
case ContentType.TYPE_OTHER:
mComposeEditText.setHint(getResources().getQuantityString(
R.plurals.compose_message_view_hint_text_attachments, attachmentCount));
break;
default:
Assert.fail("Unsupported attachment type!");
break;
}
}
}
private void setSendButtonAccessibility(final int sendWidgetMode) {
switch (sendWidgetMode) {
case SEND_WIDGET_MODE_SELF_AVATAR:
// No send button and no SIM selector; the self send button is no longer
// important for accessibility.
mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
mSelfSendIcon.setContentDescription(null);
mSendButton.setVisibility(View.GONE);
setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SELF_AVATAR);
break;
case SEND_WIDGET_MODE_SIM_SELECTOR:
mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
mSelfSendIcon.setContentDescription(getSimContentDescription());
setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SIM_SELECTOR);
break;
case SEND_WIDGET_MODE_SEND_BUTTON:
mMmsIndicator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
mMmsIndicator.setContentDescription(null);
setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SEND_BUTTON);
break;
}
}
private String getSimContentDescription() {
final SubscriptionListEntry sub = getSelfSubscriptionListEntry();
if (sub != null) {
return getResources().getString(
R.string.sim_selector_button_content_description_with_selection,
sub.displayName);
} else {
return getResources().getString(
R.string.sim_selector_button_content_description);
}
}
// Set accessibility traversal order of the components in the send widget.
private void setSendWidgetAccessibilityTraversalOrder(final int mode) {
if (OsUtil.isAtLeastL_MR1()) {
mAttachMediaButton.setAccessibilityTraversalBefore(R.id.compose_message_text);
switch (mode) {
case SEND_WIDGET_MODE_SIM_SELECTOR:
mComposeEditText.setAccessibilityTraversalBefore(R.id.self_send_icon);
break;
case SEND_WIDGET_MODE_SEND_BUTTON:
mComposeEditText.setAccessibilityTraversalBefore(R.id.send_message_button);
break;
default:
break;
}
}
}
@Override
public void afterTextChanged(final Editable editable) {
}
@Override
public void beforeTextChanged(final CharSequence s, final int start, final int count,
final int after) {
if (mHost.shouldHideAttachmentsWhenSimSelectorShown()) {
hideSimSelector();
}
}
private void hideSimSelector() {
if (mInputManager.showHideSimSelector(false /* show */, true /* animate */)) {
// Now that the sim selector has been hidden, reshow the attachments if they
// have been hidden.
hideAttachmentsWhenShowingSims(false /*simPickerVisible*/);
}
}
@Override
public void onTextChanged(final CharSequence s, final int start, final int before,
final int count) {
final BugleActionBarActivity activity = (mOriginalContext instanceof BugleActionBarActivity)
? (BugleActionBarActivity) mOriginalContext : null;
if (activity != null && activity.getIsDestroyed()) {
LogUtil.v(LogUtil.BUGLE_TAG, "got onTextChanged after onDestroy");
// if we get onTextChanged after the activity is destroyed then, ah, wtf
// b/18176615
// This appears to have occurred as the result of orientation change.
return;
}
mBinding.ensureBound();
updateVisualsOnDraftChanged();
}
@Override
public PlainTextEditText getComposeEditText() {
return mComposeEditText;
}
public void displayPhoto(final Uri photoUri, final Rect imageBounds) {
mHost.displayPhoto(photoUri, imageBounds, true /* isDraft */);
}
public void updateConversationSelfIdOnExternalChange(final String selfId) {
updateConversationSelfId(selfId, true /* notify */);
}
/**
* The selfId of the conversation. As soon as the DraftMessageData successfully loads (i.e.
* getSelfId() is non-null), the selfId in DraftMessageData is treated as the sole source
* of truth for conversation self id since it reflects any pending self id change the user
* makes in the UI.
*/
public String getConversationSelfId() {
return mBinding.getData().getSelfId();
}
public void selectSim(SubscriptionListEntry subscriptionData) {
final String oldSelfId = getConversationSelfId();
final String newSelfId = subscriptionData.selfParticipantId;
Assert.notNull(newSelfId);
// Don't attempt to change self if self hasn't been loaded, or if self hasn't changed.
if (oldSelfId == null || TextUtils.equals(oldSelfId, newSelfId)) {
return;
}
updateConversationSelfId(newSelfId, true /* notify */);
}
public void hideAllComposeInputs(final boolean animate) {
mInputManager.hideAllInputs(animate);
}
public void saveInputState(final Bundle outState) {
mInputManager.onSaveInputState(outState);
}
public void resetMediaPickerState() {
mInputManager.resetMediaPickerState();
}
public boolean onBackPressed() {
return mInputManager.onBackPressed();
}
public boolean onNavigationUpPressed() {
return mInputManager.onNavigationUpPressed();
}
public boolean updateActionBar(final ActionBar actionBar) {
return mInputManager != null ? mInputManager.updateActionBar(actionBar) : false;
}
public static boolean shouldShowSimSelector(final ConversationData convData) {
return OsUtil.isAtLeastL_MR1() &&
convData.getSelfParticipantsCountExcludingDefault(true /* activeOnly */) > 1;
}
public void sendMessageIgnoreMessageSizeLimit() {
sendMessageInternal(false /* checkMessageSize */);
}
public void onAttachmentPreviewLongClicked() {
mHost.showAttachmentChooser();
}
@Override
public void onDraftAttachmentLoadFailed() {
mHost.notifyOfAttachmentLoadFailed();
}
private boolean isOverriddenAvatarAGroup() {
final Uri overridenSelfUri = mHost.getSelfSendButtonIconUri();
if (overridenSelfUri == null) {
return false;
}
return AvatarUriUtil.TYPE_GROUP_URI.equals(AvatarUriUtil.getAvatarType(overridenSelfUri));
}
@Override
public void setAccessibility(boolean enabled) {
if (enabled) {
mAttachMediaButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
mComposeEditText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
mSendButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
setSendButtonAccessibility(mSendWidgetMode);
} else {
mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
mComposeEditText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
mSendButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
mAttachMediaButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
}
}
}