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