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