blob: 623a2c7998ece07ad2361a3a8b775ff52d8d6b28 [file] [log] [blame]
/*
* Copyright (C) 2012 Google Inc.
* Licensed to 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.mail.ui;
import android.app.ActionBar;
import android.app.SearchManager;
import android.app.SearchableInfo;
import android.app.ActionBar.OnNavigationListener;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.SearchView;
import android.widget.SearchView.OnQueryTextListener;
import android.widget.SearchView.OnSuggestionListener;
import android.widget.TextView;
import android.widget.Toast;
import com.android.mail.R;
import com.android.mail.AccountSpinnerAdapter;
import com.android.mail.ConversationListContext;
import com.android.mail.providers.Account;
import com.android.mail.providers.Settings;
import com.android.mail.providers.UIProvider.AccountCapabilities;
import com.android.mail.providers.UIProvider.FolderCapabilities;
import com.android.mail.providers.UIProvider.LastSyncResult;
import com.android.mail.providers.Folder;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
/**
* View to manage the various states of the Mail Action Bar
*
* TODO(viki): Include ConversationSubjectDisplayer here as well.
*/
public final class ActionBarView extends LinearLayout implements OnNavigationListener,
ViewMode.ModeChangeListener, OnQueryTextListener, OnSuggestionListener,
MenuItem.OnActionExpandListener {
private ActionBar mActionBar;
private RestrictedActivity mActivity;
private ActivityController mController;
private View mFolderView;
/**
* The current mode of the ActionBar. This references constants in {@link ViewMode}
*/
private int mMode = ViewMode.UNKNOWN;
private MenuItem mSearch;
private AccountSpinnerAdapter mSpinner;
/**
* The account currently being shown
*/
private Account mAccount;
/**
* The folder currently being shown
*/
private Folder mFolder;
private TextView mSubjectView;
private SearchView mSearchWidget;
private MenuItem mHelpItem;
private MenuItem mSendFeedbackItem;
private MenuItem mRefreshItem;
private MenuItem mFolderSettingsItem;
private View mRefreshActionView;
private boolean mRefreshInProgress;
public static final String LOG_TAG = new LogUtils().getLogTag();
private final Handler mHandler = new Handler();
private final Runnable mInvalidateMenu = new Runnable() {
@Override
public void run() {
mActivity.invalidateOptionsMenu();
}
};
/**
* Whether the first navigation event should be ignored. The {@link #ignoreFirstNavigation(int)}
* method talks about why this is required.
*/
private boolean mIgnoreFirstNavigation = true;
private Boolean mShowConversationSubject;
public ActionBarView(Context context) {
this(context, null);
}
public ActionBarView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ActionBarView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* Close the search view if it is expanded.
*/
public void collapseSearch() {
if (mSearch != null) {
mSearch.collapseActionView();
}
}
public boolean onCreateOptionsMenu(Menu menu) {
// If the mode is valid, then set the initial menu
if (mMode == ViewMode.UNKNOWN) {
return false;
}
mSearch = menu.findItem(R.id.search);
if (mSearch != null) {
mSearchWidget = (SearchView) mSearch.getActionView();
mSearch.setOnActionExpandListener(this);
SearchManager searchManager = (SearchManager) mActivity.getActivityContext()
.getSystemService(Context.SEARCH_SERVICE);
if (searchManager != null && mSearchWidget != null) {
SearchableInfo info = searchManager.getSearchableInfo(mActivity.getComponentName());
mSearchWidget.setSearchableInfo(info);
mSearchWidget.setOnQueryTextListener(this);
mSearchWidget.setOnSuggestionListener(this);
mSearchWidget.setIconifiedByDefault(true);
}
}
mHelpItem = menu.findItem(R.id.help_info_menu_item);
mSendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
mRefreshItem = menu.findItem(R.id.refresh);
mFolderSettingsItem = menu.findItem(R.id.folder_options);
return true;
}
public int getOptionsMenuId() {
// Relies on the ordering of the view modes, since they are integer constants.
final int[] modeMenu = {
// 0: UNKNOWN
R.menu.conversation_list_menu,
// 1: CONVERSATION
R.menu.conversation_actions,
// 2: CONVERSATION_LIST
R.menu.conversation_list_menu,
// 3: FOLDER_LIST
R.menu.folder_list_menu,
// 4: SEARCH_RESULTS_LIST
R.menu.conversation_list_search_results_actions,
// 5: SEARCH_RESULTS_CONVERSATION
R.menu.conversation_search_results_actions,
// 6: WAITING_FOR_ACCOUNT_INITIALIZATION
R.menu.wait_mode_actions
};
return modeMenu[mMode];
}
public void handleRestore(Bundle savedInstanceState) {
}
public void handleSaveInstanceState(Bundle outState) {
}
public void initialize(RestrictedActivity activity, ActivityController callback,
ViewMode viewMode, ActionBar actionBar, RecentFolderList recentFolders) {
mActionBar = actionBar;
mController = callback;
mActivity = activity;
// We don't want to include the "Show all folders" menu item on tablet devices
final boolean showAllFolders = !Utils.useTabletUI(getContext());
mSpinner = new AccountSpinnerAdapter(getContext(), recentFolders, showAllFolders);
}
/**
* Attach the action bar to the view.
*/
public void attach() {
mActionBar.setListNavigationCallbacks(mSpinner, this);
}
public void setAccounts(Account[] accounts) {
final Account currentAccount = mController.getCurrentAccount();
mSpinner.setAccountArray(accounts);
int position;
for (position = 0; position < accounts.length; position++) {
if (accounts[position].uri.equals(currentAccount.uri)) {
break;
}
}
if (position >= accounts.length) {
position = 0;
LogUtils.w(LOG_TAG, "IN actionbarview setAccounts, account not found, using first.");
}
final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
final boolean viewingDefaultInbox =
(mFolder == null || mAccount == null || mAccount.settings == null) ? false :
mFolder.uri.equals(defaultInbox);
final boolean accountInSpinner = (position >= 0);
if (accountInSpinner && viewingDefaultInbox) {
// This position corresponds to current account and default Inbox. Select it.
setSelectedPosition(position);
} else {
// Set the selected position to a dead spacer. The user is either viewing a different
// folder, or the account is missing from the spinner.
setSelectedPosition(mSpinner.getSpacerPosition());
}
}
/**
* Sets the selected navigation position in the spinner to the position given here.
* @param position
*/
private void setSelectedPosition(int position) {
// Only change the position if we are in the correct mode.
if (mActionBar.getNavigationMode() != ActionBar.NAVIGATION_MODE_LIST) {
return;
}
LogUtils.d(LOG_TAG, "ActionBarView.setSelectedNavigationItem(%d)", position);
mActionBar.setSelectedNavigationItem(position);
}
/**
* Called by the owner of the ActionBar to set the
* folder that is currently being displayed.
*/
public void setFolder(Folder folder) {
// Change the currently selected item to an element which is a spacer: valid but not useful
// This allows us to receive a tap on the account name when the user taps on it, and we can
// take the user to the default inbox.
setSelectedPosition(mSpinner.getSpacerPosition());
mSpinner.setCurrentFolder(folder);
mSpinner.notifyDataSetChanged();
mFolder = folder;
}
/**
* Called by the owner of the ActionBar to set the
* account that is currently being displayed.
*/
public void setAccount(Account account) {
mAccount = account;
mSpinner.setCurrentAccount(account);
mSpinner.notifyDataSetChanged();
}
/**
* Returns true if this list navigation event is erroneous and should be ignored.
*
* Rationale: When a spinner is brought up for the first time, and it has never been brought up
* before, it shows the 0th element. This is fine in most cases, since the navigation mode has
* to select something. However, if we already have an account: for example if we went from the
* widget to Conversation view, and the spinner never got a chance to initialize, it needs to
* ignore this first navigation. If the spinner has ever been shown, then we will allow
* subsequent calls to onNavigationItemSelected.
* @param position the position selected in the drop down.
*/
private boolean ignoreFirstNavigation(int position) {
if (mIgnoreFirstNavigation && position == 0 && mAccount != null) {
// Ignore the first navigation item selected because it is the list initializing
// We already have an account.
LogUtils.d(LOG_TAG, "ignoreFirstNavigation: Ignoring navigation to position 0."
+ " mAccount = %s", mAccount.uri);
// All user taps are now valid: even a tap on the current account to take the user to
// the default inbox.
mIgnoreFirstNavigation = false;
setSelectedPosition(mSpinner.getSpacerPosition());
// Yes, we want to ignore this navigation. It is not a user-initiated navigation.
return true;
}
// Spinner was correctly initialized and is receiving a valid first tap. All subsequent
// taps are user events.
mIgnoreFirstNavigation = false;
// No, we don't want to ignore this navigation.
return false;
}
@Override
public boolean onNavigationItemSelected(int position, long id) {
if (ignoreFirstNavigation(position)) {
return false;
}
LogUtils.d(LOG_TAG, "onNavigationItemSelected(%d, %d) called", position, id);
final int type = mSpinner.getType(position);
switch (type) {
case AccountSpinnerAdapter.TYPE_ACCOUNT:
// Get the capabilities associated with this account.
final Account account = (Account) mSpinner.getItem(position);
LogUtils.d(LOG_TAG, "onNavigationItemSelected: Selecting account: %s",
account.name);
if (mAccount.uri.equals(account.uri)) {
// The selected account is the same, let's load the default inbox.
mController.loadAccountInbox();
} else {
// Switching accounts.
mController.onAccountChanged(account);
}
break;
case AccountSpinnerAdapter.TYPE_FOLDER:
final Object folder = mSpinner.getItem(position);
assert (folder instanceof Folder);
LogUtils.d(LOG_TAG, "onNavigationItemSelected: Selecting folder: %s",
((Folder)folder).name);
mController.onFolderChanged((Folder) folder);
break;
case AccountSpinnerAdapter.TYPE_ALL_FOLDERS:
// Change the currently selected item to an element which is a spacer: valid
// but not useful. This allows us to receive subsequent taps on the
// "show all folders" menu item.
setSelectedPosition(mSpinner.getSpacerPosition());
mController.showFolderList();
break;
}
return false;
}
public void onPause() {
}
public void onResume() {
}
@Override
public void onViewModeChanged(int newMode) {
mMode = newMode;
// Always update the options menu and redraw. This will read the new mode and redraw
// the options menu.
mActivity.invalidateOptionsMenu();
}
public boolean onPrepareOptionsMenu(Menu menu) {
// We start out with every option enabled. Based on the current view, we disable actions
// that are possible.
LogUtils.d(LOG_TAG, "ActionBarView.onPrepareOptionsMenu().");
if (mSubjectView != null){
mSubjectView.setVisibility(GONE);
}
if (mFolderView != null){
mFolderView.setVisibility(GONE);
}
if (mRefreshInProgress) {
if (mRefreshItem != null) {
if (mRefreshActionView == null) {
mRefreshItem.setActionView(R.layout.action_bar_indeterminate_progress);
mRefreshActionView = mRefreshItem.getActionView();
} else {
mRefreshItem.setActionView(mRefreshActionView);
}
}
} else {
if (mRefreshItem != null) {
mRefreshItem.setActionView(null);
}
}
if (mHelpItem != null) {
mHelpItem.setVisible(mAccount != null
&& mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
}
if (mSendFeedbackItem != null) {
mSendFeedbackItem.setVisible(mAccount != null
&& mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
}
if (mFolderSettingsItem != null) {
mFolderSettingsItem.setVisible(mFolder != null
&& mFolder.supportsCapability(FolderCapabilities.SUPPORTS_SETTINGS));
}
switch (mMode) {
case ViewMode.UNKNOWN:
if (mSearch != null) {
mSearch.collapseActionView();
}
break;
case ViewMode.CONVERSATION_LIST:
// Show compose, search, folders, and sync based on the account
// The only option that needs to be disabled is search
showNavList();
Utils.setMenuItemVisibility(menu, R.id.search,
mAccount.supportsCapability(AccountCapabilities.FOLDER_SERVER_SEARCH));
break;
case ViewMode.CONVERSATION:
mActionBar.setDisplayHomeAsUpEnabled(true);
// FIXME: use a resource to have fine-grained control over whether the spinner
// or the subject appears
if (Utils.useTabletUI(mActivity.getActivityContext())
&& !showConversationSubject()) {
showNavList();
} else {
setStandardMode();
}
break;
case ViewMode.SEARCH_RESULTS_LIST:
setStandardMode();
setPopulatedSearchView();
// Remove focus from the search action menu in search results mode so the IME and
// the suggestions don't get in the way.
if (mMode == ViewMode.SEARCH_RESULTS_LIST && mSearch != null) {
mSearchWidget = (SearchView) mSearch.getActionView();
mSearchWidget.clearFocus();
}
break;
case ViewMode.SEARCH_RESULTS_CONVERSATION:
mActionBar.setDisplayHomeAsUpEnabled(true);
setStandardMode();
if (Utils.useTabletUI(mActivity.getActivityContext())) {
setPopulatedSearchView();
}
break;
case ViewMode.FOLDER_LIST:
mActionBar.setDisplayHomeAsUpEnabled(true);
mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE,
ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM);
setStandardMode();
mActionBar.setTitle(R.string.folder_list_title);
break;
}
return false;
}
private boolean showConversationSubject() {
if (mShowConversationSubject == null) {
mShowConversationSubject = new Boolean(mActivity.getActivityContext().getResources()
.getBoolean(R.bool.show_conversation_subject));
}
return mShowConversationSubject;
}
/**
* Put the ActionBar in List navigation mode. This starts the spinner up if it is missing.
*/
private void showNavList() {
mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM,
ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM);
}
/**
* Set the actionbar mode to standard mode: no list navigation.
*/
private void setStandardMode() {
mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
}
private void setPopulatedSearchView() {
if (mSearch != null) {
mSearch.expandActionView();
ConversationListContext context = mController.getCurrentListContext();
if (context != null) {
mSearchWidget.setQuery(context.searchQuery, false);
}
}
}
public void removeBackButton() {
if (mActionBar == null) {
return;
}
mActionBar.setDisplayOptions(
ActionBar.DISPLAY_SHOW_HOME,
ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
mActivity.getActionBar().setHomeButtonEnabled(false);
}
public void setBackButton() {
if (mActionBar == null){
return;
}
mActionBar.setDisplayOptions(
ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
mActivity.getActionBar().setHomeButtonEnabled(true);
}
@Override
public boolean onQueryTextSubmit(String query) {
if (mSearch != null) {
mSearch.collapseActionView();
mSearchWidget.setQuery("", false);
}
mActivity.onSearchRequested(query);
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
// TODO Auto-generated method stub
return false;
}
public boolean setRefreshInProgress(boolean inProgress) {
if (inProgress != mRefreshInProgress) {
mRefreshInProgress = inProgress;
if (mSearch == null || !mSearch.isActionViewExpanded()) {
mHandler.post(mInvalidateMenu);
}
return true;
}
return false;
}
public void onRefreshStarted() {
setRefreshInProgress(true);
}
public void onRefreshStopped(int status) {
if (setRefreshInProgress(false)) {
switch (status) {
case LastSyncResult.SUCCESS:
break;
default:
Context context = mActivity.getActivityContext();
Toast.makeText(context, Utils.getSyncStatusText(context, status),
Toast.LENGTH_LONG).show();
break;
}
}
}
/**
* Get the query text the user entered in the search widget, or empty string
* if there is none.
*/
public String getQuery() {
return mSearchWidget != null ? mSearchWidget.getQuery().toString() : "";
}
// Next two methods are called when search suggestions are clicked.
@Override
public boolean onSuggestionSelect(int position) {
return onSuggestionClick(position);
}
@Override
public boolean onSuggestionClick(int position) {
final Cursor c = mSearchWidget.getSuggestionsAdapter().getCursor();
final boolean haveValidQuery = (c != null) && c.moveToPosition(position);
if (!haveValidQuery) {
LogUtils.d(LOG_TAG, "onSuggestionClick: Couldn't get a search query");
// We haven't handled this query, but the default behavior will leave EXTRA_ACCOUNT
// un-populated, leading to a crash. So claim that we have handled the event.
return true;
}
final String query = c.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY));
mController.onSearchRequested(query);
return true;
}
/**
* Notify that the folder has changed.
*/
public void onFolderUpdated(Folder folder) {
mSpinner.onFolderUpdated(folder);
}
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
// Do nothing. Required as part of the interface, we ar only interested in
// onMenuItemActionCollapse(MenuItem).
// Have to return true here. Unlike other callbacks, the return value here is whether
// we want to suppress the action (rather than consume the action). We don't want to
// suppress the action.
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
// When the action menu is collapsed, we have performed a search, pop the search fragment.
mController.exitSearchMode();
// Have to return true here. Unlike other callbacks, the return value here is whether
// we want to suppress the action (rather than consume the action). We don't want to
// suppress the action.
return true;
}
/**
* Request the Accounts spinner to redraw itself in light of new data that it needs to request.
*/
public void requestRecentFoldersAndRedraw() {
mSpinner.requestRecentFoldersAndRedraw();
}
}