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