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
18package com.android.mail.ui;
19
20import android.animation.Animator;
21import android.animation.AnimatorListenerAdapter;
22import android.app.Activity;
23import android.content.ActivityNotFoundException;
24import android.content.Intent;
25import android.os.AsyncTask;
26import android.os.Bundle;
27import android.speech.RecognizerIntent;
28import android.text.TextUtils;
29import android.view.View;
30import android.widget.Toast;
31
32import com.android.mail.ConversationListContext;
33import com.android.mail.R;
34import com.android.mail.providers.SearchRecentSuggestionsProvider;
35import com.android.mail.utils.ViewUtils;
36
37import java.util.Locale;
38
39/**
40 * Controller for interactions between ActivityController and our custom search views.
41 */
42public 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