| /******************************************************************************* |
| * 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.mail.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); |
| } |
| } |
| } |