ActionBarController.java revision 80d3875d306c60da83e547c573427627911f8a99
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.ContentUris;
23import android.content.Context;
24import android.content.Loader;
25import android.database.Cursor;
26import android.os.Bundle;
27import android.text.TextUtils;
28import android.util.Log;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.widget.SearchView;
32import android.widget.TextView;
33
34import com.android.email.FolderProperties;
35import com.android.email.R;
36import com.android.email.data.ThrottlingCursorLoader;
37import com.android.emailcommon.Logging;
38import com.android.emailcommon.provider.Account;
39import com.android.emailcommon.provider.EmailContent;
40import com.android.emailcommon.provider.EmailContent.MailboxColumns;
41import com.android.emailcommon.provider.Mailbox;
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    private static final int LOADER_ID_MAILBOX
72            = EmailActivity.ACTION_BAR_CONTROLLER_LOADER_ID_BASE + 1;
73
74    private final Context mContext;
75    private final LoaderManager mLoaderManager;
76    private final ActionBar mActionBar;
77
78    private final View mActionBarCustomView;
79    private final View mMailboxNameContainer;
80    private final TextView mMailboxNameView;
81    private final TextView mUnreadCountView;
82    private final View mSearchContainer;
83    private final SearchView mSearchView;
84
85    private final ActionBarNavigationCallback mActionBarNavigationCallback =
86        new ActionBarNavigationCallback();
87
88    private final AccountSelectorAdapter mAccountsSelectorAdapter;
89    private AccountSelectorAdapter.CursorWithExtras mAccountCursor;
90    /** The current account ID; used to determine if the account has changed. */
91    private long mLastAccountIdForDirtyCheck = Account.NO_ACCOUNT;
92
93    /** The current mailbox ID; used to determine if the mailbox has changed. */
94    private long mLastMailboxIdForDirtyCheck = Mailbox.NO_MAILBOX;
95
96    /** Either {@link #MODE_NORMAL} or {@link #MODE_SEARCH}. */
97    private int mSearchMode = MODE_NORMAL;
98
99    public final Callback mCallback;
100
101    public interface SearchContext {
102        public long getTargetMailboxId();
103    }
104
105    public interface Callback {
106        /** @return true if an account is selected. */
107        public boolean isAccountSelected();
108
109        /**
110         * @return currently selected account ID, {@link Account#ACCOUNT_ID_COMBINED_VIEW},
111         * or -1 if no account is selected.
112         */
113        public long getUIAccountId();
114
115        /**
116         * @return currently selected mailbox ID, or {@link Mailbox#NO_MAILBOX} if no mailbox is
117         * selected.
118         */
119        public long getMailboxId();
120
121        /** @return true if the current mailbox name should be shown.  */
122        public boolean shouldShowMailboxName();
123
124        /** @return the "UP" arrow should be shown. */
125        public boolean shouldShowUp();
126
127        /**
128         * Called when an account is selected on the account spinner.
129         * @param accountId ID of the selected account, or {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
130         */
131        public void onAccountSelected(long accountId);
132
133        /**
134         * Invoked when a recent mailbox is selected on the account spinner.
135         * @param mailboxId The ID of the selected mailbox, or {@link Mailbox#NO_MAILBOX} if the
136         *          special option "show all mailboxes" was selected.
137         */
138        public void onMailboxSelected(long mailboxId);
139
140        /** Called when no accounts are found in the database. */
141        public void onNoAccountsFound();
142
143        /**
144         * Called when a search is submitted.
145         *
146         * @param queryTerm query string
147         */
148        public void onSearchSubmit(String queryTerm);
149
150        /**
151         * Called when the search box is closed.
152         */
153        public void onSearchExit();
154    }
155
156    public ActionBarController(Context context, LoaderManager loaderManager,
157            ActionBar actionBar, Callback callback) {
158        mContext = context;
159        mLoaderManager = loaderManager;
160        mActionBar = actionBar;
161        mCallback = callback;
162        mAccountsSelectorAdapter = new AccountSelectorAdapter(mContext);
163
164        mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE
165                | ActionBar.DISPLAY_SHOW_HOME
166                | ActionBar.DISPLAY_SHOW_CUSTOM);
167
168        // Prepare the custom view
169        final LayoutInflater inflater = LayoutInflater.from(mContext);
170        mActionBarCustomView = inflater.inflate(R.layout.action_bar_custom_view, null);
171        final ActionBar.LayoutParams customViewLayout = new ActionBar.LayoutParams(
172                ActionBar.LayoutParams.MATCH_PARENT,
173                ActionBar.LayoutParams.MATCH_PARENT);
174        customViewLayout.setMargins(0 , 0, 0, 0);
175        mActionBar.setCustomView(mActionBarCustomView, customViewLayout);
176
177        // Mailbox name / unread count
178        mMailboxNameContainer = UiUtilities.getView(mActionBarCustomView,
179                R.id.current_mailbox_container);
180        mMailboxNameView = UiUtilities.getView(mMailboxNameContainer, R.id.mailbox_name);
181        mUnreadCountView = UiUtilities.getView(mMailboxNameContainer, R.id.unread_count);
182
183        // Search
184        mSearchContainer = UiUtilities.getView(mActionBarCustomView, R.id.search_container);
185        mSearchView = UiUtilities.getView(mSearchContainer, R.id.search_view);
186        mSearchView.setSubmitButtonEnabled(true);
187        mSearchView.setOnQueryTextListener(mOnQueryText);
188    }
189
190    /** Must be called from {@link UIControllerBase#onActivityCreated()} */
191    public void onActivityCreated() {
192        loadAccounts();
193        refresh();
194    }
195
196    /** Must be called from {@link UIControllerBase#onActivityDestroy()} */
197    public void onActivityDestroy() {
198    }
199
200    /** Must be called from {@link UIControllerBase#onSaveInstanceState} */
201    public void onSaveInstanceState(Bundle outState) {
202        outState.putInt(BUNDLE_KEY_MODE, mSearchMode);
203    }
204
205    /** Must be called from {@link UIControllerBase#onRestoreInstanceState} */
206    public void onRestoreInstanceState(Bundle savedState) {
207        int mode = savedState.getInt(BUNDLE_KEY_MODE);
208        if (mode == MODE_SEARCH) {
209            // No need to re-set the initial query, as the View tree restoration does that
210            enterSearchMode(null);
211        }
212    }
213
214    /**
215     * @return true if the search box is shown.
216     */
217    private boolean isInSearchMode() {
218        return mSearchMode == MODE_SEARCH;
219    }
220
221    /**
222     * Show the search box.
223     *
224     * @param initialQueryTerm if non-empty, set to the search box.
225     */
226    public void enterSearchMode(String initialQueryTerm) {
227        if (isInSearchMode()) {
228            return;
229        }
230        if (!TextUtils.isEmpty(initialQueryTerm)) {
231            mSearchView.setQuery(initialQueryTerm, false);
232        } else {
233            mSearchView.setQuery("", false);
234        }
235        mSearchMode = MODE_SEARCH;
236
237        // Need to force it to mode "standard" to hide it.
238        mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
239        mActionBar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);
240        mSearchContainer.setVisibility(View.VISIBLE);
241
242        // Focus on the search input box and throw up the IME if specified.
243        // TODO: HACK. this is a workaround IME not popping up.
244        mSearchView.setIconified(false);
245
246        refresh();
247    }
248
249    public void exitSearchMode() {
250        if (!isInSearchMode()) {
251            return;
252        }
253        mSearchMode = MODE_NORMAL;
254        mSearchContainer.setVisibility(View.GONE);
255        mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE, ActionBar.DISPLAY_SHOW_TITLE);
256
257        // Force update of account list when we exit search.
258        updateAccountList();
259
260        refresh();
261        mCallback.onSearchExit();
262    }
263
264    /**
265     * Performs the back action.
266     *
267     * @param isSystemBackKey <code>true</code> if the system back key was pressed.
268     * <code>false</code> if it's caused by the "home" icon click on the action bar.
269     */
270    public boolean onBackPressed(boolean isSystemBackKey) {
271        if (isInSearchMode()) {
272            exitSearchMode();
273            return true;
274        }
275        return false;
276    }
277
278    /** Refreshes the action bar display. */
279    public void refresh() {
280        final boolean showUp = isInSearchMode() || mCallback.shouldShowUp();
281        mActionBar.setDisplayOptions(showUp
282                ? ActionBar.DISPLAY_HOME_AS_UP : 0, ActionBar.DISPLAY_HOME_AS_UP);
283
284        if (isInSearchMode()) {
285            mMailboxNameContainer.setVisibility(View.GONE);
286        } else {
287            mMailboxNameContainer.setVisibility(mCallback.shouldShowMailboxName()
288                    ? View.VISIBLE : View.GONE);
289        }
290
291        // Update the account list only when the account has changed.
292        if (mLastAccountIdForDirtyCheck != mCallback.getUIAccountId()) {
293            mLastAccountIdForDirtyCheck = mCallback.getUIAccountId();
294            // If the selected account changes, reload the cursor to update the recent mailboxes
295            if (mLastAccountIdForDirtyCheck != Account.NO_ACCOUNT) {
296                mLoaderManager.destroyLoader(LOADER_ID_ACCOUNT_LIST);
297                loadAccounts();
298            } else {
299                updateAccountList();
300            }
301        }
302
303        // Update current mailbox info
304        final long mailboxId = mCallback.getMailboxId();
305        if (mailboxId == Mailbox.NO_MAILBOX) {
306            clearMailboxInfo();
307        } else {
308            if (mLastMailboxIdForDirtyCheck != mailboxId) {
309                mLastMailboxIdForDirtyCheck = mailboxId;
310                loadMailboxInfo(mailboxId);
311            }
312        }
313    }
314
315    /**
316     * Load account cursor, and update the action bar.
317     */
318    private void loadAccounts() {
319        mLoaderManager.initLoader(LOADER_ID_ACCOUNT_LIST, null,
320                new LoaderCallbacks<Cursor>() {
321            @Override
322            public Loader<Cursor> onCreateLoader(int id, Bundle args) {
323                return AccountSelectorAdapter.createLoader(mContext, mCallback.getUIAccountId());
324            }
325
326            @Override
327            public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
328                mAccountCursor = (AccountSelectorAdapter.CursorWithExtras) data;
329                updateAccountList();
330            }
331
332            @Override
333            public void onLoaderReset(Loader<Cursor> loader) {
334                mAccountCursor = null;
335                updateAccountList();
336            }
337        });
338    }
339
340    /**
341     * Called when the LOADER_ID_ACCOUNT_LIST loader loads the data.  Update the account spinner
342     * on the action bar.
343     */
344    private void updateAccountList() {
345        mAccountsSelectorAdapter.swapCursor(mAccountCursor);
346
347        if (mSearchMode == MODE_SEARCH) {
348            // In search mode, so we don't care about the account list - it'll get updated when
349            // it goes visible again.
350            return;
351        }
352
353        final ActionBar ab = mActionBar;
354        if (mAccountCursor == null) {
355            // Cursor not ready or closed.
356            ab.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);
357            ab.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
358            return;
359        }
360
361        final int count = mAccountCursor.getAccountCount() + mAccountCursor.getRecentMailboxCount();
362        if (count == 0) {
363            mCallback.onNoAccountsFound();
364            return;
365        }
366
367        // If only one account, don't show the drop down.
368        int selectedPosition = mAccountCursor.getPosition(mCallback.getUIAccountId());
369        if (count == 1) {
370            // Show the account name as the title.
371            ab.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE, ActionBar.DISPLAY_SHOW_TITLE);
372            ab.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
373            if (selectedPosition >= 0) {
374                mAccountCursor.moveToPosition(selectedPosition);
375                ab.setTitle(AccountSelectorAdapter.getDisplayName(mAccountCursor));
376            }
377            return;
378        }
379
380        // Update the drop down list.
381        if (ab.getNavigationMode() != ActionBar.NAVIGATION_MODE_LIST) {
382            ab.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);
383            ab.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
384            ab.setListNavigationCallbacks(mAccountsSelectorAdapter, mActionBarNavigationCallback);
385        }
386        // Find the currently selected account, and select it.
387        if (selectedPosition >= 0) {
388            ab.setSelectedNavigationItem(selectedPosition);
389        }
390    }
391
392    private class ActionBarNavigationCallback implements ActionBar.OnNavigationListener {
393        @Override
394        public boolean onNavigationItemSelected(int itemPosition, long itemId) {
395            if (mAccountsSelectorAdapter.isAccountItem(itemPosition)
396                    && itemId != mCallback.getUIAccountId()) {
397                mCallback.onAccountSelected(itemId);
398            } else if (mAccountsSelectorAdapter.isMailboxItem(itemPosition)) {
399                mCallback.onMailboxSelected(itemId);
400                // We need to update the selection, otherwise the user is unable to select the
401                // recent folder a second time w/o first selecting another item in the spinner
402                int selectedPosition = mAccountsSelectorAdapter.getAccountPosition(itemPosition);
403                if (selectedPosition != AccountSelectorAdapter.UNKNOWN_POSITION) {
404                    mActionBar.setSelectedNavigationItem(selectedPosition);
405                }
406            } else {
407                Log.i(Logging.LOG_TAG,
408                        "Invalid type selected in ActionBarController at index " + itemPosition);
409            }
410            return true;
411        }
412    }
413
414    private static final String[] MAILBOX_NAME_COUNT_PROJECTION = new String[] {
415        MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE,
416        MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT
417    };
418
419    private void loadMailboxInfo(final long mailboxId) {
420        clearMailboxInfo();
421        mLoaderManager.restartLoader(LOADER_ID_MAILBOX, null,
422                new LoaderCallbacks<Cursor>() {
423            @Override
424            public Loader<Cursor> onCreateLoader(int id, Bundle args) {
425                return new ThrottlingCursorLoader(mContext,
426                        ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
427                        MAILBOX_NAME_COUNT_PROJECTION, null, null, null);
428            }
429
430            @Override
431            public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
432                // Update action bar
433                FolderProperties fp = FolderProperties.getInstance(mContext);
434                updateMailboxInfo(
435                        fp.getDisplayName(cursor),
436                        fp.getMessageCount(cursor)
437                        );
438            }
439
440            @Override
441            public void onLoaderReset(Loader<Cursor> loader) {
442            }
443        });
444    }
445
446    private void clearMailboxInfo() {
447        updateMailboxInfo("", 0);
448    }
449
450    private void updateMailboxInfo(String mailboxName, int count) {
451        mMailboxNameView.setText(mailboxName);
452        mUnreadCountView.setText(UiUtilities.getMessageCountForUi(mContext, count, true));
453    }
454
455    private final SearchView.OnQueryTextListener mOnQueryText
456            = new SearchView.OnQueryTextListener() {
457        @Override
458        public boolean onQueryTextChange(String newText) {
459            // Event not handled.  Let the search do the default action.
460            return false;
461        }
462
463        @Override
464        public boolean onQueryTextSubmit(String query) {
465            mCallback.onSearchSubmit(mSearchView.getQuery().toString());
466            return true; // Event handled.
467        }
468    };
469
470}
471