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.app.FragmentManager;
19import android.content.Context;
20import android.os.Bundle;
21import android.support.v7.app.ActionBar;
22import android.widget.EditText;
23
24import com.android.messaging.R;
25import com.android.messaging.datamodel.binding.BindingBase;
26import com.android.messaging.datamodel.binding.ImmutableBindingRef;
27import com.android.messaging.datamodel.data.ConversationData;
28import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener;
29import com.android.messaging.datamodel.data.ConversationData.SimpleConversationDataListener;
30import com.android.messaging.datamodel.data.DraftMessageData;
31import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider;
32import com.android.messaging.datamodel.data.MessagePartData;
33import com.android.messaging.datamodel.data.PendingAttachmentData;
34import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
35import com.android.messaging.ui.ConversationDrawables;
36import com.android.messaging.ui.mediapicker.MediaPicker;
37import com.android.messaging.ui.mediapicker.MediaPicker.MediaPickerListener;
38import com.android.messaging.util.Assert;
39import com.android.messaging.util.ImeUtil;
40import com.android.messaging.util.ImeUtil.ImeStateHost;
41import com.google.common.annotations.VisibleForTesting;
42
43import java.util.Collection;
44
45/**
46 * Manages showing/hiding/persisting different mutually exclusive UI components nested in
47 * ConversationFragment that take user inputs, i.e. media picker, SIM selector and
48 * IME keyboard (the IME keyboard is not owned by Bugle, but we try to model it the same way
49 * as the other components).
50 */
51public class ConversationInputManager implements ConversationInput.ConversationInputBase {
52    /**
53     * The host component where all input components are contained. This is typically the
54     * conversation fragment but may be mocked in test code.
55     */
56    public interface ConversationInputHost extends DraftMessageSubscriptionDataProvider {
57        void invalidateActionBar();
58        void setOptionsMenuVisibility(boolean visible);
59        void dismissActionMode();
60        void selectSim(SubscriptionListEntry subscriptionData);
61        void onStartComposeMessage();
62        SimSelectorView getSimSelectorView();
63        MediaPicker createMediaPicker();
64        void showHideSimSelector(boolean show);
65        int getSimSelectorItemLayoutId();
66    }
67
68    /**
69     * The "sink" component where all inputs components will direct the user inputs to. This is
70     * typically the ComposeMessageView but may be mocked in test code.
71     */
72    public interface ConversationInputSink {
73        void onMediaItemsSelected(Collection<MessagePartData> items);
74        void onMediaItemsUnselected(MessagePartData item);
75        void onPendingAttachmentAdded(PendingAttachmentData pendingItem);
76        void resumeComposeMessage();
77        EditText getComposeEditText();
78        void setAccessibility(boolean enabled);
79    }
80
81    private final ConversationInputHost mHost;
82    private final ConversationInputSink mSink;
83
84    /** Dependencies injected from the host during construction */
85    private final FragmentManager mFragmentManager;
86    private final Context mContext;
87    private final ImeStateHost mImeStateHost;
88    private final ImmutableBindingRef<ConversationData> mConversationDataModel;
89    private final ImmutableBindingRef<DraftMessageData> mDraftDataModel;
90
91    private final ConversationInput[] mInputs;
92    private final ConversationMediaPicker mMediaInput;
93    private final ConversationSimSelector mSimInput;
94    private final ConversationImeKeyboard mImeInput;
95    private int mUpdateCount;
96
97    private final ImeUtil.ImeStateObserver mImeStateObserver = new ImeUtil.ImeStateObserver() {
98        @Override
99        public void onImeStateChanged(final boolean imeOpen) {
100            mImeInput.onVisibilityChanged(imeOpen);
101        }
102    };
103
104    private final ConversationDataListener mDataListener = new SimpleConversationDataListener() {
105        @Override
106        public void onConversationParticipantDataLoaded(ConversationData data) {
107            mConversationDataModel.ensureBound(data);
108        }
109
110        @Override
111        public void onSubscriptionListDataLoaded(ConversationData data) {
112            mConversationDataModel.ensureBound(data);
113            mSimInput.onSubscriptionListDataLoaded(data.getSubscriptionListData());
114        }
115    };
116
117    public ConversationInputManager(
118            final Context context,
119            final ConversationInputHost host,
120            final ConversationInputSink sink,
121            final ImeStateHost imeStateHost,
122            final FragmentManager fm,
123            final BindingBase<ConversationData> conversationDataModel,
124            final BindingBase<DraftMessageData> draftDataModel,
125            final Bundle savedState) {
126        mHost = host;
127        mSink = sink;
128        mFragmentManager = fm;
129        mContext = context;
130        mImeStateHost = imeStateHost;
131        mConversationDataModel = BindingBase.createBindingReference(conversationDataModel);
132        mDraftDataModel = BindingBase.createBindingReference(draftDataModel);
133
134        // Register listeners on dependencies.
135        mImeStateHost.registerImeStateObserver(mImeStateObserver);
136        mConversationDataModel.getData().addConversationDataListener(mDataListener);
137
138        // Initialize the inputs
139        mMediaInput = new ConversationMediaPicker(this);
140        mSimInput = new SimSelector(this);
141        mImeInput = new ConversationImeKeyboard(this, mImeStateHost.isImeOpen());
142        mInputs = new ConversationInput[] { mMediaInput, mSimInput, mImeInput };
143
144        if (savedState != null) {
145            for (int i = 0; i < mInputs.length; i++) {
146                mInputs[i].restoreState(savedState);
147            }
148        }
149        updateHostOptionsMenu();
150    }
151
152    public void onDetach() {
153        mImeStateHost.unregisterImeStateObserver(mImeStateObserver);
154        // Don't need to explicitly unregister for data model events. It will unregister all
155        // listeners automagically on unbind.
156    }
157
158    public void onSaveInputState(final Bundle savedState) {
159        for (int i = 0; i < mInputs.length; i++) {
160            mInputs[i].saveState(savedState);
161        }
162    }
163
164    @Override
165    public String getInputStateKey(final ConversationInput input) {
166        return input.getClass().getCanonicalName() + "_savedstate_";
167    }
168
169    public boolean onBackPressed() {
170        for (int i = 0; i < mInputs.length; i++) {
171            if (mInputs[i].onBackPressed()) {
172                return true;
173            }
174        }
175        return false;
176    }
177
178    public boolean onNavigationUpPressed() {
179        for (int i = 0; i < mInputs.length; i++) {
180            if (mInputs[i].onNavigationUpPressed()) {
181                return true;
182            }
183        }
184        return false;
185    }
186
187    public void resetMediaPickerState() {
188        mMediaInput.resetViewHolderState();
189    }
190
191    public void showHideMediaPicker(final boolean show, final boolean animate) {
192        showHideInternal(mMediaInput, show, animate);
193    }
194
195    /**
196     * Show or hide the sim selector
197     * @param show visibility
198     * @param animate whether to animate the change in visibility
199     * @return true if the state of the visibility was changed
200     */
201    public boolean showHideSimSelector(final boolean show, final boolean animate) {
202        return showHideInternal(mSimInput, show, animate);
203    }
204
205    public void showHideImeKeyboard(final boolean show, final boolean animate) {
206        showHideInternal(mImeInput, show, animate);
207    }
208
209    public void hideAllInputs(final boolean animate) {
210        beginUpdate();
211        for (int i = 0; i < mInputs.length; i++) {
212            showHideInternal(mInputs[i], false, animate);
213        }
214        endUpdate();
215    }
216
217    /**
218     * Toggle the visibility of the sim selector.
219     * @param animate
220     * @param subEntry
221     * @return true if the view is now shown, false if it now hidden
222     */
223    public boolean toggleSimSelector(final boolean animate, final SubscriptionListEntry subEntry) {
224        mSimInput.setSelected(subEntry);
225        return mSimInput.toggle(animate);
226    }
227
228    public boolean updateActionBar(final ActionBar actionBar) {
229        for (int i = 0; i < mInputs.length; i++) {
230            if (mInputs[i].mShowing) {
231                return mInputs[i].updateActionBar(actionBar);
232            }
233        }
234        return false;
235    }
236
237    @VisibleForTesting
238    boolean isMediaPickerVisible() {
239        return mMediaInput.mShowing;
240    }
241
242    @VisibleForTesting
243    boolean isSimSelectorVisible() {
244        return mSimInput.mShowing;
245    }
246
247    @VisibleForTesting
248    boolean isImeKeyboardVisible() {
249        return mImeInput.mShowing;
250    }
251
252    @VisibleForTesting
253    void testNotifyImeStateChanged(final boolean imeOpen) {
254        mImeStateObserver.onImeStateChanged(imeOpen);
255    }
256
257    /**
258     * returns true if the state of the visibility was actually changed
259     */
260    @Override
261    public boolean showHideInternal(final ConversationInput target, final boolean show,
262            final boolean animate) {
263        if (!mConversationDataModel.isBound()) {
264            return false;
265        }
266
267        if (target.mShowing == show) {
268            return false;
269        }
270        beginUpdate();
271        boolean success;
272        if (!show) {
273            success = target.hide(animate);
274        } else {
275            success = target.show(animate);
276        }
277
278        if (success) {
279            target.onVisibilityChanged(show);
280        }
281        endUpdate();
282        return true;
283    }
284
285    @Override
286    public void handleOnShow(final ConversationInput target) {
287        if (!mConversationDataModel.isBound()) {
288            return;
289        }
290        beginUpdate();
291
292        // All inputs are mutually exclusive. Showing one will hide everything else.
293        // The one exception, is that the keyboard and location media chooser can be open at the
294        // time to enable searching within that chooser
295        for (int i = 0; i < mInputs.length; i++) {
296            final ConversationInput currInput = mInputs[i];
297            if (currInput != target) {
298                // TODO : If there's more exceptions we will want to make this more
299                // generic
300                if (currInput instanceof ConversationMediaPicker &&
301                        target instanceof ConversationImeKeyboard &&
302                        mMediaInput.getExistingOrCreateMediaPicker() != null &&
303                        mMediaInput.getExistingOrCreateMediaPicker().canShowIme()) {
304                    // Allow the keyboard and location mediaPicker to be open at the same time,
305                    // but ensure the media picker is full screen to allow enough room
306                    mMediaInput.getExistingOrCreateMediaPicker().setFullScreen(true);
307                    continue;
308                }
309                showHideInternal(currInput, false /* show */, false /* animate */);
310            }
311        }
312        // Always dismiss action mode on show.
313        mHost.dismissActionMode();
314        // Invoking any non-keyboard input UI is treated as starting message compose.
315        if (target != mImeInput) {
316            mHost.onStartComposeMessage();
317        }
318        endUpdate();
319    }
320
321    @Override
322    public void beginUpdate() {
323        mUpdateCount++;
324    }
325
326    @Override
327    public void endUpdate() {
328        Assert.isTrue(mUpdateCount > 0);
329        if (--mUpdateCount == 0) {
330            // Always try to update the host action bar after every update cycle.
331            mHost.invalidateActionBar();
332        }
333    }
334
335    private void updateHostOptionsMenu() {
336        mHost.setOptionsMenuVisibility(!mMediaInput.isOpen());
337    }
338
339    /**
340     * Manages showing/hiding the media picker in conversation.
341     */
342    private class ConversationMediaPicker extends ConversationInput {
343        public ConversationMediaPicker(ConversationInputBase baseHost) {
344            super(baseHost, false);
345        }
346
347        private MediaPicker mMediaPicker;
348
349        @Override
350        public boolean show(boolean animate) {
351            if (mMediaPicker == null) {
352                mMediaPicker = getExistingOrCreateMediaPicker();
353                setConversationThemeColor(ConversationDrawables.get().getConversationThemeColor());
354                mMediaPicker.setSubscriptionDataProvider(mHost);
355                mMediaPicker.setDraftMessageDataModel(mDraftDataModel);
356                mMediaPicker.setListener(new MediaPickerListener() {
357                    @Override
358                    public void onOpened() {
359                        handleStateChange();
360                    }
361
362                    @Override
363                    public void onFullScreenChanged(boolean fullScreen) {
364                        // When we're full screen, we want to disable accessibility on the
365                        // ComposeMessageView controls (attach button, message input, sim chooser)
366                        // that are hiding underneath the action bar.
367                        mSink.setAccessibility(!fullScreen /*enabled*/);
368                        handleStateChange();
369                    }
370
371                    @Override
372                    public void onDismissed() {
373                        // Re-enable accessibility on all controls now that the media picker is
374                        // going away.
375                        mSink.setAccessibility(true /*enabled*/);
376                        handleStateChange();
377                    }
378
379                    private void handleStateChange() {
380                        onVisibilityChanged(isOpen());
381                        mHost.invalidateActionBar();
382                        updateHostOptionsMenu();
383                    }
384
385                    @Override
386                    public void onItemsSelected(final Collection<MessagePartData> items,
387                            final boolean resumeCompose) {
388                        mSink.onMediaItemsSelected(items);
389                        mHost.invalidateActionBar();
390                        if (resumeCompose) {
391                            mSink.resumeComposeMessage();
392                        }
393                    }
394
395                    @Override
396                    public void onItemUnselected(final MessagePartData item) {
397                        mSink.onMediaItemsUnselected(item);
398                        mHost.invalidateActionBar();
399                    }
400
401                    @Override
402                    public void onConfirmItemSelection() {
403                        mSink.resumeComposeMessage();
404                    }
405
406                    @Override
407                    public void onPendingItemAdded(final PendingAttachmentData pendingItem) {
408                        mSink.onPendingAttachmentAdded(pendingItem);
409                    }
410
411                    @Override
412                    public void onChooserSelected(final int chooserIndex) {
413                        mHost.invalidateActionBar();
414                        mHost.dismissActionMode();
415                    }
416                });
417            }
418
419            mMediaPicker.open(MediaPicker.MEDIA_TYPE_DEFAULT, animate);
420
421            return isOpen();
422        }
423
424        @Override
425        public boolean hide(boolean animate) {
426            if (mMediaPicker != null) {
427                mMediaPicker.dismiss(animate);
428            }
429            return !isOpen();
430        }
431
432        public void resetViewHolderState() {
433            if (mMediaPicker != null) {
434                mMediaPicker.resetViewHolderState();
435            }
436        }
437
438        public void setConversationThemeColor(final int themeColor) {
439            if (mMediaPicker != null) {
440                mMediaPicker.setConversationThemeColor(themeColor);
441            }
442        }
443
444        private boolean isOpen() {
445            return (mMediaPicker != null && mMediaPicker.isOpen());
446        }
447
448        private MediaPicker getExistingOrCreateMediaPicker() {
449            if (mMediaPicker != null) {
450                return mMediaPicker;
451            }
452            MediaPicker mediaPicker = (MediaPicker)
453                    mFragmentManager.findFragmentByTag(MediaPicker.FRAGMENT_TAG);
454            if (mediaPicker == null) {
455                mediaPicker = mHost.createMediaPicker();
456                if (mediaPicker == null) {
457                    return null;    // this use of ComposeMessageView doesn't support media picking
458                }
459                mFragmentManager.beginTransaction().replace(
460                        R.id.mediapicker_container,
461                        mediaPicker,
462                        MediaPicker.FRAGMENT_TAG).commit();
463            }
464            return mediaPicker;
465        }
466
467        @Override
468        public boolean updateActionBar(ActionBar actionBar) {
469            if (isOpen()) {
470                mMediaPicker.updateActionBar(actionBar);
471                return true;
472            }
473            return false;
474        }
475
476        @Override
477        public boolean onNavigationUpPressed() {
478            if (isOpen() && mMediaPicker.isFullScreen()) {
479                return onBackPressed();
480            }
481            return super.onNavigationUpPressed();
482        }
483
484        public boolean onBackPressed() {
485            if (mMediaPicker != null && mMediaPicker.onBackPressed()) {
486                return true;
487            }
488            return super.onBackPressed();
489        }
490    }
491
492    /**
493     * Manages showing/hiding the SIM selector in conversation.
494     */
495    private class SimSelector extends ConversationSimSelector {
496        public SimSelector(ConversationInputBase baseHost) {
497            super(baseHost);
498        }
499
500        @Override
501        protected SimSelectorView getSimSelectorView() {
502            return mHost.getSimSelectorView();
503        }
504
505        @Override
506        public int getSimSelectorItemLayoutId() {
507            return mHost.getSimSelectorItemLayoutId();
508        }
509
510        @Override
511        protected void selectSim(SubscriptionListEntry item) {
512            mHost.selectSim(item);
513        }
514
515        @Override
516        public boolean show(boolean animate) {
517            final boolean result = super.show(animate);
518            mHost.showHideSimSelector(true /*show*/);
519            return result;
520        }
521
522        @Override
523        public boolean hide(boolean animate) {
524            final boolean result = super.hide(animate);
525            mHost.showHideSimSelector(false /*show*/);
526            return result;
527        }
528    }
529
530    /**
531     * Manages showing/hiding the IME keyboard in conversation.
532     */
533    private class ConversationImeKeyboard extends ConversationInput {
534        public ConversationImeKeyboard(ConversationInputBase baseHost, final boolean isShowing) {
535            super(baseHost, isShowing);
536        }
537
538        @Override
539        public boolean show(boolean animate) {
540            ImeUtil.get().showImeKeyboard(mContext, mSink.getComposeEditText());
541            return true;
542        }
543
544        @Override
545        public boolean hide(boolean animate) {
546            ImeUtil.get().hideImeKeyboard(mContext, mSink.getComposeEditText());
547            return true;
548        }
549    }
550}
551