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