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