1/*
2 * Copyright (C) 2010 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
17package com.android.contacts.activities;
18
19import android.animation.ValueAnimator;
20import android.app.ActionBar;
21import android.content.Context;
22import android.content.SharedPreferences;
23import android.content.res.TypedArray;
24import android.os.Bundle;
25import android.preference.PreferenceManager;
26import android.text.Editable;
27import android.text.TextUtils;
28import android.text.TextWatcher;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.inputmethod.InputMethodManager;
33import android.widget.SearchView.OnCloseListener;
34import android.view.View.OnClickListener;
35import android.widget.EditText;
36import android.widget.Toolbar;
37
38import com.android.contacts.R;
39import com.android.contacts.activities.ActionBarAdapter.Listener.Action;
40import com.android.contacts.list.ContactsRequest;
41
42/**
43 * Adapter for the action bar at the top of the Contacts activity.
44 */
45public class ActionBarAdapter implements OnCloseListener {
46
47    public interface Listener {
48        public abstract class Action {
49            public static final int CHANGE_SEARCH_QUERY = 0;
50            public static final int START_SEARCH_MODE = 1;
51            public static final int STOP_SEARCH_MODE = 2;
52        }
53
54        void onAction(int action);
55
56        /**
57         * Called when the user selects a tab.  The new tab can be obtained using
58         * {@link #getCurrentTab}.
59         */
60        void onSelectedTabChanged();
61
62        void onUpButtonPressed();
63    }
64
65    private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode";
66    private static final String EXTRA_KEY_QUERY = "navBar.query";
67    private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab";
68
69    private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab";
70
71    private boolean mSearchMode;
72    private String mQueryString;
73
74    private EditText mSearchView;
75    /** The view that represents tabs when we are in portrait mode **/
76    private View mPortraitTabs;
77    /** The view that represents tabs when we are in landscape mode **/
78    private View mLandscapeTabs;
79    private View mSearchContainer;
80
81    private int mMaxPortraitTabHeight;
82    private int mMaxToolbarContentInsetStart;
83
84    private final Context mContext;
85    private final SharedPreferences mPrefs;
86
87    private Listener mListener;
88
89    private final ActionBar mActionBar;
90    private final Toolbar mToolbar;
91
92    private boolean mShowHomeIcon;
93
94    public interface TabState {
95        public static int FAVORITES = 0;
96        public static int ALL = 1;
97
98        public static int COUNT = 2;
99        public static int DEFAULT = ALL;
100    }
101
102    private int mCurrentTab = TabState.DEFAULT;
103
104    public ActionBarAdapter(Context context, Listener listener, ActionBar actionBar,
105            View portraitTabs, View landscapeTabs, Toolbar toolbar) {
106        mContext = context;
107        mListener = listener;
108        mActionBar = actionBar;
109        mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
110        mPortraitTabs = portraitTabs;
111        mLandscapeTabs = landscapeTabs;
112        mToolbar = toolbar;
113        mMaxToolbarContentInsetStart = mToolbar.getContentInsetStart();
114        mShowHomeIcon = mContext.getResources().getBoolean(R.bool.show_home_icon);
115
116        setupSearchView();
117        setupTabs(context);
118    }
119
120    private void setupTabs(Context context) {
121        final TypedArray attributeArray = context.obtainStyledAttributes(
122                new int[]{android.R.attr.actionBarSize});
123        mMaxPortraitTabHeight = attributeArray.getDimensionPixelSize(0, 0);
124        // Hide tabs initially
125        setPortraitTabHeight(0);
126    }
127
128    private void setupSearchView() {
129        final LayoutInflater inflater = (LayoutInflater) mToolbar.getContext().getSystemService(
130                Context.LAYOUT_INFLATER_SERVICE);
131        mSearchContainer = inflater.inflate(R.layout.search_bar_expanded, mToolbar,
132                /* attachToRoot = */ false);
133        mSearchContainer.setVisibility(View.VISIBLE);
134        mToolbar.addView(mSearchContainer);
135
136        mSearchContainer.setBackgroundColor(mContext.getResources().getColor(
137                R.color.searchbox_background_color));
138        mSearchView = (EditText) mSearchContainer.findViewById(R.id.search_view);
139        mSearchView.setHint(mContext.getString(R.string.hint_findContacts));
140        mSearchView.addTextChangedListener(new SearchTextWatcher());
141        mSearchContainer.findViewById(R.id.search_close_button).setOnClickListener(
142                new OnClickListener() {
143            @Override
144            public void onClick(View v) {
145                setQueryString(null);
146            }
147        });
148        mSearchContainer.findViewById(R.id.search_back_button).setOnClickListener(
149                new OnClickListener() {
150            @Override
151            public void onClick(View v) {
152                if (mListener != null) {
153                    mListener.onUpButtonPressed();
154                }
155            }
156        });
157    }
158
159    public void initialize(Bundle savedState, ContactsRequest request) {
160        if (savedState == null) {
161            mSearchMode = request.isSearchMode();
162            mQueryString = request.getQueryString();
163            mCurrentTab = loadLastTabPreference();
164        } else {
165            mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
166            mQueryString = savedState.getString(EXTRA_KEY_QUERY);
167
168            // Just set to the field here.  The listener will be notified by update().
169            mCurrentTab = savedState.getInt(EXTRA_KEY_SELECTED_TAB);
170        }
171        if (mCurrentTab >= TabState.COUNT || mCurrentTab < 0) {
172            // Invalid tab index was saved (b/12938207). Restore the default.
173            mCurrentTab = TabState.DEFAULT;
174        }
175        // Show tabs or the expanded {@link SearchView}, depending on whether or not we are in
176        // search mode.
177        update(true /* skipAnimation */);
178        // Expanding the {@link SearchView} clears the query, so set the query from the
179        // {@link ContactsRequest} after it has been expanded, if applicable.
180        if (mSearchMode && !TextUtils.isEmpty(mQueryString)) {
181            setQueryString(mQueryString);
182        }
183    }
184
185    public void setListener(Listener listener) {
186        mListener = listener;
187    }
188
189    private class SearchTextWatcher implements TextWatcher {
190
191        @Override
192        public void onTextChanged(CharSequence queryString, int start, int before, int count) {
193            if (queryString.equals(mQueryString)) {
194                return;
195            }
196            mQueryString = queryString.toString();
197            if (!mSearchMode) {
198                if (!TextUtils.isEmpty(queryString)) {
199                    setSearchMode(true);
200                }
201            } else if (mListener != null) {
202                mListener.onAction(Action.CHANGE_SEARCH_QUERY);
203            }
204        }
205
206        @Override
207        public void afterTextChanged(Editable s) {}
208
209        @Override
210        public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
211    }
212
213    /**
214     * Save the current tab selection, and notify the listener.
215     */
216    public void setCurrentTab(int tab) {
217        setCurrentTab(tab, true);
218    }
219
220    /**
221     * Save the current tab selection.
222     */
223    public void setCurrentTab(int tab, boolean notifyListener) {
224        if (tab == mCurrentTab) {
225            return;
226        }
227        mCurrentTab = tab;
228
229        if (notifyListener && mListener != null) mListener.onSelectedTabChanged();
230        saveLastTabPreference(mCurrentTab);
231    }
232
233    public int getCurrentTab() {
234        return mCurrentTab;
235    }
236
237    /**
238     * @return Whether in search mode, i.e. if the search view is visible/expanded.
239     *
240     * Note even if the action bar is in search mode, if the query is empty, the search fragment
241     * will not be in search mode.
242     */
243    public boolean isSearchMode() {
244        return mSearchMode;
245    }
246
247    public void setSearchMode(boolean flag) {
248        if (mSearchMode != flag) {
249            mSearchMode = flag;
250            update(false /* skipAnimation */);
251            if (mSearchView == null) {
252                return;
253            }
254            if (mSearchMode) {
255                mSearchView.setEnabled(true);
256                setFocusOnSearchView();
257            } else {
258                // Disable search view, so that it doesn't keep the IME visible.
259                mSearchView.setEnabled(false);
260            }
261            setQueryString(null);
262        } else if (flag) {
263            // Everything is already set up. Still make sure the keyboard is up
264            if (mSearchView != null) setFocusOnSearchView();
265        }
266    }
267
268    public String getQueryString() {
269        return mSearchMode ? mQueryString : null;
270    }
271
272    public void setQueryString(String query) {
273        mQueryString = query;
274        if (mSearchView != null) {
275            mSearchView.setText(query);
276            // When programmatically entering text into the search view, the most reasonable
277            // place for the cursor is after all the text.
278            mSearchView.setSelection(mSearchView.getText() == null ?
279                    0 : mSearchView.getText().length());
280        }
281    }
282
283    /** @return true if the "UP" icon is showing. */
284    public boolean isUpShowing() {
285        return mSearchMode; // Only shown on the search mode.
286    }
287
288    private void updateDisplayOptionsInner() {
289        // All the flags we may change in this method.
290        final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME
291                | ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_CUSTOM;
292
293        // The current flags set to the action bar.  (only the ones that we may change here)
294        final int current = mActionBar.getDisplayOptions() & MASK;
295
296        // Build the new flags...
297        int newFlags = 0;
298        if (mShowHomeIcon && !mSearchMode) {
299            newFlags |= ActionBar.DISPLAY_SHOW_HOME;
300        }
301        if (mSearchMode) {
302            newFlags |= ActionBar.DISPLAY_SHOW_CUSTOM;
303            mToolbar.setContentInsetsRelative(0, mToolbar.getContentInsetEnd());
304        } else {
305            newFlags |= ActionBar.DISPLAY_SHOW_TITLE;
306            mToolbar.setContentInsetsRelative(mMaxToolbarContentInsetStart,
307                    mToolbar.getContentInsetEnd());
308        }
309
310
311        if (current != newFlags) {
312            // Pass the mask here to preserve other flags that we're not interested here.
313            mActionBar.setDisplayOptions(newFlags, MASK);
314        }
315    }
316
317    private void update(boolean skipAnimation) {
318        final boolean isIconifiedChanging
319                = (mSearchContainer.getParent() == null) == mSearchMode;
320        if (isIconifiedChanging && !skipAnimation) {
321            mToolbar.removeView(mLandscapeTabs);
322            if (mSearchMode) {
323                addSearchContainer();
324                mSearchContainer.setAlpha(0);
325                mSearchContainer.animate().alpha(1);
326                animateTabHeightChange(mMaxPortraitTabHeight, 0);
327                updateDisplayOptions(isIconifiedChanging);
328            } else {
329                mSearchContainer.setAlpha(1);
330                animateTabHeightChange(0, mMaxPortraitTabHeight);
331                mSearchContainer.animate().alpha(0).withEndAction(new Runnable() {
332                    @Override
333                    public void run() {
334                        updateDisplayOptionsInner();
335                        updateDisplayOptions(isIconifiedChanging);
336                        addLandscapeViewPagerTabs();
337                        mToolbar.removeView(mSearchContainer);
338                    }
339                });
340            }
341            return;
342        }
343        if (isIconifiedChanging && skipAnimation) {
344            mToolbar.removeView(mLandscapeTabs);
345            if (mSearchMode) {
346                setPortraitTabHeight(0);
347                addSearchContainer();
348            } else {
349                setPortraitTabHeight(mMaxPortraitTabHeight);
350                mToolbar.removeView(mSearchContainer);
351                addLandscapeViewPagerTabs();
352            }
353        }
354        updateDisplayOptions(isIconifiedChanging);
355    }
356
357    private void addLandscapeViewPagerTabs() {
358        if (mLandscapeTabs != null) {
359            mToolbar.removeView(mLandscapeTabs);
360            mToolbar.addView(mLandscapeTabs);
361        }
362    }
363
364    private void addSearchContainer() {
365        mToolbar.removeView(mSearchContainer);
366        mToolbar.addView(mSearchContainer);
367    }
368
369    private void updateDisplayOptions(boolean isIconifiedChanging) {
370        if (mSearchMode) {
371            setFocusOnSearchView();
372            // Since we have the {@link SearchView} in a custom action bar, we must manually handle
373            // expanding the {@link SearchView} when a search is initiated. Note that a side effect
374            // of this method is that the {@link SearchView} query text is set to empty string.
375            if (isIconifiedChanging) {
376                final CharSequence queryText = mSearchView.getText();
377                if (!TextUtils.isEmpty(queryText)) {
378                    mSearchView.setText(queryText);
379                }
380            }
381            if (mListener != null) {
382                mListener.onAction(Action.START_SEARCH_MODE);
383            }
384        } else {
385            if (mListener != null) {
386                mListener.onAction(Action.STOP_SEARCH_MODE);
387                mListener.onSelectedTabChanged();
388            }
389        }
390        updateDisplayOptionsInner();
391    }
392
393    @Override
394    public boolean onClose() {
395        setSearchMode(false);
396        return false;
397    }
398
399    public void onSaveInstanceState(Bundle outState) {
400        outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode);
401        outState.putString(EXTRA_KEY_QUERY, mQueryString);
402        outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab);
403    }
404
405    public void setFocusOnSearchView() {
406        mSearchView.requestFocus();
407        showInputMethod(mSearchView); // Workaround for the "IME not popping up" issue.
408    }
409
410    private void showInputMethod(View view) {
411        final InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
412                Context.INPUT_METHOD_SERVICE);
413        if (imm != null) {
414            imm.showSoftInput(view, 0);
415        }
416    }
417
418    private void saveLastTabPreference(int tab) {
419        mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab).apply();
420    }
421
422    private int loadLastTabPreference() {
423        try {
424            return mPrefs.getInt(PERSISTENT_LAST_TAB, TabState.DEFAULT);
425        } catch (IllegalArgumentException e) {
426            // Preference is corrupt?
427            return TabState.DEFAULT;
428        }
429    }
430
431    private void animateTabHeightChange(int start, int end) {
432        if (mPortraitTabs == null) {
433            return;
434        }
435        final ValueAnimator animator = ValueAnimator.ofInt(start, end);
436        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
437            @Override
438            public void onAnimationUpdate(ValueAnimator valueAnimator) {
439                int value = (Integer) valueAnimator.getAnimatedValue();
440                setPortraitTabHeight(value);
441            }
442        });
443        animator.setDuration(100).start();
444    }
445
446    private void setPortraitTabHeight(int height) {
447        if (mPortraitTabs == null) {
448            return;
449        }
450        ViewGroup.LayoutParams layoutParams = mPortraitTabs.getLayoutParams();
451        layoutParams.height = height;
452        mPortraitTabs.setLayoutParams(layoutParams);
453    }
454}
455