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