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