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 {
sendMessage(MessageData message)84         void sendMessage(MessageData message);
onComposeEditTextFocused()85         void onComposeEditTextFocused();
onAttachmentsCleared()86         void onAttachmentsCleared();
onAttachmentsChanged(final boolean haveAttachments)87         void onAttachmentsChanged(final boolean haveAttachments);
displayPhoto(Uri photoUri, Rect imageBounds, boolean isDraft)88         void displayPhoto(Uri photoUri, Rect imageBounds, boolean isDraft);
promptForSelfPhoneNumber()89         void promptForSelfPhoneNumber();
isReadyForAction()90         boolean isReadyForAction();
warnOfMissingActionConditions(final boolean sending, final Runnable commandToRunAfterActionConditionResolved)91         void warnOfMissingActionConditions(final boolean sending,
92                 final Runnable commandToRunAfterActionConditionResolved);
warnOfExceedingMessageLimit(final boolean showAttachmentChooser, boolean tooManyVideos)93         void warnOfExceedingMessageLimit(final boolean showAttachmentChooser,
94                 boolean tooManyVideos);
notifyOfAttachmentLoadFailed()95         void notifyOfAttachmentLoadFailed();
showAttachmentChooser()96         void showAttachmentChooser();
shouldShowSubjectEditor()97         boolean shouldShowSubjectEditor();
shouldHideAttachmentsWhenSimSelectorShown()98         boolean shouldHideAttachmentsWhenSimSelectorShown();
getSelfSendButtonIconUri()99         Uri getSelfSendButtonIconUri();
overrideCounterColor()100         int overrideCounterColor();
getAttachmentsClearedFlags()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 
ComposeMessageView(final Context context, final AttributeSet attrs)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      */
bind(final DraftMessageData data, final IComposeMessageViewHost host)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      */
unbind()181     public void unbind() {
182         mBinding.unbind();
183         mHost = null;
184         mInputManager.onDetach();
185     }
186 
187     @Override
onFinishInflate()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 
hideAttachmentsWhenShowingSims(final boolean simPickerVisible)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 
setInputManager(final ConversationInputManager inputManager)330     public void setInputManager(final ConversationInputManager inputManager) {
331         mInputManager = inputManager;
332     }
333 
setConversationDataModel(final ImmutableBindingRef<ConversationData> refDataModel)334     public void setConversationDataModel(final ImmutableBindingRef<ConversationData> refDataModel) {
335         mConversationDataModel = refDataModel;
336         mConversationDataModel.getData().addConversationDataListener(mDataListener);
337     }
338 
getDraftDataModel()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
showSubjectEditor()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 
hideSubjectEditor()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
onEditorAction(final TextView view, final int actionId, final KeyEvent event)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 
sendMessageInternal(final boolean checkMessageSize)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 
playSentSound()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
onDraftChanged(final DraftMessageData data, final int changeFlags)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
onDraftAttachmentLimitReached(final DraftMessageData data)512     public void onDraftAttachmentLimitReached(final DraftMessageData data) {
513         mBinding.ensureBound(data);
514         mHost.warnOfExceedingMessageLimit(false /* sending */, false /* tooManyVideos */);
515     }
516 
updateOnSelfSubscriptionChange()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
onMediaItemsSelected(final Collection<MessagePartData> items)528     public void onMediaItemsSelected(final Collection<MessagePartData> items) {
529         mBinding.getData().addAttachments(items);
530         announceMediaItemState(true /*isSelected*/);
531     }
532 
533     @Override
onMediaItemsUnselected(final MessagePartData item)534     public void onMediaItemsUnselected(final MessagePartData item) {
535         mBinding.getData().removeAttachment(item);
536         announceMediaItemState(false /*isSelected*/);
537     }
538 
539     @Override
onPendingAttachmentAdded(final PendingAttachmentData pendingItem)540     public void onPendingAttachmentAdded(final PendingAttachmentData pendingItem) {
541         mBinding.getData().addPendingAttachment(pendingItem, mBinding);
542         resumeComposeMessage();
543     }
544 
announceMediaItemState(final boolean isSelected)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 
announceAttachmentState()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
resumeComposeMessage()567     public void resumeComposeMessage() {
568         mComposeEditText.requestFocus();
569         mInputManager.showHideImeKeyboard(true, true);
570         announceAttachmentState();
571     }
572 
clearAttachments()573     public void clearAttachments() {
574         mBinding.getData().clearAttachments(mHost.getAttachmentsClearedFlags());
575         mHost.onAttachmentsCleared();
576     }
577 
requestDraftMessage(boolean clearLocalDraft)578     public void requestDraftMessage(boolean clearLocalDraft) {
579         mBinding.getData().loadFromStorage(mBinding, null, clearLocalDraft);
580     }
581 
setDraftMessage(final MessageData message)582     public void setDraftMessage(final MessageData message) {
583         mBinding.getData().loadFromStorage(mBinding, message, false);
584     }
585 
writeDraftMessage()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 
updateConversationSelfId(final String selfId, final boolean notify)596     private void updateConversationSelfId(final String selfId, final boolean notify) {
597         mBinding.getData().setSelfId(selfId, notify);
598     }
599 
getSelfSendButtonIconUri()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 
getSelfSubscriptionListEntry()616     private SubscriptionListEntry getSelfSubscriptionListEntry() {
617         return mConversationDataModel.getData().getSubscriptionEntryForSelfParticipant(
618                 mBinding.getData().getSelfId(), false /* excludeDefault */);
619     }
620 
isDataLoadedForMessageSend()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 
updateVisualsOnDraftChanged()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 
setSendButtonAccessibility(final int sendWidgetMode)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 
getSimContentDescription()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.
setSendWidgetAccessibilityTraversalOrder(final int mode)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
afterTextChanged(final Editable editable)827     public void afterTextChanged(final Editable editable) {
828     }
829 
830     @Override
beforeTextChanged(final CharSequence s, final int start, final int count, final int after)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 
hideSimSelector()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
onTextChanged(final CharSequence s, final int start, final int before, final int count)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
getComposeEditText()865     public PlainTextEditText getComposeEditText() {
866         return mComposeEditText;
867     }
868 
displayPhoto(final Uri photoUri, final Rect imageBounds)869     public void displayPhoto(final Uri photoUri, final Rect imageBounds) {
870         mHost.displayPhoto(photoUri, imageBounds, true /* isDraft */);
871     }
872 
updateConversationSelfIdOnExternalChange(final String selfId)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      */
getConversationSelfId()883     public String getConversationSelfId() {
884         return mBinding.getData().getSelfId();
885     }
886 
selectSim(SubscriptionListEntry subscriptionData)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 
hideAllComposeInputs(final boolean animate)898     public void hideAllComposeInputs(final boolean animate) {
899         mInputManager.hideAllInputs(animate);
900     }
901 
saveInputState(final Bundle outState)902     public void saveInputState(final Bundle outState) {
903         mInputManager.onSaveInputState(outState);
904     }
905 
resetMediaPickerState()906     public void resetMediaPickerState() {
907         mInputManager.resetMediaPickerState();
908     }
909 
onBackPressed()910     public boolean onBackPressed() {
911         return mInputManager.onBackPressed();
912     }
913 
onNavigationUpPressed()914     public boolean onNavigationUpPressed() {
915         return mInputManager.onNavigationUpPressed();
916     }
917 
updateActionBar(final ActionBar actionBar)918     public boolean updateActionBar(final ActionBar actionBar) {
919         return mInputManager != null ? mInputManager.updateActionBar(actionBar) : false;
920     }
921 
shouldShowSimSelector(final ConversationData convData)922     public static boolean shouldShowSimSelector(final ConversationData convData) {
923         return OsUtil.isAtLeastL_MR1() &&
924                 convData.getSelfParticipantsCountExcludingDefault(true /* activeOnly */) > 1;
925     }
926 
sendMessageIgnoreMessageSizeLimit()927     public void sendMessageIgnoreMessageSizeLimit() {
928         sendMessageInternal(false /* checkMessageSize */);
929     }
930 
onAttachmentPreviewLongClicked()931     public void onAttachmentPreviewLongClicked() {
932         mHost.showAttachmentChooser();
933     }
934 
935     @Override
onDraftAttachmentLoadFailed()936     public void onDraftAttachmentLoadFailed() {
937         mHost.notifyOfAttachmentLoadFailed();
938     }
939 
isOverriddenAvatarAGroup()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
setAccessibility(boolean enabled)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