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

Java tutorial

Introduction

Here is the source code for com.android.mail.ui.TwoPaneController.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.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.widget.DrawerLayout;
import android.view.Gravity;
import android.widget.FrameLayout;
import android.widget.ListView;

import com.android.mail.ConversationListContext;
import com.android.exchange.R;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.providers.UIProvider.ConversationListIcon;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;

/**
 * Controller for two-pane Mail activity. Two Pane is used for tablets, where screen real estate
 * abounds.
 */
public final class TwoPaneController extends AbstractActivityController {

    private static final String SAVED_MISCELLANEOUS_VIEW = "saved-miscellaneous-view";
    private static final String SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID = "saved-miscellaneous-view-transaction-id";

    private TwoPaneLayout mLayout;
    private Conversation mConversationToShow;

    /**
     * Used to determine whether onViewModeChanged should skip a potential
     * fragment transaction that would remove a miscellaneous view.
     */
    private boolean mSavedMiscellaneousView = false;

    public TwoPaneController(MailActivity activity, ViewMode viewMode) {
        super(activity, viewMode);
    }

    /**
     * Display the conversation list fragment.
     */
    private void initializeConversationListFragment() {
        if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) {
            if (shouldEnterSearchConvMode()) {
                mViewMode.enterSearchResultsConversationMode();
            } else {
                mViewMode.enterSearchResultsListMode();
            }
        }
        renderConversationList();
    }

    /**
     * Render the conversation list in the correct pane.
     */
    private void renderConversationList() {
        if (mActivity == null) {
            return;
        }
        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
        // Use cross fading animation.
        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
        final Fragment conversationListFragment = ConversationListFragment.newInstance(mConvListContext);
        fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment, TAG_CONVERSATION_LIST);
        fragmentTransaction.commitAllowingStateLoss();
    }

    @Override
    public boolean doesActionChangeConversationListVisibility(final int action) {
        if (action == R.id.settings || action == R.id.compose || action == R.id.help_info_menu_item
                || action == R.id.manage_folders_item || action == R.id.folder_options
                || action == R.id.feedback_menu_item) {
            return true;
        }

        return false;
    }

    @Override
    protected boolean isConversationListVisible() {
        return !mLayout.isConversationListCollapsed();
    }

    @Override
    public void showConversationList(ConversationListContext listContext) {
        super.showConversationList(listContext);
        initializeConversationListFragment();
    }

    @Override
    public boolean onCreate(Bundle savedState) {
        mActivity.setContentView(R.layout.two_pane_activity);
        mDrawerContainer = (DrawerLayout) mActivity.findViewById(R.id.drawer_container);
        mDrawerPullout = mDrawerContainer.findViewById(R.id.content_pane);
        mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity);
        if (mLayout == null) {
            // We need the layout for everything. Crash/Return early if it is null.
            LogUtils.wtf(LOG_TAG, "mLayout is null!");
            return false;
        }
        mLayout.setController(this, Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()));
        mLayout.setDrawerLayout(mDrawerContainer);

        if (savedState != null) {
            mSavedMiscellaneousView = savedState.getBoolean(SAVED_MISCELLANEOUS_VIEW, false);
            mMiscellaneousViewTransactionId = savedState.getInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, -1);
        }

        // 2-pane layout is the main listener of view mode changes, and issues secondary
        // notifications upon animation completion:
        // (onConversationVisibilityChanged, onConversationListVisibilityChanged)
        mViewMode.addListener(mLayout);
        return super.onCreate(savedState);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        outState.putBoolean(SAVED_MISCELLANEOUS_VIEW, mMiscellaneousViewTransactionId >= 0);
        outState.putInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, mMiscellaneousViewTransactionId);
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        if (hasFocus && !mLayout.isConversationListCollapsed()) {
            // The conversation list is visible.
            informCursorVisiblity(true);
        }
    }

    @Override
    public void onFolderSelected(Folder folder) {
        // It's possible that we are not in conversation list mode
        if (mViewMode.getMode() != ViewMode.CONVERSATION_LIST) {
            mViewMode.enterConversationListMode();
        }

        if (folder.parent != Uri.EMPTY) {
            // Show the up affordance when digging into child folders.
            mActionBarView.setBackButton();
        }
        setHierarchyFolder(folder);
        super.onFolderSelected(folder);
    }

    @Override
    public void onViewModeChanged(int newMode) {
        if (!mSavedMiscellaneousView && mMiscellaneousViewTransactionId >= 0) {
            final FragmentManager fragmentManager = mActivity.getFragmentManager();
            fragmentManager.popBackStackImmediate(mMiscellaneousViewTransactionId,
                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
            mMiscellaneousViewTransactionId = -1;
        }
        mSavedMiscellaneousView = false;

        super.onViewModeChanged(newMode);
        if (newMode != ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
            // Clear the wait fragment
            hideWaitForInitialization();
        }
        // In conversation mode, if the conversation list is not visible, then the user cannot
        // see the selected conversations. Disable the CAB mode while leaving the selected set
        // untouched.
        // When the conversation list is made visible again, try to enable the CAB
        // mode if any conversations are selected.
        if (newMode == ViewMode.CONVERSATION || newMode == ViewMode.CONVERSATION_LIST
                || ViewMode.isAdMode(newMode)) {
            enableOrDisableCab();
        }
    }

    @Override
    public void onConversationVisibilityChanged(boolean visible) {
        super.onConversationVisibilityChanged(visible);
        if (!visible) {
            mPagerController.hide(false /* changeVisibility */);
        } else if (mConversationToShow != null) {
            mPagerController.show(mAccount, mFolder, mConversationToShow, false /* changeVisibility */);
            mConversationToShow = null;
        }
    }

    @Override
    public void onConversationListVisibilityChanged(boolean visible) {
        super.onConversationListVisibilityChanged(visible);
        enableOrDisableCab();
    }

    @Override
    public void resetActionBarIcon() {
        if (isDrawerEnabled()) {
            return;
        }
        // On two-pane, the back button is only removed in the conversation list mode for top level
        // folders, and shown for every other condition.
        if ((mViewMode.isListMode() && (mFolder == null || mFolder.parent == null || mFolder.parent == Uri.EMPTY))
                || mViewMode.isWaitingForSync()) {
            mActionBarView.removeBackButton();
        } else {
            mActionBarView.setBackButton();
        }
    }

    /**
     * Enable or disable the CAB mode based on the visibility of the conversation list fragment.
     */
    private void enableOrDisableCab() {
        if (mLayout.isConversationListCollapsed()) {
            disableCabMode();
        } else {
            enableCabMode();
        }
    }

    @Override
    public void onSetPopulated(ConversationSelectionSet set) {
        super.onSetPopulated(set);

        boolean showSenderImage = (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
        if (!showSenderImage && mViewMode.isListMode()) {
            getConversationListFragment().setChoiceNone();
        }
    }

    @Override
    public void onSetEmpty() {
        super.onSetEmpty();

        boolean showSenderImage = (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
        if (!showSenderImage && mViewMode.isListMode()) {
            getConversationListFragment().revertChoiceMode();
        }
    }

    @Override
    protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
        super.showConversation(conversation, inLoaderCallbacks);

        // 2-pane can ignore inLoaderCallbacks because it doesn't use
        // FragmentManager.popBackStack().

        if (mActivity == null) {
            return;
        }
        if (conversation == null) {
            handleBackPress();
            return;
        }
        // If conversation list is not visible, then the user cannot see the CAB mode, so exit it.
        // This is needed here (in addition to during viewmode changes) because orientation changes
        // while viewing a conversation don't change the viewmode: the mode stays
        // ViewMode.CONVERSATION and yet the conversation list goes in and out of visibility.
        enableOrDisableCab();

        // When a mode change is required, wait for onConversationVisibilityChanged(), the signal
        // that the mode change animation has finished, before rendering the conversation.
        mConversationToShow = conversation;

        final int mode = mViewMode.getMode();
        LogUtils.i(LOG_TAG, "IN TPC.showConv, oldMode=%s conv=%s", mode, mConversationToShow);
        if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
            mViewMode.enterSearchResultsConversationMode();
        } else {
            mViewMode.enterConversationMode();
        }
        // load the conversation immediately if we're already in conversation mode
        if (!mLayout.isModeChangePending()) {
            onConversationVisibilityChanged(true);
        } else {
            LogUtils.i(LOG_TAG, "TPC.showConversation will wait for TPL.animationEnd to show!");
        }
    }

    @Override
    public void setCurrentConversation(Conversation conversation) {
        // Order is important! We want to calculate different *before* the superclass changes
        // mCurrentConversation, so before super.setCurrentConversation().
        final long oldId = mCurrentConversation != null ? mCurrentConversation.id : -1;
        final long newId = conversation != null ? conversation.id : -1;
        final boolean different = oldId != newId;

        // This call might change mCurrentConversation.
        super.setCurrentConversation(conversation);

        final ConversationListFragment convList = getConversationListFragment();
        if (convList != null && conversation != null) {
            convList.setSelected(conversation.position, different);
        }
    }

    @Override
    public void showWaitForInitialization() {
        super.showWaitForInitialization();

        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
        fragmentTransaction.replace(R.id.conversation_list_pane, getWaitFragment(), TAG_WAIT);
        fragmentTransaction.commitAllowingStateLoss();
    }

    @Override
    protected void hideWaitForInitialization() {
        final WaitFragment waitFragment = getWaitFragment();
        if (waitFragment == null) {
            // We aren't showing a wait fragment: nothing to do
            return;
        }
        // Remove the existing wait fragment from the back stack.
        final FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
        fragmentTransaction.remove(waitFragment);
        fragmentTransaction.commitAllowingStateLoss();
        super.hideWaitForInitialization();
        if (mViewMode.isWaitingForSync()) {
            // We should come out of wait mode and display the account inbox.
            loadAccountInbox();
        }
    }

    /**
     * Up works as follows:
     * 1) If the user is in a conversation and:
     *  a) the conversation list is hidden (portrait mode), shows the conv list and
     *  stays in conversation view mode.
     *  b) the conversation list is shown, goes back to conversation list mode.
     * 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 conversation list mode, there is no up.
     */
    @Override
    public boolean handleUpPress() {
        int mode = mViewMode.getMode();
        if (mode == ViewMode.CONVERSATION || mViewMode.isAdMode()) {
            handleBackPress();
        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
            if (mLayout.isConversationListCollapsed() || (ConversationListContext.isSearchResult(mConvListContext)
                    && !Utils.showTwoPaneSearchResults(mActivity.getApplicationContext()))) {
                handleBackPress();
            } else {
                mActivity.finish();
            }
        } else if (mode == ViewMode.SEARCH_RESULTS_LIST) {
            mActivity.finish();
        } else if (mode == ViewMode.CONVERSATION_LIST || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
            final boolean isTopLevel = (mFolder == null) || (mFolder.parent == Uri.EMPTY);

            if (isTopLevel) {
                // Show the drawer
                toggleDrawerState();
            } else {
                popView(true);
            }
        }
        return true;
    }

    @Override
    public boolean handleBackPress() {
        // Clear any visible undo bars.
        mToastBar.hide(false, false /* actionClicked */);
        popView(false);
        return true;
    }

    /**
     * Pops the "view stack" to the last screen the user was viewing.
     *
     * @param preventClose Whether to prevent closing the app if the stack is empty.
     */
    protected void popView(boolean preventClose) {
        // If the user is in search query entry mode, or the user is viewing
        // search results, exit
        // the mode.
        int mode = mViewMode.getMode();
        if (mode == ViewMode.SEARCH_RESULTS_LIST) {
            mActivity.finish();
        } else if (mode == ViewMode.CONVERSATION || mViewMode.isAdMode()) {
            // Go to conversation list.
            mViewMode.enterConversationListMode();
        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
            mViewMode.enterSearchResultsListMode();
        } else {
            // The Folder List fragment can be null for monkeys where we get a back before the
            // folder list has had a chance to initialize.
            final FolderListFragment folderList = getFolderListFragment();
            if (mode == ViewMode.CONVERSATION_LIST && folderList != null && mFolder != null
                    && mFolder.parent != Uri.EMPTY) {
                // If the user navigated via the left folders list into a child folder,
                // back should take the user up to the parent folder's conversation list.
                navigateUpFolderHierarchy();
                // Otherwise, if we are in the conversation list but not in the default
                // inbox and not on expansive layouts, we want to switch back to the default
                // inbox. This fixes b/9006969 so that on smaller tablets where we have this
                // hybrid one and two-pane mode, we will return to the inbox. On larger tablets,
                // we will instead exit the app.
            } else {
                // Don't think mLayout could be null but checking just in case
                if (mLayout == null) {
                    LogUtils.wtf(LOG_TAG, new Throwable(), "mLayout is null");
                }
                // mFolder could be null if back is pressed while account is waiting for sync
                final boolean shouldLoadInbox = mode == ViewMode.CONVERSATION_LIST && mFolder != null
                        && !mFolder.folderUri.equals(mAccount.settings.defaultInbox) && mLayout != null
                        && !mLayout.isExpansiveLayout();
                if (shouldLoadInbox) {
                    loadAccountInbox();
                } else if (!preventClose) {
                    // There is nothing else to pop off the stack.
                    mActivity.finish();
                }
            }
        }
    }

    @Override
    public void exitSearchMode() {
        final int mode = mViewMode.getMode();
        if (mode == ViewMode.SEARCH_RESULTS_LIST || (mode == ViewMode.SEARCH_RESULTS_CONVERSATION
                && Utils.showTwoPaneSearchResults(mActivity.getApplicationContext()))) {
            mActivity.finish();
        }
    }

    @Override
    public boolean shouldShowFirstConversation() {
        return Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()) && shouldEnterSearchConvMode();
    }

    @Override
    public void onUndoAvailable(ToastBarOperation op) {
        final int mode = mViewMode.getMode();
        final ConversationListFragment convList = getConversationListFragment();

        repositionToastBar(op);

        switch (mode) {
        case ViewMode.SEARCH_RESULTS_LIST:
        case ViewMode.CONVERSATION_LIST:
        case ViewMode.SEARCH_RESULTS_CONVERSATION:
        case ViewMode.CONVERSATION:
            if (convList != null) {
                mToastBar.show(getUndoClickedListener(convList.getAnimatedAdapter()), 0,
                        Utils.convertHtmlToPlainText(op.getDescription(mActivity.getActivityContext())),
                        true, /* showActionIcon */
                        R.string.undo, true, /* replaceVisibleToast */
                        op);
            }
        }
    }

    public void repositionToastBar(ToastBarOperation op) {
        repositionToastBar(op.isBatchUndo());
    }

    /**
     * Set the toast bar's layout params to position it in the right place
     * depending the current view mode.
     *
     * @param convModeShowInList if we're in conversation mode, should the toast
     *            bar appear over the list? no effect when not in conversation mode.
     */
    private void repositionToastBar(boolean convModeShowInList) {
        final int mode = mViewMode.getMode();
        final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mToastBar.getLayoutParams();
        switch (mode) {
        case ViewMode.SEARCH_RESULTS_LIST:
        case ViewMode.CONVERSATION_LIST:
            params.width = mLayout.computeConversationListWidth() - params.leftMargin - params.rightMargin;
            params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
            mToastBar.setLayoutParams(params);
            break;
        case ViewMode.SEARCH_RESULTS_CONVERSATION:
        case ViewMode.CONVERSATION:
            if (convModeShowInList && !mLayout.isConversationListCollapsed()) {
                // Show undo bar in the conversation list.
                params.gravity = Gravity.BOTTOM | Gravity.LEFT;
                params.width = mLayout.computeConversationListWidth() - params.leftMargin - params.rightMargin;
                mToastBar.setLayoutParams(params);
            } else {
                // Show undo bar in the conversation.
                params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
                params.width = mLayout.computeConversationWidth() - params.leftMargin - params.rightMargin;
                mToastBar.setLayoutParams(params);
            }
            break;
        }
    }

    @Override
    protected void hideOrRepositionToastBar(final boolean animated) {
        final int oldViewMode = mViewMode.getMode();
        mLayout.postDelayed(new Runnable() {
            @Override
            public void run() {
                if (/* the touch did not open a conversation */oldViewMode == mViewMode.getMode() ||
                /* animation has ended */!mToastBar.isAnimating()) {
                    mToastBar.hide(animated, false /* actionClicked */);
                } else {
                    // the touch opened a conversation, reposition undo bar
                    repositionToastBar(mToastBar.getOperation());
                }
            }
        },
                /* Give time for ViewMode to change from the touch */
                mContext.getResources().getInteger(R.integer.dismiss_undo_bar_delay_ms));
    }

    @Override
    public void onError(final Folder folder, boolean replaceVisibleToast) {
        repositionToastBar(true /* convModeShowInList */);
        showErrorToast(folder, replaceVisibleToast);
    }

    @Override
    public boolean isDrawerEnabled() {
        return mLayout.isDrawerEnabled();
    }

    @Override
    public int getFolderListViewChoiceMode() {
        // By default, we want to allow one item to be selected in the folder list
        return ListView.CHOICE_MODE_SINGLE;
    }

    private int mMiscellaneousViewTransactionId = -1;

    @Override
    public void launchFragment(final Fragment fragment, final int selectPosition) {
        final int containerViewId = TwoPaneLayout.MISCELLANEOUS_VIEW_ID;

        final FragmentManager fragmentManager = mActivity.getFragmentManager();
        if (fragmentManager.findFragmentByTag(TAG_CUSTOM_FRAGMENT) == null) {
            final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            fragmentTransaction.addToBackStack(null);
            fragmentTransaction.replace(containerViewId, fragment, TAG_CUSTOM_FRAGMENT);
            mMiscellaneousViewTransactionId = fragmentTransaction.commitAllowingStateLoss();
            fragmentManager.executePendingTransactions();
        }

        if (selectPosition >= 0) {
            getConversationListFragment().setRawSelected(selectPosition, true);
        }
    }
}