com.android.mail.ui.OnePaneController.java Source code

Java tutorial

Introduction

Here is the source code for com.android.mail.ui.OnePaneController.java

Source

/*******************************************************************************
 *      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
    }

}