Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2014 Google Inc.
      3  * Licensed to The Android Open Source Project.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.mail.ui;
     19 
     20 import android.animation.Animator;
     21 import android.animation.AnimatorListenerAdapter;
     22 import android.app.Activity;
     23 import android.content.ActivityNotFoundException;
     24 import android.content.Intent;
     25 import android.os.AsyncTask;
     26 import android.os.Bundle;
     27 import android.speech.RecognizerIntent;
     28 import android.text.TextUtils;
     29 import android.view.View;
     30 import android.widget.Toast;
     31 
     32 import com.android.mail.ConversationListContext;
     33 import com.android.mail.R;
     34 import com.android.mail.providers.SearchRecentSuggestionsProvider;
     35 import com.android.mail.utils.ViewUtils;
     36 
     37 import java.util.Locale;
     38 
     39 /**
     40  * Controller for interactions between ActivityController and our custom search views.
     41  */
     42 public class MaterialSearchViewController implements ViewMode.ModeChangeListener,
     43         TwoPaneLayout.ConversationListLayoutListener {
     44     private static final long FADE_IN_OUT_DURATION_MS = 150;
     45 
     46     // The controller is not in search mode. Both search action bar and the suggestion list
     47     // are not visible to the user.
     48     public static final int SEARCH_VIEW_STATE_GONE = 0;
     49     // The controller is actively in search (as in the action bar is focused and the user can type
     50     // into the search query). Both the search action bar and the suggestion list are visible.
     51     public static final int SEARCH_VIEW_STATE_VISIBLE = 1;
     52     // The controller is in a search ViewMode but not actively searching. This is relevant when
     53     // we have to show the search actionbar on top while the user is not interacting with it.
     54     public static final int SEARCH_VIEW_STATE_ONLY_ACTIONBAR = 2;
     55 
     56     private static final String EXTRA_CONTROLLER_STATE = "extraSearchViewControllerViewState";
     57 
     58     private MailActivity mActivity;
     59     private ActivityController mController;
     60 
     61     private SearchRecentSuggestionsProvider mSuggestionsProvider;
     62 
     63     private MaterialSearchActionView mSearchActionView;
     64     private MaterialSearchSuggestionsList mSearchSuggestionList;
     65 
     66     private int mViewMode;
     67     private int mControllerState;
     68     private int mEndXCoordForTabletLandscape;
     69 
     70     private boolean mSavePending;
     71     private boolean mDestroyProvider;
     72 
     73     public MaterialSearchViewController(MailActivity activity, ActivityController controller,
     74             Intent intent, Bundle savedInstanceState) {
     75         mActivity = activity;
     76         mController = controller;
     77 
     78         final Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
     79         final boolean supportVoice =
     80                 voiceIntent.resolveActivity(mActivity.getPackageManager()) != null;
     81 
     82         mSuggestionsProvider = mActivity.getSuggestionsProvider();
     83         mSearchSuggestionList = (MaterialSearchSuggestionsList) mActivity.findViewById(
     84                 R.id.search_overlay_view);
     85         mSearchSuggestionList.setController(this, mSuggestionsProvider);
     86         mSearchActionView = (MaterialSearchActionView) mActivity.findViewById(
     87                 R.id.search_actionbar_view);
     88         mSearchActionView.setController(this, intent.getStringExtra(
     89                 ConversationListContext.EXTRA_SEARCH_QUERY), supportVoice);
     90 
     91         if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_CONTROLLER_STATE)) {
     92             mControllerState = savedInstanceState.getInt(EXTRA_CONTROLLER_STATE);
     93         }
     94 
     95         mActivity.getViewMode().addListener(this);
     96     }
     97 
     98     /**
     99      * This controller should not be used after this is called.
    100      */
    101     public void onDestroy() {
    102         mDestroyProvider = mSavePending;
    103         if (!mSavePending) {
    104             mSuggestionsProvider.cleanup();
    105         }
    106         mActivity.getViewMode().removeListener(this);
    107         mActivity = null;
    108         mController = null;
    109         mSearchActionView = null;
    110         mSearchSuggestionList = null;
    111     }
    112 
    113     public void saveState(Bundle outState) {
    114         outState.putInt(EXTRA_CONTROLLER_STATE, mControllerState);
    115     }
    116 
    117     @Override
    118     public void onViewModeChanged(int newMode) {
    119         final int oldMode = mViewMode;
    120         mViewMode = newMode;
    121         // Never animate visibility changes that are caused by view state changes.
    122         if (mController.shouldShowSearchBarByDefault(mViewMode)) {
    123             showSearchActionBar(SEARCH_VIEW_STATE_ONLY_ACTIONBAR, false /* animate */);
    124         } else if (oldMode == ViewMode.UNKNOWN) {
    125             showSearchActionBar(mControllerState, false /* animate */);
    126         } else {
    127             showSearchActionBar(SEARCH_VIEW_STATE_GONE, false /* animate */);
    128         }
    129     }
    130 
    131     @Override
    132     public void onConversationListLayout(int xEnd, boolean drawerOpen) {
    133         // Only care about the first layout
    134         if (mEndXCoordForTabletLandscape != xEnd) {
    135             // This is called when we get into tablet landscape mode
    136             mEndXCoordForTabletLandscape = xEnd;
    137             if (ViewMode.isSearchMode(mViewMode)) {
    138                 final int defaultVisibility = mController.shouldShowSearchBarByDefault(mViewMode) ?
    139                         View.VISIBLE : View.GONE;
    140                 setViewVisibilityAndAlpha(mSearchActionView,
    141                         drawerOpen ? View.INVISIBLE : defaultVisibility);
    142             }
    143             adjustViewForTwoPaneLandscape();
    144         }
    145     }
    146 
    147     public boolean handleBackPress() {
    148         final boolean shouldShowSearchBar = mController.shouldShowSearchBarByDefault(mViewMode);
    149         if (shouldShowSearchBar && mSearchSuggestionList.isShown()) {
    150             showSearchActionBar(SEARCH_VIEW_STATE_ONLY_ACTIONBAR);
    151             return true;
    152         } else if (!shouldShowSearchBar && mSearchActionView.isShown()) {
    153             showSearchActionBar(SEARCH_VIEW_STATE_GONE);
    154             return true;
    155         }
    156         return false;
    157     }
    158 
    159     /**
    160      * Set the new visibility state of the search controller.
    161      * @param state the new view state, must be one of the following options:
    162      *   {@link MaterialSearchViewController#SEARCH_VIEW_STATE_ONLY_ACTIONBAR},
    163      *   {@link MaterialSearchViewController#SEARCH_VIEW_STATE_VISIBLE},
    164      *   {@link MaterialSearchViewController#SEARCH_VIEW_STATE_GONE},
    165      */
    166     public void showSearchActionBar(int state) {
    167         // By default animate the visibility changes
    168         showSearchActionBar(state, true /* animate */);
    169     }
    170 
    171     /**
    172      * @param animate if true, the search bar and suggestion list will fade in/out of view.
    173      */
    174     public void showSearchActionBar(int state, boolean animate) {
    175         mControllerState = state;
    176 
    177         // ACTIONBAR is only applicable in search mode
    178         final boolean onlyActionBar = state == SEARCH_VIEW_STATE_ONLY_ACTIONBAR &&
    179                 mController.shouldShowSearchBarByDefault(mViewMode);
    180         final boolean isStateVisible = state == SEARCH_VIEW_STATE_VISIBLE;
    181 
    182         final boolean isSearchBarVisible = isStateVisible || onlyActionBar;
    183 
    184         final int searchBarVisibility = isSearchBarVisible ? View.VISIBLE : View.GONE;
    185         final int suggestionListVisibility = isStateVisible ? View.VISIBLE : View.GONE;
    186         if (animate) {
    187             fadeInOutView(mSearchActionView, searchBarVisibility);
    188             fadeInOutView(mSearchSuggestionList, suggestionListVisibility);
    189         } else {
    190             setViewVisibilityAndAlpha(mSearchActionView, searchBarVisibility);
    191             setViewVisibilityAndAlpha(mSearchSuggestionList, suggestionListVisibility);
    192         }
    193         mSearchActionView.focusSearchBar(isStateVisible);
    194 
    195         final boolean useDefaultColor = !isSearchBarVisible || shouldAlignWithTl();
    196         final int statusBarColor = useDefaultColor ? R.color.mail_activity_status_bar_color :
    197                 R.color.search_status_bar_color;
    198         ViewUtils.setStatusBarColor(mActivity, statusBarColor);
    199 
    200         // Specific actions for each view state
    201         if (onlyActionBar) {
    202             adjustViewForTwoPaneLandscape();
    203         } else if (isStateVisible) {
    204             // Set to default layout/assets
    205             mSearchActionView.adjustViewForTwoPaneLandscape(false /* do not align */, 0);
    206         } else {
    207             // For non-search view mode, clear the query term for search
    208             if (!ViewMode.isSearchMode(mViewMode)) {
    209                 mSearchActionView.clearSearchQuery();
    210             }
    211         }
    212     }
    213 
    214     /**
    215      * Helper function to fade in/out the provided view by animating alpha.
    216      */
    217     private void fadeInOutView(final View v, final int visibility) {
    218         if (visibility == View.VISIBLE) {
    219             v.setVisibility(View.VISIBLE);
    220             v.animate()
    221                     .alpha(1f)
    222                     .setDuration(FADE_IN_OUT_DURATION_MS)
    223                     .setListener(null);
    224         } else {
    225             v.animate()
    226                     .alpha(0f)
    227                     .setDuration(FADE_IN_OUT_DURATION_MS)
    228                     .setListener(new AnimatorListenerAdapter() {
    229                         @Override
    230                         public void onAnimationEnd(Animator animation) {
    231                             v.setVisibility(visibility);
    232                         }
    233                     });
    234         }
    235     }
    236 
    237     /**
    238      * Sets the view's visibility and alpha so that we are guaranteed that alpha = 1 when the view
    239      * is visible, and alpha = 0 otherwise.
    240      */
    241     private void setViewVisibilityAndAlpha(View v, int visibility) {
    242         v.setVisibility(visibility);
    243         if (visibility == View.VISIBLE) {
    244             v.setAlpha(1f);
    245         } else {
    246             v.setAlpha(0f);
    247         }
    248     }
    249 
    250     private boolean shouldAlignWithTl() {
    251         return mController.isTwoPaneLandscape() &&
    252                 mControllerState == SEARCH_VIEW_STATE_ONLY_ACTIONBAR &&
    253                 ViewMode.isSearchMode(mViewMode);
    254     }
    255 
    256     private void adjustViewForTwoPaneLandscape() {
    257         // Try to adjust if the layout happened already
    258         if (mEndXCoordForTabletLandscape != 0) {
    259             mSearchActionView.adjustViewForTwoPaneLandscape(shouldAlignWithTl(),
    260                     mEndXCoordForTabletLandscape);
    261         }
    262     }
    263 
    264     public void onQueryTextChanged(String query) {
    265         mSearchSuggestionList.setQuery(query);
    266     }
    267 
    268     public void onSearchCanceled() {
    269         // Special case search mode
    270         if (ViewMode.isSearchMode(mViewMode)) {
    271             mActivity.setResult(Activity.RESULT_OK);
    272             mActivity.finish();
    273         } else {
    274             mSearchActionView.clearSearchQuery();
    275             showSearchActionBar(SEARCH_VIEW_STATE_GONE);
    276         }
    277     }
    278 
    279     public void onSearchPerformed(String query) {
    280         query = query.trim();
    281         if (!TextUtils.isEmpty(query)) {
    282             mSearchActionView.clearSearchQuery();
    283             mController.executeSearch(query);
    284         }
    285     }
    286 
    287     public void onVoiceSearch() {
    288         final Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
    289         intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
    290                 RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
    291         intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault().getLanguage());
    292 
    293         // Some devices do not support the voice-to-speech functionality.
    294         try {
    295             mActivity.startActivityForResult(intent,
    296                     AbstractActivityController.VOICE_SEARCH_REQUEST_CODE);
    297         } catch (ActivityNotFoundException e) {
    298             final String toast =
    299                     mActivity.getResources().getString(R.string.voice_search_not_supported);
    300             Toast.makeText(mActivity, toast, Toast.LENGTH_LONG).show();
    301         }
    302     }
    303 
    304     public void saveRecentQuery(String query) {
    305         new SaveRecentQueryTask().execute(query);
    306     }
    307 
    308     // static asynctask to save the query in the background.
    309     private class SaveRecentQueryTask extends AsyncTask<String, Void, Void> {
    310 
    311         @Override
    312         protected void onPreExecute() {
    313             mSavePending = true;
    314         }
    315 
    316         @Override
    317         protected Void doInBackground(String... args) {
    318             mSuggestionsProvider.saveRecentQuery(args[0]);
    319             return null;
    320         }
    321 
    322         @Override
    323         protected void onPostExecute(Void aVoid) {
    324             if (mDestroyProvider) {
    325                 mSuggestionsProvider.cleanup();
    326                 mDestroyProvider = false;
    327             }
    328             mSavePending = false;
    329         }
    330     }
    331 }
    332