Home | History | Annotate | Download | only in conversation
      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.app.FragmentManager;
     19 import android.content.Context;
     20 import android.os.Bundle;
     21 import android.support.v7.app.ActionBar;
     22 import android.widget.EditText;
     23 
     24 import com.android.messaging.R;
     25 import com.android.messaging.datamodel.binding.BindingBase;
     26 import com.android.messaging.datamodel.binding.ImmutableBindingRef;
     27 import com.android.messaging.datamodel.data.ConversationData;
     28 import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener;
     29 import com.android.messaging.datamodel.data.ConversationData.SimpleConversationDataListener;
     30 import com.android.messaging.datamodel.data.DraftMessageData;
     31 import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider;
     32 import com.android.messaging.datamodel.data.MessagePartData;
     33 import com.android.messaging.datamodel.data.PendingAttachmentData;
     34 import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
     35 import com.android.messaging.ui.ConversationDrawables;
     36 import com.android.messaging.ui.mediapicker.MediaPicker;
     37 import com.android.messaging.ui.mediapicker.MediaPicker.MediaPickerListener;
     38 import com.android.messaging.util.Assert;
     39 import com.android.messaging.util.ImeUtil;
     40 import com.android.messaging.util.ImeUtil.ImeStateHost;
     41 import com.google.common.annotations.VisibleForTesting;
     42 
     43 import 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  */
     51 public 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