Home | History | Annotate | Download | only in activity
      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 {
    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. */
    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          */
    139         public long getUIAccountId();
    140 
    141         /**
    142          * @return currently selected mailbox ID, or {@link Mailbox#NO_MAILBOX} if no mailbox is
    143          * selected.
    144          */
    145         public long getMailboxId();
    146 
    147         /**
    148          * @return constants such as {@link #TITLE_MODE_ACCOUNT_NAME_ONLY}.
    149          */
    150         public int getTitleMode();
    151 
    152         /** @see #TITLE_MODE_MESSAGE_SUBJECT */
    153         public String getMessageSubject();
    154 
    155         /** @return the "UP" arrow should be shown. */
    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          */
    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          */
    171         public void onMailboxSelected(long accountId, long mailboxId);
    172 
    173         /** Called when no accounts are found in the database. */
    174         public void onNoAccountsFound();
    175 
    176         /**
    177          * Retrieves the hint text to be shown for when a search entry is being made.
    178          */
    179         public String getSearchHint();
    180 
    181         /**
    182          * Called when the action bar initially shows the search entry field.
    183          */
    184         public void onSearchStarted();
    185 
    186         /**
    187          * Called when a search is submitted.
    188          *
    189          * @param queryTerm query string
    190          */
    191         public void onSearchSubmit(String queryTerm);
    192 
    193         /**
    194          * Called when the search box is closed.
    195          */
    196         public void onSearchExit();
    197     }
    198 
    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 
    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()} */
    254     public void onActivityCreated() {
    255         refresh();
    256     }
    257 
    258     /** Must be called from {@link UIControllerBase#onActivityDestroy()} */
    259     public void onActivityDestroy() {
    260         if (mAccountDropdown.isShowing()) {
    261             mAccountDropdown.dismiss();
    262         }
    263     }
    264 
    265     /** Must be called from {@link UIControllerBase#onSaveInstanceState} */
    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} */
    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      */
    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      */
    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      */
    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 
    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      */
    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. */
    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     };
    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      */
    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      */
    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 
    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 
    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 {
    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
    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