Home | History | Annotate | Download | only in conversation
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.messaging.ui.conversation;
     17 
     18 import android.content.Context;
     19 import android.content.res.Resources;
     20 import android.graphics.Rect;
     21 import android.net.Uri;
     22 import android.os.Bundle;
     23 import android.support.v7.app.ActionBar;
     24 import android.text.Editable;
     25 import android.text.Html;
     26 import android.text.InputFilter;
     27 import android.text.InputFilter.LengthFilter;
     28 import android.text.TextUtils;
     29 import android.text.TextWatcher;
     30 import android.util.AttributeSet;
     31 import android.view.ContextThemeWrapper;
     32 import android.view.KeyEvent;
     33 import android.view.View;
     34 import android.view.accessibility.AccessibilityEvent;
     35 import android.view.inputmethod.EditorInfo;
     36 import android.widget.ImageButton;
     37 import android.widget.LinearLayout;
     38 import android.widget.TextView;
     39 
     40 import com.android.messaging.Factory;
     41 import com.android.messaging.R;
     42 import com.android.messaging.datamodel.binding.Binding;
     43 import com.android.messaging.datamodel.binding.BindingBase;
     44 import com.android.messaging.datamodel.binding.ImmutableBindingRef;
     45 import com.android.messaging.datamodel.data.ConversationData;
     46 import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener;
     47 import com.android.messaging.datamodel.data.ConversationData.SimpleConversationDataListener;
     48 import com.android.messaging.datamodel.data.DraftMessageData;
     49 import com.android.messaging.datamodel.data.DraftMessageData.CheckDraftForSendTask;
     50 import com.android.messaging.datamodel.data.DraftMessageData.CheckDraftTaskCallback;
     51 import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageDataListener;
     52 import com.android.messaging.datamodel.data.MessageData;
     53 import com.android.messaging.datamodel.data.MessagePartData;
     54 import com.android.messaging.datamodel.data.ParticipantData;
     55 import com.android.messaging.datamodel.data.PendingAttachmentData;
     56 import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
     57 import com.android.messaging.sms.MmsConfig;
     58 import com.android.messaging.ui.AttachmentPreview;
     59 import com.android.messaging.ui.BugleActionBarActivity;
     60 import com.android.messaging.ui.PlainTextEditText;
     61 import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputSink;
     62 import com.android.messaging.util.AccessibilityUtil;
     63 import com.android.messaging.util.Assert;
     64 import com.android.messaging.util.AvatarUriUtil;
     65 import com.android.messaging.util.BuglePrefs;
     66 import com.android.messaging.util.ContentType;
     67 import com.android.messaging.util.LogUtil;
     68 import com.android.messaging.util.MediaUtil;
     69 import com.android.messaging.util.OsUtil;
     70 import com.android.messaging.util.UiUtils;
     71 
     72 import java.util.Collection;
     73 import java.util.List;
     74 
     75 /**
     76  * This view contains the UI required to generate and send messages.
     77  */
     78 public class ComposeMessageView extends LinearLayout
     79         implements TextView.OnEditorActionListener, DraftMessageDataListener, TextWatcher,
     80         ConversationInputSink {
     81 
     82     public interface IComposeMessageViewHost extends
     83             DraftMessageData.DraftMessageSubscriptionDataProvider {
     84         void sendMessage(MessageData message);
     85         void onComposeEditTextFocused();
     86         void onAttachmentsCleared();
     87         void onAttachmentsChanged(final boolean haveAttachments);
     88         void displayPhoto(Uri photoUri, Rect imageBounds, boolean isDraft);
     89         void promptForSelfPhoneNumber();
     90         boolean isReadyForAction();
     91         void warnOfMissingActionConditions(final boolean sending,
     92                 final Runnable commandToRunAfterActionConditionResolved);
     93         void warnOfExceedingMessageLimit(final boolean showAttachmentChooser,
     94                 boolean tooManyVideos);
     95         void notifyOfAttachmentLoadFailed();
     96         void showAttachmentChooser();
     97         boolean shouldShowSubjectEditor();
     98         boolean shouldHideAttachmentsWhenSimSelectorShown();
     99         Uri getSelfSendButtonIconUri();
    100         int overrideCounterColor();
    101         int getAttachmentsClearedFlags();
    102     }
    103 
    104     public static final int CODEPOINTS_REMAINING_BEFORE_COUNTER_SHOWN = 10;
    105 
    106     // There is no draft and there is no need for the SIM selector
    107     private static final int SEND_WIDGET_MODE_SELF_AVATAR = 1;
    108     // There is no draft but we need to show the SIM selector
    109     private static final int SEND_WIDGET_MODE_SIM_SELECTOR = 2;
    110     // There is a draft
    111     private static final int SEND_WIDGET_MODE_SEND_BUTTON = 3;
    112 
    113     private PlainTextEditText mComposeEditText;
    114     private PlainTextEditText mComposeSubjectText;
    115     private TextView mCharCounter;
    116     private TextView mMmsIndicator;
    117     private SimIconView mSelfSendIcon;
    118     private ImageButton mSendButton;
    119     private View mSubjectView;
    120     private ImageButton mDeleteSubjectButton;
    121     private AttachmentPreview mAttachmentPreview;
    122     private ImageButton mAttachMediaButton;
    123 
    124     private final Binding<DraftMessageData> mBinding;
    125     private IComposeMessageViewHost mHost;
    126     private final Context mOriginalContext;
    127     private int mSendWidgetMode = SEND_WIDGET_MODE_SELF_AVATAR;
    128 
    129     // Shared data model object binding from the conversation.
    130     private ImmutableBindingRef<ConversationData> mConversationDataModel;
    131 
    132     // Centrally manages all the mutual exclusive UI components accepting user input, i.e.
    133     // media picker, IME keyboard and SIM selector.
    134     private ConversationInputManager mInputManager;
    135 
    136     private final ConversationDataListener mDataListener = new SimpleConversationDataListener() {
    137         @Override
    138         public void onConversationMetadataUpdated(ConversationData data) {
    139             mConversationDataModel.ensureBound(data);
    140             updateVisualsOnDraftChanged();
    141         }
    142 
    143         @Override
    144         public void onConversationParticipantDataLoaded(ConversationData data) {
    145             mConversationDataModel.ensureBound(data);
    146             updateVisualsOnDraftChanged();
    147         }
    148 
    149         @Override
    150         public void onSubscriptionListDataLoaded(ConversationData data) {
    151             mConversationDataModel.ensureBound(data);
    152             updateOnSelfSubscriptionChange();
    153             updateVisualsOnDraftChanged();
    154         }
    155     };
    156 
    157     public ComposeMessageView(final Context context, final AttributeSet attrs) {
    158         super(new ContextThemeWrapper(context, R.style.ColorAccentBlueOverrideStyle), attrs);
    159         mOriginalContext = context;
    160         mBinding = BindingBase.createBinding(this);
    161     }
    162 
    163     /**
    164      * Host calls this to bind view to DraftMessageData object
    165      */
    166     public void bind(final DraftMessageData data, final IComposeMessageViewHost host) {
    167         mHost = host;
    168         mBinding.bind(data);
    169         data.addListener(this);
    170         data.setSubscriptionDataProvider(host);
    171 
    172         final int counterColor = mHost.overrideCounterColor();
    173         if (counterColor != -1) {
    174             mCharCounter.setTextColor(counterColor);
    175         }
    176     }
    177 
    178     /**
    179      * Host calls this to unbind view
    180      */
    181     public void unbind() {
    182         mBinding.unbind();
    183         mHost = null;
    184         mInputManager.onDetach();
    185     }
    186 
    187     @Override
    188     protected void onFinishInflate() {
    189         mComposeEditText = (PlainTextEditText) findViewById(
    190                 R.id.compose_message_text);
    191         mComposeEditText.setOnEditorActionListener(this);
    192         mComposeEditText.addTextChangedListener(this);
    193         mComposeEditText.setOnFocusChangeListener(new OnFocusChangeListener() {
    194             @Override
    195             public void onFocusChange(final View v, final boolean hasFocus) {
    196                 if (v == mComposeEditText && hasFocus) {
    197                     mHost.onComposeEditTextFocused();
    198                 }
    199             }
    200         });
    201         mComposeEditText.setOnClickListener(new View.OnClickListener() {
    202             @Override
    203             public void onClick(View arg0) {
    204                 if (mHost.shouldHideAttachmentsWhenSimSelectorShown()) {
    205                     hideSimSelector();
    206                 }
    207             }
    208         });
    209 
    210         // onFinishInflate() is called before self is loaded from db. We set the default text
    211         // limit here, and apply the real limit later in updateOnSelfSubscriptionChange().
    212         mComposeEditText.setFilters(new InputFilter[] {
    213                 new LengthFilter(MmsConfig.get(ParticipantData.DEFAULT_SELF_SUB_ID)
    214                         .getMaxTextLimit()) });
    215 
    216         mSelfSendIcon = (SimIconView) findViewById(R.id.self_send_icon);
    217         mSelfSendIcon.setOnClickListener(new OnClickListener() {
    218             @Override
    219             public void onClick(View v) {
    220                 boolean shown = mInputManager.toggleSimSelector(true /* animate */,
    221                         getSelfSubscriptionListEntry());
    222                 hideAttachmentsWhenShowingSims(shown);
    223             }
    224         });
    225         mSelfSendIcon.setOnLongClickListener(new OnLongClickListener() {
    226             @Override
    227             public boolean onLongClick(final View v) {
    228                 if (mHost.shouldShowSubjectEditor()) {
    229                     showSubjectEditor();
    230                 } else {
    231                     boolean shown = mInputManager.toggleSimSelector(true /* animate */,
    232                             getSelfSubscriptionListEntry());
    233                     hideAttachmentsWhenShowingSims(shown);
    234                 }
    235                 return true;
    236             }
    237         });
    238 
    239         mComposeSubjectText = (PlainTextEditText) findViewById(
    240                 R.id.compose_subject_text);
    241         // We need the listener to change the avatar to the send button when the user starts
    242         // typing a subject without a message.
    243         mComposeSubjectText.addTextChangedListener(this);
    244         // onFinishInflate() is called before self is loaded from db. We set the default text
    245         // limit here, and apply the real limit later in updateOnSelfSubscriptionChange().
    246         mComposeSubjectText.setFilters(new InputFilter[] {
    247                 new LengthFilter(MmsConfig.get(ParticipantData.DEFAULT_SELF_SUB_ID)
    248                         .getMaxSubjectLength())});
    249 
    250         mDeleteSubjectButton = (ImageButton) findViewById(R.id.delete_subject_button);
    251         mDeleteSubjectButton.setOnClickListener(new OnClickListener() {
    252             @Override
    253             public void onClick(final View clickView) {
    254                 hideSubjectEditor();
    255                 mComposeSubjectText.setText(null);
    256                 mBinding.getData().setMessageSubject(null);
    257             }
    258         });
    259 
    260         mSubjectView = findViewById(R.id.subject_view);
    261 
    262         mSendButton = (ImageButton) findViewById(R.id.send_message_button);
    263         mSendButton.setOnClickListener(new OnClickListener() {
    264             @Override
    265             public void onClick(final View clickView) {
    266                 sendMessageInternal(true /* checkMessageSize */);
    267             }
    268         });
    269         mSendButton.setOnLongClickListener(new OnLongClickListener() {
    270             @Override
    271             public boolean onLongClick(final View arg0) {
    272                 boolean shown = mInputManager.toggleSimSelector(true /* animate */,
    273                         getSelfSubscriptionListEntry());
    274                 hideAttachmentsWhenShowingSims(shown);
    275                 if (mHost.shouldShowSubjectEditor()) {
    276                     showSubjectEditor();
    277                 }
    278                 return true;
    279             }
    280         });
    281         mSendButton.setAccessibilityDelegate(new AccessibilityDelegate() {
    282             @Override
    283             public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
    284                 super.onPopulateAccessibilityEvent(host, event);
    285                 // When the send button is long clicked, we want TalkBack to announce the real
    286                 // action (select SIM or edit subject), as opposed to "long press send button."
    287                 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_LONG_CLICKED) {
    288                     event.getText().clear();
    289                     event.getText().add(getResources()
    290                             .getText(shouldShowSimSelector(mConversationDataModel.getData()) ?
    291                             R.string.send_button_long_click_description_with_sim_selector :
    292                                 R.string.send_button_long_click_description_no_sim_selector));
    293                     // Make this an announcement so TalkBack will read our custom message.
    294                     event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
    295                 }
    296             }
    297         });
    298 
    299         mAttachMediaButton =
    300                 (ImageButton) findViewById(R.id.attach_media_button);
    301         mAttachMediaButton.setOnClickListener(new View.OnClickListener() {
    302             @Override
    303             public void onClick(final View clickView) {
    304                 // Showing the media picker is treated as starting to compose the message.
    305                 mInputManager.showHideMediaPicker(true /* show */, true /* animate */);
    306             }
    307         });
    308 
    309         mAttachmentPreview = (AttachmentPreview) findViewById(R.id.attachment_draft_view);
    310         mAttachmentPreview.setComposeMessageView(this);
    311 
    312         mCharCounter = (TextView) findViewById(R.id.char_counter);
    313         mMmsIndicator = (TextView) findViewById(R.id.mms_indicator);
    314     }
    315 
    316     private void hideAttachmentsWhenShowingSims(final boolean simPickerVisible) {
    317         if (!mHost.shouldHideAttachmentsWhenSimSelectorShown()) {
    318             return;
    319         }
    320         final boolean haveAttachments = mBinding.getData().hasAttachments();
    321         if (simPickerVisible && haveAttachments) {
    322             mHost.onAttachmentsChanged(false);
    323             mAttachmentPreview.hideAttachmentPreview();
    324         } else {
    325             mHost.onAttachmentsChanged(haveAttachments);
    326             mAttachmentPreview.onAttachmentsChanged(mBinding.getData());
    327         }
    328     }
    329 
    330     public void setInputManager(final ConversationInputManager inputManager) {
    331         mInputManager = inputManager;
    332     }
    333 
    334     public void setConversationDataModel(final ImmutableBindingRef<ConversationData> refDataModel) {
    335         mConversationDataModel = refDataModel;
    336         mConversationDataModel.getData().addConversationDataListener(mDataListener);
    337     }
    338 
    339     ImmutableBindingRef<DraftMessageData> getDraftDataModel() {
    340         return BindingBase.createBindingReference(mBinding);
    341     }
    342 
    343     // returns true if it actually shows the subject editor and false if already showing
    344     private boolean showSubjectEditor() {
    345         // show the subject editor
    346         if (mSubjectView.getVisibility() == View.GONE) {
    347             mSubjectView.setVisibility(View.VISIBLE);
    348             mSubjectView.requestFocus();
    349             return true;
    350         }
    351         return false;
    352     }
    353 
    354     private void hideSubjectEditor() {
    355         mSubjectView.setVisibility(View.GONE);
    356         mComposeEditText.requestFocus();
    357     }
    358 
    359     /**
    360      * {@inheritDoc} from TextView.OnEditorActionListener
    361      */
    362     @Override // TextView.OnEditorActionListener.onEditorAction
    363     public boolean onEditorAction(final TextView view, final int actionId, final KeyEvent event) {
    364         if (actionId == EditorInfo.IME_ACTION_SEND) {
    365             sendMessageInternal(true /* checkMessageSize */);
    366             return true;
    367         }
    368         return false;
    369     }
    370 
    371     private void sendMessageInternal(final boolean checkMessageSize) {
    372         LogUtil.i(LogUtil.BUGLE_TAG, "UI initiated message sending in conversation " +
    373                 mBinding.getData().getConversationId());
    374         if (mBinding.getData().isCheckingDraft()) {
    375             // Don't send message if we are currently checking draft for sending.
    376             LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: still checking draft");
    377             return;
    378         }
    379         // Check the host for pre-conditions about any action.
    380         if (mHost.isReadyForAction()) {
    381             mInputManager.showHideSimSelector(false /* show */, true /* animate */);
    382             final String messageToSend = mComposeEditText.getText().toString();
    383             mBinding.getData().setMessageText(messageToSend);
    384             final String subject = mComposeSubjectText.getText().toString();
    385             mBinding.getData().setMessageSubject(subject);
    386             // Asynchronously check the draft against various requirements before sending.
    387             mBinding.getData().checkDraftForAction(checkMessageSize,
    388                     mHost.getConversationSelfSubId(), new CheckDraftTaskCallback() {
    389                 @Override
    390                 public void onDraftChecked(DraftMessageData data, int result) {
    391                     mBinding.ensureBound(data);
    392                     switch (result) {
    393                         case CheckDraftForSendTask.RESULT_PASSED:
    394                             // Continue sending after check succeeded.
    395                             final MessageData message = mBinding.getData()
    396                                     .prepareMessageForSending(mBinding);
    397                             if (message != null && message.hasContent()) {
    398                                 playSentSound();
    399                                 mHost.sendMessage(message);
    400                                 hideSubjectEditor();
    401                                 if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
    402                                     AccessibilityUtil.announceForAccessibilityCompat(
    403                                             ComposeMessageView.this, null,
    404                                             R.string.sending_message);
    405                                 }
    406                             }
    407                             break;
    408 
    409                         case CheckDraftForSendTask.RESULT_HAS_PENDING_ATTACHMENTS:
    410                             // Cannot send while there's still attachment(s) being loaded.
    411                             UiUtils.showToastAtBottom(
    412                                     R.string.cant_send_message_while_loading_attachments);
    413                             break;
    414 
    415                         case CheckDraftForSendTask.RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS:
    416                             mHost.promptForSelfPhoneNumber();
    417                             break;
    418 
    419                         case CheckDraftForSendTask.RESULT_MESSAGE_OVER_LIMIT:
    420                             Assert.isTrue(checkMessageSize);
    421                             mHost.warnOfExceedingMessageLimit(
    422                                     true /*sending*/, false /* tooManyVideos */);
    423                             break;
    424 
    425                         case CheckDraftForSendTask.RESULT_VIDEO_ATTACHMENT_LIMIT_EXCEEDED:
    426                             Assert.isTrue(checkMessageSize);
    427                             mHost.warnOfExceedingMessageLimit(
    428                                     true /*sending*/, true /* tooManyVideos */);
    429                             break;
    430 
    431                         case CheckDraftForSendTask.RESULT_SIM_NOT_READY:
    432                             // Cannot send if there is no active subscription
    433                             UiUtils.showToastAtBottom(
    434                                     R.string.cant_send_message_without_active_subscription);
    435                             break;
    436 
    437                         default:
    438                             break;
    439                     }
    440                 }
    441             }, mBinding);
    442         } else {
    443             mHost.warnOfMissingActionConditions(true /*sending*/,
    444                     new Runnable() {
    445                         @Override
    446                         public void run() {
    447                             sendMessageInternal(checkMessageSize);
    448                         }
    449 
    450             });
    451         }
    452     }
    453 
    454     public static void playSentSound() {
    455         // Check if this setting is enabled before playing
    456         final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
    457         final Context context = Factory.get().getApplicationContext();
    458         final String prefKey = context.getString(R.string.send_sound_pref_key);
    459         final boolean defaultValue = context.getResources().getBoolean(
    460                 R.bool.send_sound_pref_default);
    461         if (!prefs.getBoolean(prefKey, defaultValue)) {
    462             return;
    463         }
    464         MediaUtil.get().playSound(context, R.raw.message_sent, null /* completionListener */);
    465     }
    466 
    467     /**
    468      * {@inheritDoc} from DraftMessageDataListener
    469      */
    470     @Override // From DraftMessageDataListener
    471     public void onDraftChanged(final DraftMessageData data, final int changeFlags) {
    472         // As this is called asynchronously when message read check bound before updating text
    473         mBinding.ensureBound(data);
    474 
    475         // We have to cache the values of the DraftMessageData because when we set
    476         // mComposeEditText, its onTextChanged calls updateVisualsOnDraftChanged,
    477         // which immediately reloads the text from the subject and message fields and replaces
    478         // what's in the DraftMessageData.
    479 
    480         final String subject = data.getMessageSubject();
    481         final String message = data.getMessageText();
    482 
    483         if ((changeFlags & DraftMessageData.MESSAGE_SUBJECT_CHANGED) ==
    484                 DraftMessageData.MESSAGE_SUBJECT_CHANGED) {
    485             mComposeSubjectText.setText(subject);
    486 
    487             // Set the cursor selection to the end since setText resets it to the start
    488             mComposeSubjectText.setSelection(mComposeSubjectText.getText().length());
    489         }
    490 
    491         if ((changeFlags & DraftMessageData.MESSAGE_TEXT_CHANGED) ==
    492                 DraftMessageData.MESSAGE_TEXT_CHANGED) {
    493             mComposeEditText.setText(message);
    494 
    495             // Set the cursor selection to the end since setText resets it to the start
    496             mComposeEditText.setSelection(mComposeEditText.getText().length());
    497         }
    498 
    499         if ((changeFlags & DraftMessageData.ATTACHMENTS_CHANGED) ==
    500                 DraftMessageData.ATTACHMENTS_CHANGED) {
    501             final boolean haveAttachments = mAttachmentPreview.onAttachmentsChanged(data);
    502             mHost.onAttachmentsChanged(haveAttachments);
    503         }
    504 
    505         if ((changeFlags & DraftMessageData.SELF_CHANGED) == DraftMessageData.SELF_CHANGED) {
    506             updateOnSelfSubscriptionChange();
    507         }
    508         updateVisualsOnDraftChanged();
    509     }
    510 
    511     @Override   // From DraftMessageDataListener
    512     public void onDraftAttachmentLimitReached(final DraftMessageData data) {
    513         mBinding.ensureBound(data);
    514         mHost.warnOfExceedingMessageLimit(false /* sending */, false /* tooManyVideos */);
    515     }
    516 
    517     private void updateOnSelfSubscriptionChange() {
    518         // Refresh the length filters according to the selected self's MmsConfig.
    519         mComposeEditText.setFilters(new InputFilter[] {
    520                 new LengthFilter(MmsConfig.get(mBinding.getData().getSelfSubId())
    521                         .getMaxTextLimit()) });
    522         mComposeSubjectText.setFilters(new InputFilter[] {
    523                 new LengthFilter(MmsConfig.get(mBinding.getData().getSelfSubId())
    524                         .getMaxSubjectLength())});
    525     }
    526 
    527     @Override
    528     public void onMediaItemsSelected(final Collection<MessagePartData> items) {
    529         mBinding.getData().addAttachments(items);
    530         announceMediaItemState(true /*isSelected*/);
    531     }
    532 
    533     @Override
    534     public void onMediaItemsUnselected(final MessagePartData item) {
    535         mBinding.getData().removeAttachment(item);
    536         announceMediaItemState(false /*isSelected*/);
    537     }
    538 
    539     @Override
    540     public void onPendingAttachmentAdded(final PendingAttachmentData pendingItem) {
    541         mBinding.getData().addPendingAttachment(pendingItem, mBinding);
    542         resumeComposeMessage();
    543     }
    544 
    545     private void announceMediaItemState(final boolean isSelected) {
    546         final Resources res = getContext().getResources();
    547         final String announcement = isSelected ? res.getString(
    548                 R.string.mediapicker_gallery_item_selected_content_description) :
    549                     res.getString(R.string.mediapicker_gallery_item_unselected_content_description);
    550         AccessibilityUtil.announceForAccessibilityCompat(
    551                 this, null, announcement);
    552     }
    553 
    554     private void announceAttachmentState() {
    555         if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
    556             int attachmentCount = mBinding.getData().getReadOnlyAttachments().size()
    557                     + mBinding.getData().getReadOnlyPendingAttachments().size();
    558             final String announcement = getContext().getResources().getQuantityString(
    559                     R.plurals.attachment_changed_accessibility_announcement,
    560                     attachmentCount, attachmentCount);
    561             AccessibilityUtil.announceForAccessibilityCompat(
    562                     this, null, announcement);
    563         }
    564     }
    565 
    566     @Override
    567     public void resumeComposeMessage() {
    568         mComposeEditText.requestFocus();
    569         mInputManager.showHideImeKeyboard(true, true);
    570         announceAttachmentState();
    571     }
    572 
    573     public void clearAttachments() {
    574         mBinding.getData().clearAttachments(mHost.getAttachmentsClearedFlags());
    575         mHost.onAttachmentsCleared();
    576     }
    577 
    578     public void requestDraftMessage(boolean clearLocalDraft) {
    579         mBinding.getData().loadFromStorage(mBinding, null, clearLocalDraft);
    580     }
    581 
    582     public void setDraftMessage(final MessageData message) {
    583         mBinding.getData().loadFromStorage(mBinding, message, false);
    584     }
    585 
    586     public void writeDraftMessage() {
    587         final String messageText = mComposeEditText.getText().toString();
    588         mBinding.getData().setMessageText(messageText);
    589 
    590         final String subject = mComposeSubjectText.getText().toString();
    591         mBinding.getData().setMessageSubject(subject);
    592 
    593         mBinding.getData().saveToStorage(mBinding);
    594     }
    595 
    596     private void updateConversationSelfId(final String selfId, final boolean notify) {
    597         mBinding.getData().setSelfId(selfId, notify);
    598     }
    599 
    600     private Uri getSelfSendButtonIconUri() {
    601         final Uri overridenSelfUri = mHost.getSelfSendButtonIconUri();
    602         if (overridenSelfUri != null) {
    603             return overridenSelfUri;
    604         }
    605         final SubscriptionListEntry subscriptionListEntry = getSelfSubscriptionListEntry();
    606 
    607         if (subscriptionListEntry != null) {
    608             return subscriptionListEntry.selectedIconUri;
    609         }
    610 
    611         // Fall back to default self-avatar in the base case.
    612         final ParticipantData self = mConversationDataModel.getData().getDefaultSelfParticipant();
    613         return self == null ? null : AvatarUriUtil.createAvatarUri(self);
    614     }
    615 
    616     private SubscriptionListEntry getSelfSubscriptionListEntry() {
    617         return mConversationDataModel.getData().getSubscriptionEntryForSelfParticipant(
    618                 mBinding.getData().getSelfId(), false /* excludeDefault */);
    619     }
    620 
    621     private boolean isDataLoadedForMessageSend() {
    622         // Check data loading prerequisites for sending a message.
    623         return mConversationDataModel != null && mConversationDataModel.isBound() &&
    624                 mConversationDataModel.getData().getParticipantsLoaded();
    625     }
    626 
    627     private void updateVisualsOnDraftChanged() {
    628         final String messageText = mComposeEditText.getText().toString();
    629         final DraftMessageData draftMessageData = mBinding.getData();
    630         draftMessageData.setMessageText(messageText);
    631 
    632         final String subject = mComposeSubjectText.getText().toString();
    633         draftMessageData.setMessageSubject(subject);
    634         if (!TextUtils.isEmpty(subject)) {
    635              mSubjectView.setVisibility(View.VISIBLE);
    636         }
    637 
    638         final boolean hasMessageText = (TextUtils.getTrimmedLength(messageText) > 0);
    639         final boolean hasSubject = (TextUtils.getTrimmedLength(subject) > 0);
    640         final boolean hasWorkingDraft = hasMessageText || hasSubject ||
    641                 mBinding.getData().hasAttachments();
    642 
    643         // Update the SMS text counter.
    644         final int messageCount = draftMessageData.getNumMessagesToBeSent();
    645         final int codePointsRemaining = draftMessageData.getCodePointsRemainingInCurrentMessage();
    646         // Show the counter only if:
    647         // - We are not in MMS mode
    648         // - We are going to send more than one message OR we are getting close
    649         boolean showCounter = false;
    650         if (!draftMessageData.getIsMms() && (messageCount > 1 ||
    651                  codePointsRemaining <= CODEPOINTS_REMAINING_BEFORE_COUNTER_SHOWN)) {
    652             showCounter = true;
    653         }
    654 
    655         if (showCounter) {
    656             // Update the remaining characters and number of messages required.
    657             final String counterText = messageCount > 1 ? codePointsRemaining + " / " +
    658                     messageCount : String.valueOf(codePointsRemaining);
    659             mCharCounter.setText(counterText);
    660             mCharCounter.setVisibility(View.VISIBLE);
    661         } else {
    662             mCharCounter.setVisibility(View.INVISIBLE);
    663         }
    664 
    665         // Update the send message button. Self icon uri might be null if self participant data
    666         // and/or conversation metadata hasn't been loaded by the host.
    667         final Uri selfSendButtonUri = getSelfSendButtonIconUri();
    668         int sendWidgetMode = SEND_WIDGET_MODE_SELF_AVATAR;
    669         if (selfSendButtonUri != null) {
    670             if (hasWorkingDraft && isDataLoadedForMessageSend()) {
    671                 UiUtils.revealOrHideViewWithAnimation(mSendButton, VISIBLE, null);
    672                 if (isOverriddenAvatarAGroup()) {
    673                     // If the host has overriden the avatar to show a group avatar where the
    674                     // send button sits, we have to hide the group avatar because it can be larger
    675                     // than the send button and pieces of the avatar will stick out from behind
    676                     // the send button.
    677                     UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, GONE, null);
    678                 }
    679                 mMmsIndicator.setVisibility(draftMessageData.getIsMms() ? VISIBLE : INVISIBLE);
    680                 sendWidgetMode = SEND_WIDGET_MODE_SEND_BUTTON;
    681             } else {
    682                 mSelfSendIcon.setImageResourceUri(selfSendButtonUri);
    683                 if (isOverriddenAvatarAGroup()) {
    684                     UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, VISIBLE, null);
    685                 }
    686                 UiUtils.revealOrHideViewWithAnimation(mSendButton, GONE, null);
    687                 mMmsIndicator.setVisibility(INVISIBLE);
    688                 if (shouldShowSimSelector(mConversationDataModel.getData())) {
    689                     sendWidgetMode = SEND_WIDGET_MODE_SIM_SELECTOR;
    690                 }
    691             }
    692         } else {
    693             mSelfSendIcon.setImageResourceUri(null);
    694         }
    695 
    696         if (mSendWidgetMode != sendWidgetMode || sendWidgetMode == SEND_WIDGET_MODE_SIM_SELECTOR) {
    697             setSendButtonAccessibility(sendWidgetMode);
    698             mSendWidgetMode = sendWidgetMode;
    699         }
    700 
    701         // Update the text hint on the message box depending on the attachment type.
    702         final List<MessagePartData> attachments = draftMessageData.getReadOnlyAttachments();
    703         final int attachmentCount = attachments.size();
    704         if (attachmentCount == 0) {
    705             final SubscriptionListEntry subscriptionListEntry =
    706                     mConversationDataModel.getData().getSubscriptionEntryForSelfParticipant(
    707                             mBinding.getData().getSelfId(), false /* excludeDefault */);
    708             if (subscriptionListEntry == null) {
    709                 mComposeEditText.setHint(R.string.compose_message_view_hint_text);
    710             } else {
    711                 mComposeEditText.setHint(Html.fromHtml(getResources().getString(
    712                         R.string.compose_message_view_hint_text_multi_sim,
    713                         subscriptionListEntry.displayName)));
    714             }
    715         } else {
    716             int type = -1;
    717             for (final MessagePartData attachment : attachments) {
    718                 int newType;
    719                 if (attachment.isImage()) {
    720                     newType = ContentType.TYPE_IMAGE;
    721                 } else if (attachment.isAudio()) {
    722                     newType = ContentType.TYPE_AUDIO;
    723                 } else if (attachment.isVideo()) {
    724                     newType = ContentType.TYPE_VIDEO;
    725                 } else if (attachment.isVCard()) {
    726                     newType = ContentType.TYPE_VCARD;
    727                 } else {
    728                     newType = ContentType.TYPE_OTHER;
    729                 }
    730 
    731                 if (type == -1) {
    732                     type = newType;
    733                 } else if (type != newType || type == ContentType.TYPE_OTHER) {
    734                     type = ContentType.TYPE_OTHER;
    735                     break;
    736                 }
    737             }
    738 
    739             switch (type) {
    740                 case ContentType.TYPE_IMAGE:
    741                     mComposeEditText.setHint(getResources().getQuantityString(
    742                             R.plurals.compose_message_view_hint_text_photo, attachmentCount));
    743                     break;
    744 
    745                 case ContentType.TYPE_AUDIO:
    746                     mComposeEditText.setHint(getResources().getQuantityString(
    747                             R.plurals.compose_message_view_hint_text_audio, attachmentCount));
    748                     break;
    749 
    750                 case ContentType.TYPE_VIDEO:
    751                     mComposeEditText.setHint(getResources().getQuantityString(
    752                             R.plurals.compose_message_view_hint_text_video, attachmentCount));
    753                     break;
    754 
    755                 case ContentType.TYPE_VCARD:
    756                     mComposeEditText.setHint(getResources().getQuantityString(
    757                             R.plurals.compose_message_view_hint_text_vcard, attachmentCount));
    758                     break;
    759 
    760                 case ContentType.TYPE_OTHER:
    761                     mComposeEditText.setHint(getResources().getQuantityString(
    762                             R.plurals.compose_message_view_hint_text_attachments, attachmentCount));
    763                     break;
    764 
    765                 default:
    766                     Assert.fail("Unsupported attachment type!");
    767                     break;
    768             }
    769         }
    770     }
    771 
    772     private void setSendButtonAccessibility(final int sendWidgetMode) {
    773         switch (sendWidgetMode) {
    774             case SEND_WIDGET_MODE_SELF_AVATAR:
    775                 // No send button and no SIM selector; the self send button is no longer
    776                 // important for accessibility.
    777                 mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
    778                 mSelfSendIcon.setContentDescription(null);
    779                 mSendButton.setVisibility(View.GONE);
    780                 setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SELF_AVATAR);
    781                 break;
    782 
    783             case SEND_WIDGET_MODE_SIM_SELECTOR:
    784                 mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    785                 mSelfSendIcon.setContentDescription(getSimContentDescription());
    786                 setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SIM_SELECTOR);
    787                 break;
    788 
    789             case SEND_WIDGET_MODE_SEND_BUTTON:
    790                 mMmsIndicator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
    791                 mMmsIndicator.setContentDescription(null);
    792                 setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SEND_BUTTON);
    793                 break;
    794         }
    795     }
    796 
    797     private String getSimContentDescription() {
    798         final SubscriptionListEntry sub = getSelfSubscriptionListEntry();
    799         if (sub != null) {
    800             return getResources().getString(
    801                     R.string.sim_selector_button_content_description_with_selection,
    802                     sub.displayName);
    803         } else {
    804             return getResources().getString(
    805                     R.string.sim_selector_button_content_description);
    806         }
    807     }
    808 
    809     // Set accessibility traversal order of the components in the send widget.
    810     private void setSendWidgetAccessibilityTraversalOrder(final int mode) {
    811         if (OsUtil.isAtLeastL_MR1()) {
    812             mAttachMediaButton.setAccessibilityTraversalBefore(R.id.compose_message_text);
    813             switch (mode) {
    814                 case SEND_WIDGET_MODE_SIM_SELECTOR:
    815                     mComposeEditText.setAccessibilityTraversalBefore(R.id.self_send_icon);
    816                     break;
    817                 case SEND_WIDGET_MODE_SEND_BUTTON:
    818                     mComposeEditText.setAccessibilityTraversalBefore(R.id.send_message_button);
    819                     break;
    820                 default:
    821                     break;
    822             }
    823         }
    824     }
    825 
    826     @Override
    827     public void afterTextChanged(final Editable editable) {
    828     }
    829 
    830     @Override
    831     public void beforeTextChanged(final CharSequence s, final int start, final int count,
    832             final int after) {
    833         if (mHost.shouldHideAttachmentsWhenSimSelectorShown()) {
    834             hideSimSelector();
    835         }
    836     }
    837 
    838     private void hideSimSelector() {
    839         if (mInputManager.showHideSimSelector(false /* show */, true /* animate */)) {
    840             // Now that the sim selector has been hidden, reshow the attachments if they
    841             // have been hidden.
    842             hideAttachmentsWhenShowingSims(false /*simPickerVisible*/);
    843         }
    844     }
    845 
    846     @Override
    847     public void onTextChanged(final CharSequence s, final int start, final int before,
    848             final int count) {
    849         final BugleActionBarActivity activity = (mOriginalContext instanceof BugleActionBarActivity)
    850                 ? (BugleActionBarActivity) mOriginalContext : null;
    851         if (activity != null && activity.getIsDestroyed()) {
    852             LogUtil.v(LogUtil.BUGLE_TAG, "got onTextChanged after onDestroy");
    853 
    854             // if we get onTextChanged after the activity is destroyed then, ah, wtf
    855             // b/18176615
    856             // This appears to have occurred as the result of orientation change.
    857             return;
    858         }
    859 
    860         mBinding.ensureBound();
    861         updateVisualsOnDraftChanged();
    862     }
    863 
    864     @Override
    865     public PlainTextEditText getComposeEditText() {
    866         return mComposeEditText;
    867     }
    868 
    869     public void displayPhoto(final Uri photoUri, final Rect imageBounds) {
    870         mHost.displayPhoto(photoUri, imageBounds, true /* isDraft */);
    871     }
    872 
    873     public void updateConversationSelfIdOnExternalChange(final String selfId) {
    874         updateConversationSelfId(selfId, true /* notify */);
    875     }
    876 
    877     /**
    878      * The selfId of the conversation. As soon as the DraftMessageData successfully loads (i.e.
    879      * getSelfId() is non-null), the selfId in DraftMessageData is treated as the sole source
    880      * of truth for conversation self id since it reflects any pending self id change the user
    881      * makes in the UI.
    882      */
    883     public String getConversationSelfId() {
    884         return mBinding.getData().getSelfId();
    885     }
    886 
    887     public void selectSim(SubscriptionListEntry subscriptionData) {
    888         final String oldSelfId = getConversationSelfId();
    889         final String newSelfId = subscriptionData.selfParticipantId;
    890         Assert.notNull(newSelfId);
    891         // Don't attempt to change self if self hasn't been loaded, or if self hasn't changed.
    892         if (oldSelfId == null || TextUtils.equals(oldSelfId, newSelfId)) {
    893             return;
    894         }
    895         updateConversationSelfId(newSelfId, true /* notify */);
    896     }
    897 
    898     public void hideAllComposeInputs(final boolean animate) {
    899         mInputManager.hideAllInputs(animate);
    900     }
    901 
    902     public void saveInputState(final Bundle outState) {
    903         mInputManager.onSaveInputState(outState);
    904     }
    905 
    906     public void resetMediaPickerState() {
    907         mInputManager.resetMediaPickerState();
    908     }
    909 
    910     public boolean onBackPressed() {
    911         return mInputManager.onBackPressed();
    912     }
    913 
    914     public boolean onNavigationUpPressed() {
    915         return mInputManager.onNavigationUpPressed();
    916     }
    917 
    918     public boolean updateActionBar(final ActionBar actionBar) {
    919         return mInputManager != null ? mInputManager.updateActionBar(actionBar) : false;
    920     }
    921 
    922     public static boolean shouldShowSimSelector(final ConversationData convData) {
    923         return OsUtil.isAtLeastL_MR1() &&
    924                 convData.getSelfParticipantsCountExcludingDefault(true /* activeOnly */) > 1;
    925     }
    926 
    927     public void sendMessageIgnoreMessageSizeLimit() {
    928         sendMessageInternal(false /* checkMessageSize */);
    929     }
    930 
    931     public void onAttachmentPreviewLongClicked() {
    932         mHost.showAttachmentChooser();
    933     }
    934 
    935     @Override
    936     public void onDraftAttachmentLoadFailed() {
    937         mHost.notifyOfAttachmentLoadFailed();
    938     }
    939 
    940     private boolean isOverriddenAvatarAGroup() {
    941         final Uri overridenSelfUri = mHost.getSelfSendButtonIconUri();
    942         if (overridenSelfUri == null) {
    943             return false;
    944         }
    945         return AvatarUriUtil.TYPE_GROUP_URI.equals(AvatarUriUtil.getAvatarType(overridenSelfUri));
    946     }
    947 
    948     @Override
    949     public void setAccessibility(boolean enabled) {
    950         if (enabled) {
    951             mAttachMediaButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    952             mComposeEditText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    953             mSendButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    954             setSendButtonAccessibility(mSendWidgetMode);
    955         } else {
    956             mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
    957             mComposeEditText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
    958             mSendButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
    959             mAttachMediaButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
    960         }
    961     }
    962 }
    963