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