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