blob: ef42ab9b0cebb73078202000792f4696c31446a2 [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.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.LayoutRes;
import android.support.v4.widget.DrawerLayout;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.widget.ListView;
import com.android.mail.ConversationListContext;
import com.android.mail.R;
import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.providers.UIProvider;
import com.android.mail.utils.FolderUri;
import com.android.mail.utils.Utils;
/**
* Controller for one-pane Mail activity. One Pane is used for phones, where screen real estate is
* limited. This controller also does the layout, since the layout is simpler in the one pane case.
*/
public final class OnePaneController extends AbstractActivityController {
/** Key used to store {@link #mLastConversationListTransactionId} */
private static final String CONVERSATION_LIST_TRANSACTION_KEY = "conversation-list-transaction";
/** Key used to store {@link #mLastConversationTransactionId}. */
private static final String CONVERSATION_TRANSACTION_KEY = "conversation-transaction";
/** Key used to store {@link #mConversationListVisible}. */
private static final String CONVERSATION_LIST_VISIBLE_KEY = "conversation-list-visible";
/** Key used to store {@link #mConversationListNeverShown}. */
private static final String CONVERSATION_LIST_NEVER_SHOWN_KEY = "conversation-list-never-shown";
private static final int INVALID_ID = -1;
private boolean mConversationListVisible = false;
private int mLastConversationListTransactionId = INVALID_ID;
private int mLastConversationTransactionId = INVALID_ID;
/** Whether a conversation list for this account has ever been shown.*/
private boolean mConversationListNeverShown = true;
/**
* Listener for pager animation to complete and then remove the TL fragment.
* This is a work-around for fragment remove animation not working as intended, so we
* still get feedback on conversation item tap in the transition from TL to CV.
*/
private final AnimatorListenerAdapter mPagerAnimationListener =
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Make sure that while we were animating, the mode did not change back
// If it's still in conversation view mode, remove the TL fragment from behind
if (mViewMode.isConversationMode()) {
// Once the pager is done animating in, we are ready to remove the
// conversation list fragment. Since we track the fragment by either what's
// in content_pane or by the tag, we grab it and remove without animations
// since it's already covered by the conversation view and its white bg.
final FragmentManager fm = mActivity.getFragmentManager();
final FragmentTransaction ft = fm.beginTransaction();
final Fragment f = fm.findFragmentById(R.id.content_pane);
// FragmentManager#findFragmentById can return fragments that are not
// added to the activity. We want to make sure that we don't attempt to
// remove fragments that are not added to the activity, as when the
// transaction is popped off, the FragmentManager will attempt to read
// the same fragment twice.
if (f != null && f.isAdded()) {
ft.remove(f);
ft.commitAllowingStateLoss();
fm.executePendingTransactions();
}
}
}
};
public OnePaneController(MailActivity activity, ViewMode viewMode) {
super(activity, viewMode);
}
@Override
public void onRestoreInstanceState(Bundle inState) {
super.onRestoreInstanceState(inState);
if (inState == null) {
return;
}
mLastConversationListTransactionId =
inState.getInt(CONVERSATION_LIST_TRANSACTION_KEY, INVALID_ID);
mLastConversationTransactionId = inState.getInt(CONVERSATION_TRANSACTION_KEY, INVALID_ID);
mConversationListVisible = inState.getBoolean(CONVERSATION_LIST_VISIBLE_KEY);
mConversationListNeverShown = inState.getBoolean(CONVERSATION_LIST_NEVER_SHOWN_KEY);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(CONVERSATION_LIST_TRANSACTION_KEY, mLastConversationListTransactionId);
outState.putInt(CONVERSATION_TRANSACTION_KEY, mLastConversationTransactionId);
outState.putBoolean(CONVERSATION_LIST_VISIBLE_KEY, mConversationListVisible);
outState.putBoolean(CONVERSATION_LIST_NEVER_SHOWN_KEY, mConversationListNeverShown);
}
@Override
public void resetActionBarIcon() {
// Calling resetActionBarIcon should never remove the up affordance
// even when waiting for sync (Folder list should still show with one
// account. Currently this method is blank to avoid any changes.
}
/**
* Returns true if the candidate URI is the URI for the default inbox for the given account.
* @param candidate the URI to check
* @param account the account whose default Inbox the candidate might be
* @return true if the candidate is indeed the default inbox for the given account.
*/
private static boolean isDefaultInbox(FolderUri candidate, Account account) {
return (candidate != null && account != null)
&& candidate.equals(account.settings.defaultInbox);
}
/**
* Returns true if the user is currently in the conversation list view, viewing the default
* inbox.
* @return true if user is in conversation list mode, viewing the default inbox.
*/
private static boolean inInbox(final Account account, final ConversationListContext context) {
// If we don't have valid state, then we are not in the inbox.
return !(account == null || context == null || context.folder == null
|| account.settings == null) && !ConversationListContext.isSearchResult(context)
&& isDefaultInbox(context.folder.folderUri, account);
}
/**
* On account change, carry out super implementation, load FolderListFragment
* into drawer (to avoid repetitive calls to replaceFragment).
*/
@Override
public void changeAccount(Account account) {
super.changeAccount(account);
mConversationListNeverShown = true;
closeDrawerIfOpen();
}
@Override
public @LayoutRes int getContentViewResource() {
return R.layout.one_pane_activity;
}
@Override
public void onCreate(Bundle savedInstanceState) {
mDrawerContainer = (DrawerLayout) mActivity.findViewById(R.id.drawer_container);
mDrawerContainer.setDrawerTitle(Gravity.START,
mActivity.getActivityContext().getString(R.string.drawer_title));
mDrawerContainer.setStatusBarBackground(R.color.primary_dark_color);
final String drawerPulloutTag = mActivity.getString(R.string.drawer_pullout_tag);
mDrawerPullout = mDrawerContainer.findViewWithTag(drawerPulloutTag);
mDrawerPullout.setBackgroundResource(R.color.list_background_color);
// CV is initially GONE on 1-pane (mode changes trigger visibility changes)
mActivity.findViewById(R.id.conversation_pager).setVisibility(View.GONE);
// The parent class sets the correct viewmode and starts the application off.
super.onCreate(savedInstanceState);
}
@Override
protected ActionableToastBar findActionableToastBar(MailActivity activity) {
final ActionableToastBar tb = super.findActionableToastBar(activity);
// notify the toast bar of its sibling floating action button so it can move them together
// as they animate
tb.setFloatingActionButton(activity.findViewById(R.id.compose_button));
return tb;
}
@Override
protected boolean isConversationListVisible() {
return mConversationListVisible;
}
@Override
public void onViewModeChanged(int newMode) {
super.onViewModeChanged(newMode);
// When entering conversation list mode, hide and clean up any currently visible
// conversation.
if (ViewMode.isListMode(newMode)) {
mPagerController.hide(true /* changeVisibility */);
}
if (ViewMode.isAdMode(newMode)) {
onConversationListVisibilityChanged(false);
}
// When we step away from the conversation mode, we don't have a current conversation
// anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
if (!ViewMode.isConversationMode(newMode)) {
setCurrentConversation(null);
}
}
@Override
protected void appendToString(StringBuilder sb) {
sb.append(" lastConvListTransId=");
sb.append(mLastConversationListTransactionId);
}
@Override
protected void showConversationList(ConversationListContext listContext) {
enableCabMode();
mConversationListVisible = true;
if (ConversationListContext.isSearchResult(listContext)) {
mViewMode.enterSearchResultsListMode();
} else {
mViewMode.enterConversationListMode();
}
final int transition = mConversationListNeverShown
? FragmentTransaction.TRANSIT_FRAGMENT_FADE
: FragmentTransaction.TRANSIT_FRAGMENT_OPEN;
final Fragment conversationListFragment =
ConversationListFragment.newInstance(listContext);
if (!inInbox(mAccount, listContext)) {
// Maintain fragment transaction history so we can get back to the
// fragment used to launch this list.
mLastConversationListTransactionId = replaceFragment(conversationListFragment,
transition, TAG_CONVERSATION_LIST, R.id.content_pane);
} else {
// If going to the inbox, clear the folder list transaction history.
mInbox = listContext.folder;
replaceFragment(conversationListFragment, transition, TAG_CONVERSATION_LIST,
R.id.content_pane);
// If we ever to to the inbox, we want to unset the transation id for any other
// non-inbox folder.
mLastConversationListTransactionId = INVALID_ID;
}
mActivity.getFragmentManager().executePendingTransactions();
onConversationVisibilityChanged(false);
onConversationListVisibilityChanged(true);
mConversationListNeverShown = false;
}
/**
* Override showConversation with animation parameter so that we animate in the pager when
* selecting in the conversation, but don't animate on opening the app from an intent.
* @param conversation
* @param shouldAnimate true if we want to animate the conversation in, false otherwise
*/
@Override
protected void showConversation(Conversation conversation, boolean shouldAnimate) {
super.showConversation(conversation, shouldAnimate);
mConversationListVisible = false;
if (conversation == null) {
transitionBackToConversationListMode();
return;
}
disableCabMode();
if (ConversationListContext.isSearchResult(mConvListContext)) {
mViewMode.enterSearchResultsConversationMode();
} else {
mViewMode.enterConversationMode();
}
mPagerController.show(mAccount, mFolder, conversation, true /* changeVisibility */,
shouldAnimate? mPagerAnimationListener : null);
onConversationVisibilityChanged(true);
onConversationListVisibilityChanged(false);
}
@Override
public void onConversationFocused(Conversation conversation) {
// Do nothing
}
@Override
protected void showWaitForInitialization() {
super.showWaitForInitialization();
replaceFragment(getWaitFragment(), FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT,
R.id.content_pane);
}
@Override
protected void hideWaitForInitialization() {
transitionToInbox();
super.hideWaitForInitialization();
}
/**
* Switch to the Inbox by creating a new conversation list context that loads the inbox.
*/
private void transitionToInbox() {
// The inbox could have changed, in which case we should load it again.
if (mInbox == null || !isDefaultInbox(mInbox.folderUri, mAccount)) {
loadAccountInbox();
} else {
onFolderChanged(mInbox, false /* force */);
}
}
@Override
public boolean doesActionChangeConversationListVisibility(final int action) {
if (action == R.id.archive
|| action == R.id.remove_folder
|| action == R.id.delete
|| action == R.id.discard_drafts
|| action == R.id.discard_outbox
|| action == R.id.mark_important
|| action == R.id.mark_not_important
|| action == R.id.mute
|| action == R.id.report_spam
|| action == R.id.mark_not_spam
|| action == R.id.report_phishing
|| action == R.id.refresh
|| action == R.id.change_folders) {
return false;
} else {
return true;
}
}
/**
* Replace the content_pane with the fragment specified here. The tag is specified so that
* the {@link ActivityController} can look up the fragments through the
* {@link android.app.FragmentManager}.
* @param fragment the new fragment to put
* @param transition the transition to show
* @param tag a tag for the fragment manager.
* @param anchor ID of view to replace fragment in
* @return transaction ID returned when the transition is committed.
*/
private int replaceFragment(Fragment fragment, int transition, String tag, int anchor) {
final FragmentManager fm = mActivity.getFragmentManager();
FragmentTransaction fragmentTransaction = fm.beginTransaction();
fragmentTransaction.setTransition(transition);
fragmentTransaction.replace(anchor, fragment, tag);
final int id = fragmentTransaction.commitAllowingStateLoss();
fm.executePendingTransactions();
return id;
}
/**
* Back works as follows:
* 1) If the drawer is pulled out (Or mid-drag), close it - handled.
* 2) If the user is in the folder list view, go back
* to the account default inbox.
* 3) If the user is in a conversation list
* that is not the inbox AND:
* a) they got there by going through the folder
* list view, go back to the folder list view.
* b) they got there by using some other means (account dropdown), go back to the inbox.
* 4) If the user is in a conversation, go back to the conversation list they were last in.
* 5) If the user is in the conversation list for the default account inbox,
* back exits the app.
*/
@Override
public boolean handleBackPress() {
final int mode = mViewMode.getMode();
if (mode == ViewMode.SEARCH_RESULTS_LIST) {
mActivity.finish();
} else if (mViewMode.isListMode() && !inInbox(mAccount, mConvListContext)) {
navigateUpFolderHierarchy();
} else if (mViewMode.isConversationMode() || mViewMode.isAdMode()) {
transitionBackToConversationListMode();
} else {
mActivity.finish();
}
mToastBar.hide(false, false /* actionClicked */);
return true;
}
@Override
public void onFolderSelected(Folder folder) {
if (mViewMode.isSearchMode()) {
// We are in an activity on top of the main navigation activity.
// We need to return to it with a result code that indicates it should navigate to
// a different folder.
final Intent intent = new Intent();
intent.putExtra(AbstractActivityController.EXTRA_FOLDER, folder);
mActivity.setResult(Activity.RESULT_OK, intent);
mActivity.finish();
return;
}
setHierarchyFolder(folder);
super.onFolderSelected(folder);
}
/**
* Up works as follows:
* 1) If the user is in a conversation list that is not the default account inbox,
* a conversation, or the folder list, up follows the rules of back.
* 2) If the user is in search results, up exits search
* mode and returns the user to whatever view they were in when they began search.
* 3) If the user is in the inbox, there is no up.
*/
@Override
public boolean handleUpPress() {
final int mode = mViewMode.getMode();
if (mode == ViewMode.SEARCH_RESULTS_LIST) {
mActivity.finish();
// Not needed, the activity is going away anyway.
} else if (mode == ViewMode.CONVERSATION_LIST
|| mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
final boolean isTopLevel = Folder.isRoot(mFolder);
if (isTopLevel) {
// Show the drawer.
toggleDrawerState();
} else {
navigateUpFolderHierarchy();
}
} else if (mode == ViewMode.CONVERSATION || mode == ViewMode.SEARCH_RESULTS_CONVERSATION
|| mode == ViewMode.AD) {
// Same as go back.
handleBackPress();
}
return true;
}
private void transitionBackToConversationListMode() {
final int mode = mViewMode.getMode();
enableCabMode();
mConversationListVisible = true;
if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
mViewMode.enterSearchResultsListMode();
} else {
mViewMode.enterConversationListMode();
}
final Folder folder = mFolder != null ? mFolder : mInbox;
onFolderChanged(folder, true /* force */);
onConversationVisibilityChanged(false);
onConversationListVisibilityChanged(true);
}
@Override
public boolean shouldShowFirstConversation() {
return false;
}
@Override
public void onUndoAvailable(ToastBarOperation op) {
if (op != null && mAccount.supportsCapability(UIProvider.AccountCapabilities.UNDO)) {
final int mode = mViewMode.getMode();
final ConversationListFragment convList = getConversationListFragment();
switch (mode) {
case ViewMode.SEARCH_RESULTS_CONVERSATION:
case ViewMode.CONVERSATION:
mToastBar.show(getUndoClickedListener(
convList != null ? convList.getAnimatedAdapter() : null),
Utils.convertHtmlToPlainText
(op.getDescription(mActivity.getActivityContext())),
R.string.undo,
true /* replaceVisibleToast */,
true /* autohide */,
op);
break;
case ViewMode.SEARCH_RESULTS_LIST:
case ViewMode.CONVERSATION_LIST:
if (convList != null) {
mToastBar.show(
getUndoClickedListener(convList.getAnimatedAdapter()),
Utils.convertHtmlToPlainText
(op.getDescription(mActivity.getActivityContext())),
R.string.undo,
true /* replaceVisibleToast */,
true /* autohide */,
op);
} else {
mActivity.setPendingToastOperation(op);
}
break;
}
}
}
@Override
public void onError(final Folder folder, boolean replaceVisibleToast) {
final int mode = mViewMode.getMode();
switch (mode) {
case ViewMode.SEARCH_RESULTS_LIST:
case ViewMode.CONVERSATION_LIST:
showErrorToast(folder, replaceVisibleToast);
break;
default:
break;
}
}
@Override
public boolean isDrawerEnabled() {
// The drawer is enabled for one pane mode
return true;
}
@Override
public int getFolderListViewChoiceMode() {
// By default, we do not want to allow any item to be selected in the folder list
return ListView.CHOICE_MODE_NONE;
}
@Override
public void launchFragment(final Fragment fragment, final int selectPosition) {
replaceFragment(fragment, FragmentTransaction.TRANSIT_FRAGMENT_OPEN,
TAG_CUSTOM_FRAGMENT, R.id.content_pane);
}
@Override
public boolean onInterceptKeyFromCV(int keyCode, KeyEvent keyEvent, boolean navigateAway) {
// Not applicable
return false;
}
@Override
public boolean isTwoPaneLandscape() {
return false;
}
@Override
public boolean shouldShowSearchBarByDefault(int viewMode) {
return viewMode == ViewMode.SEARCH_RESULTS_LIST;
}
@Override
public boolean shouldShowSearchMenuItem() {
return mViewMode.getMode() == ViewMode.CONVERSATION_LIST;
}
@Override
public void addConversationListLayoutListener(
TwoPaneLayout.ConversationListLayoutListener listener) {
// Do nothing
}
}