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