ActionBarController.java revision ed060515f11fd22b2f8e32eacab28a720ea552ca
1/*
2 * Copyright (C) 2011 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.email.activity;
18
19import android.app.ActionBar;
20import android.app.LoaderManager;
21import android.app.LoaderManager.LoaderCallbacks;
22import android.content.Context;
23import android.content.Loader;
24import android.database.Cursor;
25import android.graphics.drawable.Drawable;
26import android.os.Bundle;
27import android.text.TextUtils;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.AdapterView;
32import android.widget.AdapterView.OnItemClickListener;
33import android.widget.ListPopupWindow;
34import android.widget.ListView;
35import android.widget.SearchView;
36import android.widget.TextView;
37
38import com.android.email.R;
39import com.android.emailcommon.provider.Account;
40import com.android.emailcommon.provider.Mailbox;
41import com.android.emailcommon.utility.DelayedOperations;
42import com.android.emailcommon.utility.Utility;
43
44/**
45 * Manages the account name and the custom view part on the action bar.
46 */
47public class ActionBarController {
48    private static final String BUNDLE_KEY_MODE = "ActionBarController.BUNDLE_KEY_MODE";
49
50    /**
51     * Constants for {@link #mSearchMode}.
52     *
53     * In {@link #MODE_NORMAL} mode, we don't show the search box.
54     * In {@link #MODE_SEARCH} mode, we do show the search box.
55     * The action bar doesn't really care if the activity is showing search results.
56     * If the activity is showing search results, and the {@link Callback#onSearchExit} is called,
57     * the activity probably wants to close itself, but this class doesn't make the desision.
58     */
59    private static final int MODE_NORMAL = 0;
60    private static final int MODE_SEARCH = 1;
61
62    private static final int LOADER_ID_ACCOUNT_LIST
63            = EmailActivity.ACTION_BAR_CONTROLLER_LOADER_ID_BASE + 0;
64
65    private final Context mContext;
66    private final LoaderManager mLoaderManager;
67    private final ActionBar mActionBar;
68    private final DelayedOperations mDelayedOperations;
69
70    /** "Folders" label shown with account name on 1-pane mailbox list */
71    private final String mAllFoldersLabel;
72
73    private final ViewGroup mActionBarCustomView;
74    private final ViewGroup mAccountSpinnerContainer;
75    private final View mAccountSpinner;
76    private final Drawable mAccountSpinnerDefaultBackground;
77    private final TextView mAccountSpinnerLine1View;
78    private final TextView mAccountSpinnerLine2View;
79    private final TextView mAccountSpinnerCountView;
80
81    private View mSearchContainer;
82    private SearchView mSearchView;
83
84    private final AccountDropdownPopup mAccountDropdown;
85
86    private final AccountSelectorAdapter mAccountsSelectorAdapter;
87
88    private AccountSelectorAdapter.CursorWithExtras mCursor;
89
90    /** The current account ID; used to determine if the account has changed. */
91    private long mLastAccountIdForDirtyCheck = Account.NO_ACCOUNT;
92
93    /** The current mailbox ID; used to determine if the mailbox has changed. */
94    private long mLastMailboxIdForDirtyCheck = Mailbox.NO_MAILBOX;
95
96    /** Either {@link #MODE_NORMAL} or {@link #MODE_SEARCH}. */
97    private int mSearchMode = MODE_NORMAL;
98
99    /** The current title mode, which should be one of {@code Callback TITLE_MODE_*} */
100    private int mTitleMode;
101
102    public final Callback mCallback;
103
104    public interface SearchContext {
105        public long getTargetMailboxId();
106    }
107
108    private static final int TITLE_MODE_SPINNER_ENABLED = 0x10;
109
110    public interface Callback {
111        /** Values for {@link #getTitleMode}.  Show only account name */
112        public static final int TITLE_MODE_ACCOUNT_NAME_ONLY = 0 | TITLE_MODE_SPINNER_ENABLED;
113
114        /**
115         * Show the current account name with "Folders"
116         * The account spinner will be disabled in this mode.
117         */
118        public static final int TITLE_MODE_ACCOUNT_WITH_ALL_FOLDERS_LABEL = 1;
119
120        /**
121         * Show the current account name and the current mailbox name.
122         */
123        public static final int TITLE_MODE_ACCOUNT_WITH_MAILBOX = 2 | TITLE_MODE_SPINNER_ENABLED;
124        /**
125         * Show the current message subject.  Actual subject is obtained via
126         * {@link #getMessageSubject()}.
127         *
128         * The account spinner will be disabled in this mode.
129         */
130        public static final int TITLE_MODE_MESSAGE_SUBJECT = 3;
131
132        /** @return true if an account is selected. */
133        public boolean isAccountSelected();
134
135        /**
136         * @return currently selected account ID, {@link Account#ACCOUNT_ID_COMBINED_VIEW},
137         * or -1 if no account is selected.
138         */
139        public long getUIAccountId();
140
141        /**
142         * @return currently selected mailbox ID, or {@link Mailbox#NO_MAILBOX} if no mailbox is
143         * selected.
144         */
145        public long getMailboxId();
146
147        /**
148         * @return constants such as {@link #TITLE_MODE_ACCOUNT_NAME_ONLY}.
149         */
150        public int getTitleMode();
151
152        /** @see #TITLE_MODE_MESSAGE_SUBJECT */
153        public String getMessageSubject();
154
155        /** @return the "UP" arrow should be shown. */
156        public boolean shouldShowUp();
157
158        /**
159         * Called when an account is selected on the account spinner.
160         * @param accountId ID of the selected account, or {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
161         */
162        public void onAccountSelected(long accountId);
163
164        /**
165         * Invoked when a recent mailbox is selected on the account spinner.
166         *
167         * @param accountId ID of the selected account, or {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
168         * @param mailboxId The ID of the selected mailbox, or {@link Mailbox#NO_MAILBOX} if the
169         *          special option "show all mailboxes" was selected.
170         */
171        public void onMailboxSelected(long accountId, long mailboxId);
172
173        /** Called when no accounts are found in the database. */
174        public void onNoAccountsFound();
175
176        /**
177         * Retrieves the hint text to be shown for when a search entry is being made.
178         */
179        public String getSearchHint();
180
181        /**
182         * Called when the action bar initially shows the search entry field.
183         */
184        public void onSearchStarted();
185
186        /**
187         * Called when a search is submitted.
188         *
189         * @param queryTerm query string
190         */
191        public void onSearchSubmit(String queryTerm);
192
193        /**
194         * Called when the search box is closed.
195         */
196        public void onSearchExit();
197
198        public void onUpPressed();
199    }
200
201    public ActionBarController(Context context, LoaderManager loaderManager,
202            ActionBar actionBar, Callback callback) {
203        mContext = context;
204        mLoaderManager = loaderManager;
205        mActionBar = actionBar;
206        mCallback = callback;
207        mDelayedOperations = new DelayedOperations(Utility.getMainThreadHandler());
208        mAllFoldersLabel = mContext.getResources().getString(
209                R.string.action_bar_mailbox_list_title);
210        mAccountsSelectorAdapter = new AccountSelectorAdapter(mContext);
211
212        // Configure action bar.
213        mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_CUSTOM);
214
215        // Prepare the custom view
216        mActionBar.setCustomView(R.layout.action_bar_custom_view);
217        mActionBarCustomView = (ViewGroup) mActionBar.getCustomView();
218
219        // Account spinner
220        mAccountSpinnerContainer =
221                UiUtilities.getView(mActionBarCustomView, R.id.account_spinner_container);
222        mAccountSpinner = UiUtilities.getView(mActionBarCustomView, R.id.account_spinner);
223        mAccountSpinnerDefaultBackground = mAccountSpinner.getBackground();
224
225        mAccountSpinnerLine1View = UiUtilities.getView(mActionBarCustomView, R.id.spinner_line_1);
226        mAccountSpinnerLine2View = UiUtilities.getView(mActionBarCustomView, R.id.spinner_line_2);
227        mAccountSpinnerCountView = UiUtilities.getView(mActionBarCustomView, R.id.spinner_count);
228
229        // Account dropdown
230        mAccountDropdown = new AccountDropdownPopup(mContext);
231        mAccountDropdown.setAdapter(mAccountsSelectorAdapter);
232
233        mAccountSpinner.setOnClickListener(new View.OnClickListener() {
234            @Override public void onClick(View v) {
235                if (mAccountsSelectorAdapter.getCount() > 0) {
236                    mAccountDropdown.show();
237                }
238            }
239        });
240        // this other click listener handles clicks that ought to be aliased to "up"
241        // only one or the other listener should be active at any time
242        mActionBarCustomView.setOnClickListener(new View.OnClickListener() {
243            @Override
244            public void onClick(View v) {
245                mCallback.onUpPressed();
246            }
247        });
248        // pick a sane default. later enabled in updateTitle().
249        mActionBarCustomView.setClickable(false);
250    }
251
252    private void initSearchViews() {
253        if (mSearchContainer == null) {
254            final LayoutInflater inflater = LayoutInflater.from(mContext);
255            mSearchContainer = inflater.inflate(R.layout.action_bar_search, null);
256            mSearchView = UiUtilities.getView(mSearchContainer, R.id.search_view);
257            mSearchView.setSubmitButtonEnabled(false);
258            mSearchView.setOnQueryTextListener(mOnQueryText);
259            mSearchView.onActionViewExpanded();
260            mActionBarCustomView.addView(mSearchContainer);
261        }
262    }
263
264
265    /** Must be called from {@link UIControllerBase#onActivityCreated()} */
266    public void onActivityCreated() {
267        refresh();
268    }
269
270    /** Must be called from {@link UIControllerBase#onActivityDestroy()} */
271    public void onActivityDestroy() {
272        if (mAccountDropdown.isShowing()) {
273            mAccountDropdown.dismiss();
274        }
275    }
276
277    /** Must be called from {@link UIControllerBase#onSaveInstanceState} */
278    public void onSaveInstanceState(Bundle outState) {
279        mDelayedOperations.removeCallbacks(); // Remove all pending operations
280        outState.putInt(BUNDLE_KEY_MODE, mSearchMode);
281    }
282
283    /** Must be called from {@link UIControllerBase#onRestoreInstanceState} */
284    public void onRestoreInstanceState(Bundle savedState) {
285        int mode = savedState.getInt(BUNDLE_KEY_MODE);
286        if (mode == MODE_SEARCH) {
287            // No need to re-set the initial query, as the View tree restoration does that
288            enterSearchMode(null);
289        }
290    }
291
292    /**
293     * @return true if the search box is shown.
294     */
295    public boolean isInSearchMode() {
296        return mSearchMode == MODE_SEARCH;
297    }
298
299    /**
300     * @return Whether or not the search bar should be shown. This is a function of whether or not a
301     *     search is active, and if the current layout supports it.
302     */
303    private boolean shouldShowSearchBar() {
304        return isInSearchMode() && (mTitleMode != Callback.TITLE_MODE_MESSAGE_SUBJECT);
305    }
306
307    /**
308     * Show the search box.
309     *
310     * @param initialQueryTerm if non-empty, set to the search box.
311     */
312    public void enterSearchMode(String initialQueryTerm) {
313        initSearchViews();
314        if (isInSearchMode()) {
315            return;
316        }
317        if (!TextUtils.isEmpty(initialQueryTerm)) {
318            mSearchView.setQuery(initialQueryTerm, false);
319        } else {
320            mSearchView.setQuery("", false);
321        }
322        mSearchView.setQueryHint(mCallback.getSearchHint());
323
324        mSearchMode = MODE_SEARCH;
325
326        // Focus on the search input box and throw up the IME if specified.
327        // TODO: HACK. this is a workaround IME not popping up.
328        mSearchView.setIconified(false);
329
330        refresh();
331        mCallback.onSearchStarted();
332    }
333
334    public void exitSearchMode() {
335        if (!isInSearchMode()) {
336            return;
337        }
338        mSearchMode = MODE_NORMAL;
339
340        refresh();
341        mCallback.onSearchExit();
342    }
343
344    /**
345     * Performs the back action.
346     *
347     * @param isSystemBackKey <code>true</code> if the system back key was pressed.
348     * <code>false</code> if it's caused by the "home" icon click on the action bar.
349     */
350    public boolean onBackPressed(boolean isSystemBackKey) {
351        if (shouldShowSearchBar()) {
352            exitSearchMode();
353            return true;
354        }
355        return false;
356    }
357
358    /** Refreshes the action bar display. */
359    public void refresh() {
360        // The actual work is in refreshInernal(), but we don't call it directly here, because:
361        // 1. refresh() is called very often.
362        // 2. to avoid nested fragment transaction.
363        //    refresh is often called during a fragment transaction, but updateTitle() may call
364        //    a callback which would initiate another fragment transaction.
365        mDelayedOperations.removeCallbacks(mRefreshRunnable);
366        mDelayedOperations.post(mRefreshRunnable);
367    }
368
369    private final Runnable mRefreshRunnable = new Runnable() {
370        @Override public void run() {
371            refreshInernal();
372        }
373    };
374    private void refreshInernal() {
375        final boolean showUp = isInSearchMode() || mCallback.shouldShowUp();
376        mActionBar.setDisplayOptions(showUp
377                ? ActionBar.DISPLAY_HOME_AS_UP : 0, ActionBar.DISPLAY_HOME_AS_UP);
378
379        final long accountId = mCallback.getUIAccountId();
380        final long mailboxId = mCallback.getMailboxId();
381        if ((mLastAccountIdForDirtyCheck != accountId)
382                || (mLastMailboxIdForDirtyCheck != mailboxId)) {
383            mLastAccountIdForDirtyCheck = accountId;
384            mLastMailboxIdForDirtyCheck = mailboxId;
385
386            if (accountId != Account.NO_ACCOUNT) {
387                loadAccountMailboxInfo(accountId, mailboxId);
388            }
389        }
390
391        updateTitle();
392    }
393
394    /**
395     * Load account/mailbox info, and account/recent mailbox list.
396     */
397    private void loadAccountMailboxInfo(final long accountId, final long mailboxId) {
398        mLoaderManager.restartLoader(LOADER_ID_ACCOUNT_LIST, null,
399                new LoaderCallbacks<Cursor>() {
400            @Override
401            public Loader<Cursor> onCreateLoader(int id, Bundle args) {
402                return AccountSelectorAdapter.createLoader(mContext, accountId, mailboxId);
403            }
404
405            @Override
406            public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
407                mCursor = (AccountSelectorAdapter.CursorWithExtras) data;
408                updateTitle();
409            }
410
411            @Override
412            public void onLoaderReset(Loader<Cursor> loader) {
413                mCursor = null;
414                updateTitle();
415            }
416        });
417    }
418
419    /**
420     * Update the "title" part.
421     */
422    private void updateTitle() {
423        mAccountsSelectorAdapter.swapCursor(mCursor);
424
425        if (mCursor == null) {
426            // Initial load not finished.
427            mActionBarCustomView.setVisibility(View.GONE);
428            return;
429        }
430        mActionBarCustomView.setVisibility(View.VISIBLE);
431
432        if (mCursor.getAccountCount() == 0) {
433            mCallback.onNoAccountsFound();
434            return;
435        }
436
437        if ((mCursor.getAccountId() != Account.NO_ACCOUNT) && !mCursor.accountExists()) {
438            // Account specified, but does not exist.
439            if (isInSearchMode()) {
440                exitSearchMode();
441            }
442
443            // Switch to the default account.
444            mCallback.onAccountSelected(Account.getDefaultAccountId(mContext));
445            return;
446        }
447
448        mTitleMode = mCallback.getTitleMode();
449
450        if (shouldShowSearchBar()) {
451            initSearchViews();
452            // In search mode, the search box is a replacement of the account spinner, so ignore
453            // the work needed to update that. It will get updated when it goes visible again.
454            mAccountSpinnerContainer.setVisibility(View.GONE);
455            mSearchContainer.setVisibility(View.VISIBLE);
456            return;
457        }
458
459        // Account spinner visible.
460        mAccountSpinnerContainer.setVisibility(View.VISIBLE);
461        UiUtilities.setVisibilitySafe(mSearchContainer, View.GONE);
462
463        if (mTitleMode == Callback.TITLE_MODE_MESSAGE_SUBJECT) {
464            mAccountSpinnerLine1View.setSingleLine(false);
465            mAccountSpinnerLine1View.setMaxLines(2);
466            mAccountSpinnerLine1View.setText(mCallback.getMessageSubject());
467            mAccountSpinnerLine2View.setVisibility(View.GONE);
468
469            mAccountSpinnerCountView.setVisibility(View.GONE);
470
471        } else {
472            // Get mailbox name
473            final String mailboxName;
474            if (mTitleMode == Callback.TITLE_MODE_ACCOUNT_WITH_ALL_FOLDERS_LABEL) {
475                mailboxName = mAllFoldersLabel;
476            } else if (mTitleMode == Callback.TITLE_MODE_ACCOUNT_WITH_MAILBOX) {
477                mailboxName = mCursor.getMailboxDisplayName();
478            } else {
479                mailboxName = null;
480            }
481
482            // Note - setSingleLine is needed as well as setMaxLines since they set different
483            // flags on the view.
484            mAccountSpinnerLine1View.setSingleLine();
485            mAccountSpinnerLine1View.setMaxLines(1);
486            if (TextUtils.isEmpty(mailboxName)) {
487                mAccountSpinnerLine1View.setText(mCursor.getAccountDisplayName());
488
489                // Change the visibility of line 2, so line 1 will be vertically-centered.
490                mAccountSpinnerLine2View.setVisibility(View.GONE);
491            } else {
492                mAccountSpinnerLine1View.setText(mailboxName);
493                mAccountSpinnerLine2View.setVisibility(View.VISIBLE);
494                mAccountSpinnerLine2View.setText(mCursor.getAccountDisplayName());
495            }
496
497            mAccountSpinnerCountView.setVisibility(View.VISIBLE);
498            mAccountSpinnerCountView.setText(UiUtilities.getMessageCountForUi(
499                    mContext, mCursor.getMailboxMessageCount(), true));
500        }
501
502        boolean spinnerEnabled =
503            ((mTitleMode & TITLE_MODE_SPINNER_ENABLED) != 0) && mCursor.shouldEnableSpinner();
504
505
506        setSpinnerEnabled(spinnerEnabled);
507    }
508
509    private void setSpinnerEnabled(boolean enabled) {
510        if (enabled == mAccountSpinner.isEnabled()) {
511            return;
512        }
513
514        mAccountSpinner.setEnabled(enabled);
515        mAccountSpinner.setClickable(enabled);
516        mActionBarCustomView.setClickable(!enabled);
517        if (enabled) {
518            mAccountSpinner.setBackgroundDrawable(mAccountSpinnerDefaultBackground);
519        } else {
520            mAccountSpinner.setBackgroundDrawable(null);
521        }
522
523        // For some reason, changing the background mucks with the padding so we have to manually
524        // reset vertical padding here (also specified in XML, but it seems to be ignored for
525        // some reason.
526        mAccountSpinner.setPadding(
527                mAccountSpinner.getPaddingLeft(),
528                0,
529                mAccountSpinner.getPaddingRight(),
530                0);
531    }
532
533
534    private final SearchView.OnQueryTextListener mOnQueryText
535            = new SearchView.OnQueryTextListener() {
536        @Override
537        public boolean onQueryTextChange(String newText) {
538            // Event not handled.  Let the search do the default action.
539            return false;
540        }
541
542        @Override
543        public boolean onQueryTextSubmit(String query) {
544            mCallback.onSearchSubmit(mSearchView.getQuery().toString());
545            return true; // Event handled.
546        }
547    };
548
549    private void onAccountSpinnerItemClicked(int position) {
550        if (mAccountsSelectorAdapter == null) { // just in case...
551            return;
552        }
553        final long accountId = mAccountsSelectorAdapter.getAccountId(position);
554
555        if (mAccountsSelectorAdapter.isAccountItem(position)) {
556            mCallback.onAccountSelected(accountId);
557        } else if (mAccountsSelectorAdapter.isMailboxItem(position)) {
558            mCallback.onMailboxSelected(accountId,
559                    mAccountsSelectorAdapter.getId(position));
560        }
561    }
562
563    // Based on Spinner.DropdownPopup
564    private class AccountDropdownPopup extends ListPopupWindow {
565        public AccountDropdownPopup(Context context) {
566            super(context);
567            setAnchorView(mAccountSpinner);
568            setModal(true);
569            setPromptPosition(POSITION_PROMPT_ABOVE);
570            setOnItemClickListener(new OnItemClickListener() {
571                public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
572                    onAccountSpinnerItemClicked(position);
573                    dismiss();
574                }
575            });
576        }
577
578        @Override
579        public void show() {
580            setWidth(mContext.getResources().getDimensionPixelSize(
581                    R.dimen.account_dropdown_dropdownwidth));
582            setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
583            super.show();
584            // List view is instantiated in super.show(), so we need to do this after...
585            getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
586        }
587    }
588}
589