575 lines
21 KiB
Java
575 lines
21 KiB
Java
/*
|
|
* Copyright (C) 2011 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.email.activity;
|
|
|
|
import android.app.ActionBar;
|
|
import android.app.LoaderManager;
|
|
import android.app.LoaderManager.LoaderCallbacks;
|
|
import android.content.Context;
|
|
import android.content.Loader;
|
|
import android.database.Cursor;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.os.Bundle;
|
|
import android.text.TextUtils;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.widget.AdapterView;
|
|
import android.widget.AdapterView.OnItemClickListener;
|
|
import android.widget.ListPopupWindow;
|
|
import android.widget.ListView;
|
|
import android.widget.SearchView;
|
|
import android.widget.TextView;
|
|
|
|
import com.android.email.R;
|
|
import com.android.emailcommon.provider.Account;
|
|
import com.android.emailcommon.provider.Mailbox;
|
|
import com.android.emailcommon.utility.DelayedOperations;
|
|
import com.android.emailcommon.utility.Utility;
|
|
|
|
/**
|
|
* Manages the account name and the custom view part on the action bar.
|
|
*/
|
|
public class ActionBarController {
|
|
private static final String BUNDLE_KEY_MODE = "ActionBarController.BUNDLE_KEY_MODE";
|
|
|
|
/**
|
|
* Constants for {@link #mSearchMode}.
|
|
*
|
|
* In {@link #MODE_NORMAL} mode, we don't show the search box.
|
|
* In {@link #MODE_SEARCH} mode, we do show the search box.
|
|
* The action bar doesn't really care if the activity is showing search results.
|
|
* If the activity is showing search results, and the {@link Callback#onSearchExit} is called,
|
|
* the activity probably wants to close itself, but this class doesn't make the desision.
|
|
*/
|
|
private static final int MODE_NORMAL = 0;
|
|
private static final int MODE_SEARCH = 1;
|
|
|
|
private static final int LOADER_ID_ACCOUNT_LIST
|
|
= EmailActivity.ACTION_BAR_CONTROLLER_LOADER_ID_BASE + 0;
|
|
|
|
private final Context mContext;
|
|
private final LoaderManager mLoaderManager;
|
|
private final ActionBar mActionBar;
|
|
private final DelayedOperations mDelayedOperations;
|
|
|
|
/** "Folders" label shown with account name on 1-pane mailbox list */
|
|
private final String mAllFoldersLabel;
|
|
|
|
private final ViewGroup mActionBarCustomView;
|
|
private final ViewGroup mAccountSpinnerContainer;
|
|
private final View mAccountSpinner;
|
|
private final Drawable mAccountSpinnerDefaultBackground;
|
|
private final TextView mAccountSpinnerLine1View;
|
|
private final TextView mAccountSpinnerLine2View;
|
|
private final TextView mAccountSpinnerCountView;
|
|
|
|
private View mSearchContainer;
|
|
private SearchView mSearchView;
|
|
|
|
private final AccountDropdownPopup mAccountDropdown;
|
|
|
|
private final AccountSelectorAdapter mAccountsSelectorAdapter;
|
|
|
|
private AccountSelectorAdapter.CursorWithExtras mCursor;
|
|
|
|
/** The current account ID; used to determine if the account has changed. */
|
|
private long mLastAccountIdForDirtyCheck = Account.NO_ACCOUNT;
|
|
|
|
/** The current mailbox ID; used to determine if the mailbox has changed. */
|
|
private long mLastMailboxIdForDirtyCheck = Mailbox.NO_MAILBOX;
|
|
|
|
/** Either {@link #MODE_NORMAL} or {@link #MODE_SEARCH}. */
|
|
private int mSearchMode = MODE_NORMAL;
|
|
|
|
/** The current title mode, which should be one of {@code Callback TITLE_MODE_*} */
|
|
private int mTitleMode;
|
|
|
|
public final Callback mCallback;
|
|
|
|
public interface SearchContext {
|
|
public long getTargetMailboxId();
|
|
}
|
|
|
|
private static final int TITLE_MODE_SPINNER_ENABLED = 0x10;
|
|
|
|
public interface Callback {
|
|
/** Values for {@link #getTitleMode}. Show only account name */
|
|
public static final int TITLE_MODE_ACCOUNT_NAME_ONLY = 0 | TITLE_MODE_SPINNER_ENABLED;
|
|
|
|
/**
|
|
* Show the current account name with "Folders"
|
|
* The account spinner will be disabled in this mode.
|
|
*/
|
|
public static final int TITLE_MODE_ACCOUNT_WITH_ALL_FOLDERS_LABEL = 1;
|
|
|
|
/**
|
|
* Show the current account name and the current mailbox name.
|
|
*/
|
|
public static final int TITLE_MODE_ACCOUNT_WITH_MAILBOX = 2 | TITLE_MODE_SPINNER_ENABLED;
|
|
/**
|
|
* Show the current message subject. Actual subject is obtained via
|
|
* {@link #getMessageSubject()}.
|
|
*
|
|
* The account spinner will be disabled in this mode.
|
|
*/
|
|
public static final int TITLE_MODE_MESSAGE_SUBJECT = 3;
|
|
|
|
/** @return true if an account is selected. */
|
|
public boolean isAccountSelected();
|
|
|
|
/**
|
|
* @return currently selected account ID, {@link Account#ACCOUNT_ID_COMBINED_VIEW},
|
|
* or -1 if no account is selected.
|
|
*/
|
|
public long getUIAccountId();
|
|
|
|
/**
|
|
* @return currently selected mailbox ID, or {@link Mailbox#NO_MAILBOX} if no mailbox is
|
|
* selected.
|
|
*/
|
|
public long getMailboxId();
|
|
|
|
/**
|
|
* @return constants such as {@link #TITLE_MODE_ACCOUNT_NAME_ONLY}.
|
|
*/
|
|
public int getTitleMode();
|
|
|
|
/** @see #TITLE_MODE_MESSAGE_SUBJECT */
|
|
public String getMessageSubject();
|
|
|
|
/** @return the "UP" arrow should be shown. */
|
|
public boolean shouldShowUp();
|
|
|
|
/**
|
|
* Called when an account is selected on the account spinner.
|
|
* @param accountId ID of the selected account, or {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
|
|
*/
|
|
public void onAccountSelected(long accountId);
|
|
|
|
/**
|
|
* Invoked when a recent mailbox is selected on the account spinner.
|
|
*
|
|
* @param accountId ID of the selected account, or {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
|
|
* @param mailboxId The ID of the selected mailbox, or {@link Mailbox#NO_MAILBOX} if the
|
|
* special option "show all mailboxes" was selected.
|
|
*/
|
|
public void onMailboxSelected(long accountId, long mailboxId);
|
|
|
|
/** Called when no accounts are found in the database. */
|
|
public void onNoAccountsFound();
|
|
|
|
/**
|
|
* Retrieves the hint text to be shown for when a search entry is being made.
|
|
*/
|
|
public String getSearchHint();
|
|
|
|
/**
|
|
* Called when the action bar initially shows the search entry field.
|
|
*/
|
|
public void onSearchStarted();
|
|
|
|
/**
|
|
* Called when a search is submitted.
|
|
*
|
|
* @param queryTerm query string
|
|
*/
|
|
public void onSearchSubmit(String queryTerm);
|
|
|
|
/**
|
|
* Called when the search box is closed.
|
|
*/
|
|
public void onSearchExit();
|
|
}
|
|
|
|
public ActionBarController(Context context, LoaderManager loaderManager,
|
|
ActionBar actionBar, Callback callback) {
|
|
mContext = context;
|
|
mLoaderManager = loaderManager;
|
|
mActionBar = actionBar;
|
|
mCallback = callback;
|
|
mDelayedOperations = new DelayedOperations(Utility.getMainThreadHandler());
|
|
mAllFoldersLabel = mContext.getResources().getString(
|
|
R.string.action_bar_mailbox_list_title);
|
|
mAccountsSelectorAdapter = new AccountSelectorAdapter(mContext);
|
|
|
|
// Configure action bar.
|
|
mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_CUSTOM);
|
|
|
|
// Prepare the custom view
|
|
mActionBar.setCustomView(R.layout.action_bar_custom_view);
|
|
mActionBarCustomView = (ViewGroup) mActionBar.getCustomView();
|
|
|
|
// Account spinner
|
|
mAccountSpinnerContainer =
|
|
UiUtilities.getView(mActionBarCustomView, R.id.account_spinner_container);
|
|
mAccountSpinner = UiUtilities.getView(mActionBarCustomView, R.id.account_spinner);
|
|
mAccountSpinnerDefaultBackground = mAccountSpinner.getBackground();
|
|
|
|
mAccountSpinnerLine1View = UiUtilities.getView(mActionBarCustomView, R.id.spinner_line_1);
|
|
mAccountSpinnerLine2View = UiUtilities.getView(mActionBarCustomView, R.id.spinner_line_2);
|
|
mAccountSpinnerCountView = UiUtilities.getView(mActionBarCustomView, R.id.spinner_count);
|
|
|
|
// Account dropdown
|
|
mAccountDropdown = new AccountDropdownPopup(mContext);
|
|
mAccountDropdown.setAdapter(mAccountsSelectorAdapter);
|
|
|
|
mAccountSpinner.setOnClickListener(new View.OnClickListener() {
|
|
@Override public void onClick(View v) {
|
|
if (mAccountsSelectorAdapter.getCount() > 0) {
|
|
mAccountDropdown.show();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private void initSearchViews() {
|
|
if (mSearchContainer == null) {
|
|
final LayoutInflater inflater = LayoutInflater.from(mContext);
|
|
mSearchContainer = inflater.inflate(R.layout.action_bar_search, null);
|
|
mSearchView = UiUtilities.getView(mSearchContainer, R.id.search_view);
|
|
mSearchView.setSubmitButtonEnabled(false);
|
|
mSearchView.setOnQueryTextListener(mOnQueryText);
|
|
mSearchView.onActionViewExpanded();
|
|
mActionBarCustomView.addView(mSearchContainer);
|
|
}
|
|
}
|
|
|
|
|
|
/** Must be called from {@link UIControllerBase#onActivityCreated()} */
|
|
public void onActivityCreated() {
|
|
refresh();
|
|
}
|
|
|
|
/** Must be called from {@link UIControllerBase#onActivityDestroy()} */
|
|
public void onActivityDestroy() {
|
|
if (mAccountDropdown.isShowing()) {
|
|
mAccountDropdown.dismiss();
|
|
}
|
|
}
|
|
|
|
/** Must be called from {@link UIControllerBase#onSaveInstanceState} */
|
|
public void onSaveInstanceState(Bundle outState) {
|
|
mDelayedOperations.removeCallbacks(); // Remove all pending operations
|
|
outState.putInt(BUNDLE_KEY_MODE, mSearchMode);
|
|
}
|
|
|
|
/** Must be called from {@link UIControllerBase#onRestoreInstanceState} */
|
|
public void onRestoreInstanceState(Bundle savedState) {
|
|
int mode = savedState.getInt(BUNDLE_KEY_MODE);
|
|
if (mode == MODE_SEARCH) {
|
|
// No need to re-set the initial query, as the View tree restoration does that
|
|
enterSearchMode(null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return true if the search box is shown.
|
|
*/
|
|
public boolean isInSearchMode() {
|
|
return mSearchMode == MODE_SEARCH;
|
|
}
|
|
|
|
/**
|
|
* @return Whether or not the search bar should be shown. This is a function of whether or not a
|
|
* search is active, and if the current layout supports it.
|
|
*/
|
|
private boolean shouldShowSearchBar() {
|
|
return isInSearchMode() && (mTitleMode != Callback.TITLE_MODE_MESSAGE_SUBJECT);
|
|
}
|
|
|
|
/**
|
|
* Show the search box.
|
|
*
|
|
* @param initialQueryTerm if non-empty, set to the search box.
|
|
*/
|
|
public void enterSearchMode(String initialQueryTerm) {
|
|
initSearchViews();
|
|
if (isInSearchMode()) {
|
|
return;
|
|
}
|
|
if (!TextUtils.isEmpty(initialQueryTerm)) {
|
|
mSearchView.setQuery(initialQueryTerm, false);
|
|
} else {
|
|
mSearchView.setQuery("", false);
|
|
}
|
|
mSearchView.setQueryHint(mCallback.getSearchHint());
|
|
|
|
mSearchMode = MODE_SEARCH;
|
|
|
|
// Focus on the search input box and throw up the IME if specified.
|
|
// TODO: HACK. this is a workaround IME not popping up.
|
|
mSearchView.setIconified(false);
|
|
|
|
refresh();
|
|
mCallback.onSearchStarted();
|
|
}
|
|
|
|
public void exitSearchMode() {
|
|
if (!isInSearchMode()) {
|
|
return;
|
|
}
|
|
mSearchMode = MODE_NORMAL;
|
|
|
|
refresh();
|
|
mCallback.onSearchExit();
|
|
}
|
|
|
|
/**
|
|
* Performs the back action.
|
|
*
|
|
* @param isSystemBackKey <code>true</code> if the system back key was pressed.
|
|
* <code>false</code> if it's caused by the "home" icon click on the action bar.
|
|
*/
|
|
public boolean onBackPressed(boolean isSystemBackKey) {
|
|
if (shouldShowSearchBar()) {
|
|
exitSearchMode();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Refreshes the action bar display. */
|
|
public void refresh() {
|
|
// The actual work is in refreshInernal(), but we don't call it directly here, because:
|
|
// 1. refresh() is called very often.
|
|
// 2. to avoid nested fragment transaction.
|
|
// refresh is often called during a fragment transaction, but updateTitle() may call
|
|
// a callback which would initiate another fragment transaction.
|
|
mDelayedOperations.removeCallbacks(mRefreshRunnable);
|
|
mDelayedOperations.post(mRefreshRunnable);
|
|
}
|
|
|
|
private final Runnable mRefreshRunnable = new Runnable() {
|
|
@Override public void run() {
|
|
refreshInernal();
|
|
}
|
|
};
|
|
private void refreshInernal() {
|
|
final boolean showUp = isInSearchMode() || mCallback.shouldShowUp();
|
|
mActionBar.setDisplayOptions(showUp
|
|
? ActionBar.DISPLAY_HOME_AS_UP : 0, ActionBar.DISPLAY_HOME_AS_UP);
|
|
|
|
final long accountId = mCallback.getUIAccountId();
|
|
final long mailboxId = mCallback.getMailboxId();
|
|
if ((mLastAccountIdForDirtyCheck != accountId)
|
|
|| (mLastMailboxIdForDirtyCheck != mailboxId)) {
|
|
mLastAccountIdForDirtyCheck = accountId;
|
|
mLastMailboxIdForDirtyCheck = mailboxId;
|
|
|
|
if (accountId != Account.NO_ACCOUNT) {
|
|
loadAccountMailboxInfo(accountId, mailboxId);
|
|
}
|
|
}
|
|
|
|
updateTitle();
|
|
}
|
|
|
|
/**
|
|
* Load account/mailbox info, and account/recent mailbox list.
|
|
*/
|
|
private void loadAccountMailboxInfo(final long accountId, final long mailboxId) {
|
|
mLoaderManager.restartLoader(LOADER_ID_ACCOUNT_LIST, null,
|
|
new LoaderCallbacks<Cursor>() {
|
|
@Override
|
|
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
|
return AccountSelectorAdapter.createLoader(mContext, accountId, mailboxId);
|
|
}
|
|
|
|
@Override
|
|
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
|
mCursor = (AccountSelectorAdapter.CursorWithExtras) data;
|
|
updateTitle();
|
|
}
|
|
|
|
@Override
|
|
public void onLoaderReset(Loader<Cursor> loader) {
|
|
mCursor = null;
|
|
updateTitle();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update the "title" part.
|
|
*/
|
|
private void updateTitle() {
|
|
mAccountsSelectorAdapter.swapCursor(mCursor);
|
|
|
|
if (mCursor == null) {
|
|
// Initial load not finished.
|
|
mActionBarCustomView.setVisibility(View.GONE);
|
|
return;
|
|
}
|
|
mActionBarCustomView.setVisibility(View.VISIBLE);
|
|
|
|
if (mCursor.getAccountCount() == 0) {
|
|
mCallback.onNoAccountsFound();
|
|
return;
|
|
}
|
|
|
|
if ((mCursor.getAccountId() != Account.NO_ACCOUNT) && !mCursor.accountExists()) {
|
|
// Account specified, but does not exist.
|
|
if (isInSearchMode()) {
|
|
exitSearchMode();
|
|
}
|
|
|
|
// Switch to the default account.
|
|
mCallback.onAccountSelected(Account.getDefaultAccountId(mContext));
|
|
return;
|
|
}
|
|
|
|
mTitleMode = mCallback.getTitleMode();
|
|
|
|
if (shouldShowSearchBar()) {
|
|
initSearchViews();
|
|
// In search mode, the search box is a replacement of the account spinner, so ignore
|
|
// the work needed to update that. It will get updated when it goes visible again.
|
|
mAccountSpinnerContainer.setVisibility(View.GONE);
|
|
mSearchContainer.setVisibility(View.VISIBLE);
|
|
return;
|
|
}
|
|
|
|
// Account spinner visible.
|
|
mAccountSpinnerContainer.setVisibility(View.VISIBLE);
|
|
UiUtilities.setVisibilitySafe(mSearchContainer, View.GONE);
|
|
|
|
if (mTitleMode == Callback.TITLE_MODE_MESSAGE_SUBJECT) {
|
|
mAccountSpinnerLine1View.setSingleLine(false);
|
|
mAccountSpinnerLine1View.setMaxLines(2);
|
|
mAccountSpinnerLine1View.setText(mCallback.getMessageSubject());
|
|
mAccountSpinnerLine2View.setVisibility(View.GONE);
|
|
|
|
mAccountSpinnerCountView.setVisibility(View.GONE);
|
|
|
|
} else {
|
|
// Get mailbox name
|
|
final String mailboxName;
|
|
if (mTitleMode == Callback.TITLE_MODE_ACCOUNT_WITH_ALL_FOLDERS_LABEL) {
|
|
mailboxName = mAllFoldersLabel;
|
|
} else if (mTitleMode == Callback.TITLE_MODE_ACCOUNT_WITH_MAILBOX) {
|
|
mailboxName = mCursor.getMailboxDisplayName();
|
|
} else {
|
|
mailboxName = null;
|
|
}
|
|
|
|
// Note - setSingleLine is needed as well as setMaxLines since they set different
|
|
// flags on the view.
|
|
mAccountSpinnerLine1View.setSingleLine();
|
|
mAccountSpinnerLine1View.setMaxLines(1);
|
|
if (TextUtils.isEmpty(mailboxName)) {
|
|
mAccountSpinnerLine1View.setText(mCursor.getAccountDisplayName());
|
|
|
|
// Change the visibility of line 2, so line 1 will be vertically-centered.
|
|
mAccountSpinnerLine2View.setVisibility(View.GONE);
|
|
} else {
|
|
mAccountSpinnerLine1View.setText(mailboxName);
|
|
mAccountSpinnerLine2View.setVisibility(View.VISIBLE);
|
|
mAccountSpinnerLine2View.setText(mCursor.getAccountDisplayName());
|
|
}
|
|
|
|
mAccountSpinnerCountView.setVisibility(View.VISIBLE);
|
|
mAccountSpinnerCountView.setText(UiUtilities.getMessageCountForUi(
|
|
mContext, mCursor.getMailboxMessageCount(), true));
|
|
}
|
|
|
|
boolean spinnerEnabled =
|
|
((mTitleMode & TITLE_MODE_SPINNER_ENABLED) != 0) && mCursor.shouldEnableSpinner();
|
|
|
|
|
|
setSpinnerEnabled(spinnerEnabled);
|
|
}
|
|
|
|
private void setSpinnerEnabled(boolean enabled) {
|
|
if (enabled == mAccountSpinner.isEnabled()) {
|
|
return;
|
|
}
|
|
|
|
mAccountSpinner.setEnabled(enabled);
|
|
if (enabled) {
|
|
mAccountSpinner.setBackgroundDrawable(mAccountSpinnerDefaultBackground);
|
|
} else {
|
|
mAccountSpinner.setBackgroundDrawable(null);
|
|
}
|
|
|
|
// For some reason, changing the background mucks with the padding so we have to manually
|
|
// reset vertical padding here (also specified in XML, but it seems to be ignored for
|
|
// some reason.
|
|
mAccountSpinner.setPadding(
|
|
mAccountSpinner.getPaddingLeft(),
|
|
0,
|
|
mAccountSpinner.getPaddingRight(),
|
|
0);
|
|
}
|
|
|
|
|
|
private final SearchView.OnQueryTextListener mOnQueryText
|
|
= new SearchView.OnQueryTextListener() {
|
|
@Override
|
|
public boolean onQueryTextChange(String newText) {
|
|
// Event not handled. Let the search do the default action.
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onQueryTextSubmit(String query) {
|
|
mCallback.onSearchSubmit(mSearchView.getQuery().toString());
|
|
return true; // Event handled.
|
|
}
|
|
};
|
|
|
|
private void onAccountSpinnerItemClicked(int position) {
|
|
if (mAccountsSelectorAdapter == null) { // just in case...
|
|
return;
|
|
}
|
|
final long accountId = mAccountsSelectorAdapter.getAccountId(position);
|
|
|
|
if (mAccountsSelectorAdapter.isAccountItem(position)) {
|
|
mCallback.onAccountSelected(accountId);
|
|
} else if (mAccountsSelectorAdapter.isMailboxItem(position)) {
|
|
mCallback.onMailboxSelected(accountId,
|
|
mAccountsSelectorAdapter.getId(position));
|
|
}
|
|
}
|
|
|
|
// Based on Spinner.DropdownPopup
|
|
private class AccountDropdownPopup extends ListPopupWindow {
|
|
public AccountDropdownPopup(Context context) {
|
|
super(context);
|
|
setAnchorView(mAccountSpinner);
|
|
setModal(true);
|
|
setPromptPosition(POSITION_PROMPT_ABOVE);
|
|
setOnItemClickListener(new OnItemClickListener() {
|
|
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
|
|
onAccountSpinnerItemClicked(position);
|
|
dismiss();
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void show() {
|
|
setWidth(mContext.getResources().getDimensionPixelSize(
|
|
R.dimen.account_dropdown_dropdownwidth));
|
|
setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
|
|
super.show();
|
|
// List view is instantiated in super.show(), so we need to do this after...
|
|
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
|
|
}
|
|
}
|
|
}
|