Home | History | Annotate | Download | only in ui
      1 /*******************************************************************************
      2  *      Copyright (C) 2012 Google Inc.
      3  *      Licensed to The Android Open Source Project.
      4  *
      5  *      Licensed under the Apache License, Version 2.0 (the "License");
      6  *      you may not use this file except in compliance with the License.
      7  *      You may obtain a copy of the License at
      8  *
      9  *           http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  *      Unless required by applicable law or agreed to in writing, software
     12  *      distributed under the License is distributed on an "AS IS" BASIS,
     13  *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  *      See the License for the specific language governing permissions and
     15  *      limitations under the License.
     16  *******************************************************************************/
     17 
     18 package com.android.mail.ui;
     19 
     20 import android.app.Fragment;
     21 import android.app.FragmentManager;
     22 import android.app.FragmentTransaction;
     23 import android.content.Intent;
     24 import android.net.Uri;
     25 import android.os.Bundle;
     26 import android.support.v4.widget.DrawerLayout;
     27 import android.view.Gravity;
     28 import android.widget.FrameLayout;
     29 import android.widget.ListView;
     30 
     31 import com.android.mail.ConversationListContext;
     32 import com.android.mail.R;
     33 import com.android.mail.providers.Conversation;
     34 import com.android.mail.providers.Folder;
     35 import com.android.mail.providers.UIProvider.ConversationListIcon;
     36 import com.android.mail.utils.LogUtils;
     37 import com.android.mail.utils.Utils;
     38 
     39 /**
     40  * Controller for two-pane Mail activity. Two Pane is used for tablets, where screen real estate
     41  * abounds.
     42  */
     43 public final class TwoPaneController extends AbstractActivityController {
     44 
     45     private static final String SAVED_MISCELLANEOUS_VIEW = "saved-miscellaneous-view";
     46     private static final String SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID =
     47             "saved-miscellaneous-view-transaction-id";
     48 
     49     private TwoPaneLayout mLayout;
     50     private Conversation mConversationToShow;
     51 
     52     /**
     53      * Used to determine whether onViewModeChanged should skip a potential
     54      * fragment transaction that would remove a miscellaneous view.
     55      */
     56     private boolean mSavedMiscellaneousView = false;
     57 
     58     public TwoPaneController(MailActivity activity, ViewMode viewMode) {
     59         super(activity, viewMode);
     60     }
     61 
     62     /**
     63      * Display the conversation list fragment.
     64      */
     65     private void initializeConversationListFragment() {
     66         if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) {
     67             if (shouldEnterSearchConvMode()) {
     68                 mViewMode.enterSearchResultsConversationMode();
     69             } else {
     70                 mViewMode.enterSearchResultsListMode();
     71             }
     72         }
     73         renderConversationList();
     74     }
     75 
     76     /**
     77      * Render the conversation list in the correct pane.
     78      */
     79     private void renderConversationList() {
     80         if (mActivity == null) {
     81             return;
     82         }
     83         FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
     84         // Use cross fading animation.
     85         fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
     86         final Fragment conversationListFragment =
     87                 ConversationListFragment.newInstance(mConvListContext);
     88         fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment,
     89                 TAG_CONVERSATION_LIST);
     90         fragmentTransaction.commitAllowingStateLoss();
     91     }
     92 
     93     @Override
     94     public boolean doesActionChangeConversationListVisibility(final int action) {
     95         if (action == R.id.settings
     96                 || action == R.id.compose
     97                 || action == R.id.help_info_menu_item
     98                 || action == R.id.manage_folders_item
     99                 || action == R.id.folder_options
    100                 || action == R.id.feedback_menu_item) {
    101             return true;
    102         }
    103 
    104         return false;
    105     }
    106 
    107     @Override
    108     protected boolean isConversationListVisible() {
    109         return !mLayout.isConversationListCollapsed();
    110     }
    111 
    112     @Override
    113     public void showConversationList(ConversationListContext listContext) {
    114         super.showConversationList(listContext);
    115         initializeConversationListFragment();
    116     }
    117 
    118     @Override
    119     public boolean onCreate(Bundle savedState) {
    120         mActivity.setContentView(R.layout.two_pane_activity);
    121         mDrawerContainer = (DrawerLayout) mActivity.findViewById(R.id.drawer_container);
    122         mDrawerPullout = mDrawerContainer.findViewById(R.id.content_pane);
    123         mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity);
    124         if (mLayout == null) {
    125             // We need the layout for everything. Crash/Return early if it is null.
    126             LogUtils.wtf(LOG_TAG, "mLayout is null!");
    127             return false;
    128         }
    129         mLayout.setController(this, Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()));
    130         mLayout.setDrawerLayout(mDrawerContainer);
    131 
    132         if (savedState != null) {
    133             mSavedMiscellaneousView = savedState.getBoolean(SAVED_MISCELLANEOUS_VIEW, false);
    134             mMiscellaneousViewTransactionId =
    135                     savedState.getInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, -1);
    136         }
    137 
    138         // 2-pane layout is the main listener of view mode changes, and issues secondary
    139         // notifications upon animation completion:
    140         // (onConversationVisibilityChanged, onConversationListVisibilityChanged)
    141         mViewMode.addListener(mLayout);
    142         return super.onCreate(savedState);
    143     }
    144 
    145     @Override
    146     public void onSaveInstanceState(Bundle outState) {
    147         super.onSaveInstanceState(outState);
    148 
    149         outState.putBoolean(SAVED_MISCELLANEOUS_VIEW, mMiscellaneousViewTransactionId >= 0);
    150         outState.putInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, mMiscellaneousViewTransactionId);
    151     }
    152 
    153     @Override
    154     public void onWindowFocusChanged(boolean hasFocus) {
    155         if (hasFocus && !mLayout.isConversationListCollapsed()) {
    156             // The conversation list is visible.
    157             informCursorVisiblity(true);
    158         }
    159     }
    160 
    161     @Override
    162     public void onFolderSelected(Folder folder) {
    163         // It's possible that we are not in conversation list mode
    164         if (mViewMode.getMode() != ViewMode.CONVERSATION_LIST) {
    165             mViewMode.enterConversationListMode();
    166         }
    167 
    168         if (folder.parent != Uri.EMPTY) {
    169             // Show the up affordance when digging into child folders.
    170             mActionBarView.setBackButton();
    171         }
    172         setHierarchyFolder(folder);
    173         super.onFolderSelected(folder);
    174     }
    175 
    176     @Override
    177     public void onViewModeChanged(int newMode) {
    178         if (!mSavedMiscellaneousView && mMiscellaneousViewTransactionId >= 0) {
    179             final FragmentManager fragmentManager = mActivity.getFragmentManager();
    180             fragmentManager.popBackStackImmediate(mMiscellaneousViewTransactionId,
    181                     FragmentManager.POP_BACK_STACK_INCLUSIVE);
    182             mMiscellaneousViewTransactionId = -1;
    183         }
    184         mSavedMiscellaneousView = false;
    185 
    186         super.onViewModeChanged(newMode);
    187         if (newMode != ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
    188             // Clear the wait fragment
    189             hideWaitForInitialization();
    190         }
    191         // In conversation mode, if the conversation list is not visible, then the user cannot
    192         // see the selected conversations. Disable the CAB mode while leaving the selected set
    193         // untouched.
    194         // When the conversation list is made visible again, try to enable the CAB
    195         // mode if any conversations are selected.
    196         if (newMode == ViewMode.CONVERSATION || newMode == ViewMode.CONVERSATION_LIST
    197                 || ViewMode.isAdMode(newMode)) {
    198             enableOrDisableCab();
    199         }
    200     }
    201 
    202     @Override
    203     public void onConversationVisibilityChanged(boolean visible) {
    204         super.onConversationVisibilityChanged(visible);
    205         if (!visible) {
    206             mPagerController.hide(false /* changeVisibility */);
    207         } else if (mConversationToShow != null) {
    208             mPagerController.show(mAccount, mFolder, mConversationToShow,
    209                     false /* changeVisibility */);
    210             mConversationToShow = null;
    211         }
    212     }
    213 
    214     @Override
    215     public void onConversationListVisibilityChanged(boolean visible) {
    216         super.onConversationListVisibilityChanged(visible);
    217         enableOrDisableCab();
    218     }
    219 
    220     @Override
    221     public void resetActionBarIcon() {
    222         if (isDrawerEnabled()) {
    223             return;
    224         }
    225         // On two-pane, the back button is only removed in the conversation list mode for top level
    226         // folders, and shown for every other condition.
    227         if ((mViewMode.isListMode() && (mFolder == null || mFolder.parent == null
    228                 || mFolder.parent == Uri.EMPTY)) || mViewMode.isWaitingForSync()) {
    229             mActionBarView.removeBackButton();
    230         } else {
    231             mActionBarView.setBackButton();
    232         }
    233     }
    234 
    235     /**
    236      * Enable or disable the CAB mode based on the visibility of the conversation list fragment.
    237      */
    238     private void enableOrDisableCab() {
    239         if (mLayout.isConversationListCollapsed()) {
    240             disableCabMode();
    241         } else {
    242             enableCabMode();
    243         }
    244     }
    245 
    246     @Override
    247     public void onSetPopulated(ConversationSelectionSet set) {
    248         super.onSetPopulated(set);
    249 
    250         boolean showSenderImage =
    251                 (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
    252         if (!showSenderImage && mViewMode.isListMode()) {
    253             getConversationListFragment().setChoiceNone();
    254         }
    255     }
    256 
    257     @Override
    258     public void onSetEmpty() {
    259         super.onSetEmpty();
    260 
    261         boolean showSenderImage =
    262                 (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
    263         if (!showSenderImage && mViewMode.isListMode()) {
    264             getConversationListFragment().revertChoiceMode();
    265         }
    266     }
    267 
    268     @Override
    269     protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
    270         super.showConversation(conversation, inLoaderCallbacks);
    271 
    272         // 2-pane can ignore inLoaderCallbacks because it doesn't use
    273         // FragmentManager.popBackStack().
    274 
    275         if (mActivity == null) {
    276             return;
    277         }
    278         if (conversation == null) {
    279             handleBackPress();
    280             return;
    281         }
    282         // If conversation list is not visible, then the user cannot see the CAB mode, so exit it.
    283         // This is needed here (in addition to during viewmode changes) because orientation changes
    284         // while viewing a conversation don't change the viewmode: the mode stays
    285         // ViewMode.CONVERSATION and yet the conversation list goes in and out of visibility.
    286         enableOrDisableCab();
    287 
    288         // When a mode change is required, wait for onConversationVisibilityChanged(), the signal
    289         // that the mode change animation has finished, before rendering the conversation.
    290         mConversationToShow = conversation;
    291 
    292         final int mode = mViewMode.getMode();
    293         LogUtils.i(LOG_TAG, "IN TPC.showConv, oldMode=%s conv=%s", mode, mConversationToShow);
    294         if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
    295             mViewMode.enterSearchResultsConversationMode();
    296         } else {
    297             mViewMode.enterConversationMode();
    298         }
    299         // load the conversation immediately if we're already in conversation mode
    300         if (!mLayout.isModeChangePending()) {
    301             onConversationVisibilityChanged(true);
    302         } else {
    303             LogUtils.i(LOG_TAG, "TPC.showConversation will wait for TPL.animationEnd to show!");
    304         }
    305     }
    306 
    307     @Override
    308     public void setCurrentConversation(Conversation conversation) {
    309         // Order is important! We want to calculate different *before* the superclass changes
    310         // mCurrentConversation, so before super.setCurrentConversation().
    311         final long oldId = mCurrentConversation != null ? mCurrentConversation.id : -1;
    312         final long newId = conversation != null ? conversation.id : -1;
    313         final boolean different = oldId != newId;
    314 
    315         // This call might change mCurrentConversation.
    316         super.setCurrentConversation(conversation);
    317 
    318         final ConversationListFragment convList = getConversationListFragment();
    319         if (convList != null && conversation != null) {
    320             convList.setSelected(conversation.position, different);
    321         }
    322     }
    323 
    324     @Override
    325     public void showWaitForInitialization() {
    326         super.showWaitForInitialization();
    327 
    328         FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
    329         fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
    330         fragmentTransaction.replace(R.id.conversation_list_pane, getWaitFragment(), TAG_WAIT);
    331         fragmentTransaction.commitAllowingStateLoss();
    332     }
    333 
    334     @Override
    335     protected void hideWaitForInitialization() {
    336         final WaitFragment waitFragment = getWaitFragment();
    337         if (waitFragment == null) {
    338             // We aren't showing a wait fragment: nothing to do
    339             return;
    340         }
    341         // Remove the existing wait fragment from the back stack.
    342         final FragmentTransaction fragmentTransaction =
    343                 mActivity.getFragmentManager().beginTransaction();
    344         fragmentTransaction.remove(waitFragment);
    345         fragmentTransaction.commitAllowingStateLoss();
    346         super.hideWaitForInitialization();
    347         if (mViewMode.isWaitingForSync()) {
    348             // We should come out of wait mode and display the account inbox.
    349             loadAccountInbox();
    350         }
    351     }
    352 
    353     /**
    354      * Up works as follows:
    355      * 1) If the user is in a conversation and:
    356      *  a) the conversation list is hidden (portrait mode), shows the conv list and
    357      *  stays in conversation view mode.
    358      *  b) the conversation list is shown, goes back to conversation list mode.
    359      * 2) If the user is in search results, up exits search.
    360      * mode and returns the user to whatever view they were in when they began search.
    361      * 3) If the user is in conversation list mode, there is no up.
    362      */
    363     @Override
    364     public boolean handleUpPress() {
    365         int mode = mViewMode.getMode();
    366         if (mode == ViewMode.CONVERSATION || mViewMode.isAdMode()) {
    367             handleBackPress();
    368         } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
    369             if (mLayout.isConversationListCollapsed()
    370                     || (ConversationListContext.isSearchResult(mConvListContext) && !Utils.
    371                             showTwoPaneSearchResults(mActivity.getApplicationContext()))) {
    372                 handleBackPress();
    373             } else {
    374                 mActivity.finish();
    375             }
    376         } else if (mode == ViewMode.SEARCH_RESULTS_LIST) {
    377             mActivity.finish();
    378         } else if (mode == ViewMode.CONVERSATION_LIST
    379                 || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
    380             final boolean isTopLevel = (mFolder == null) || (mFolder.parent == Uri.EMPTY);
    381 
    382             if (isTopLevel) {
    383                 // Show the drawer
    384                 toggleDrawerState();
    385             } else {
    386                 popView(true);
    387             }
    388         }
    389         return true;
    390     }
    391 
    392     @Override
    393     public boolean handleBackPress() {
    394         // Clear any visible undo bars.
    395         mToastBar.hide(false, false /* actionClicked */);
    396         popView(false);
    397         return true;
    398     }
    399 
    400     /**
    401      * Pops the "view stack" to the last screen the user was viewing.
    402      *
    403      * @param preventClose Whether to prevent closing the app if the stack is empty.
    404      */
    405     protected void popView(boolean preventClose) {
    406         // If the user is in search query entry mode, or the user is viewing
    407         // search results, exit
    408         // the mode.
    409         int mode = mViewMode.getMode();
    410         if (mode == ViewMode.SEARCH_RESULTS_LIST) {
    411             mActivity.finish();
    412         } else if (mode == ViewMode.CONVERSATION || mViewMode.isAdMode()) {
    413             // Go to conversation list.
    414             mViewMode.enterConversationListMode();
    415         } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
    416             mViewMode.enterSearchResultsListMode();
    417         } else {
    418             // The Folder List fragment can be null for monkeys where we get a back before the
    419             // folder list has had a chance to initialize.
    420             final FolderListFragment folderList = getFolderListFragment();
    421             if (mode == ViewMode.CONVERSATION_LIST && folderList != null
    422                     && mFolder != null && mFolder.parent != Uri.EMPTY) {
    423                 // If the user navigated via the left folders list into a child folder,
    424                 // back should take the user up to the parent folder's conversation list.
    425                 navigateUpFolderHierarchy();
    426             // Otherwise, if we are in the conversation list but not in the default
    427             // inbox and not on expansive layouts, we want to switch back to the default
    428             // inbox. This fixes b/9006969 so that on smaller tablets where we have this
    429             // hybrid one and two-pane mode, we will return to the inbox. On larger tablets,
    430             // we will instead exit the app.
    431             } else {
    432                 // Don't think mLayout could be null but checking just in case
    433                 if (mLayout == null) {
    434                     LogUtils.wtf(LOG_TAG, new Throwable(), "mLayout is null");
    435                 }
    436                 // mFolder could be null if back is pressed while account is waiting for sync
    437                 final boolean shouldLoadInbox = mode == ViewMode.CONVERSATION_LIST &&
    438                         mFolder != null &&
    439                         !mFolder.folderUri.equals(mAccount.settings.defaultInbox) &&
    440                         mLayout != null && !mLayout.isExpansiveLayout();
    441                 if (shouldLoadInbox) {
    442                     loadAccountInbox();
    443                 } else if (!preventClose) {
    444                     // There is nothing else to pop off the stack.
    445                     mActivity.finish();
    446                 }
    447             }
    448         }
    449     }
    450 
    451     @Override
    452     public void exitSearchMode() {
    453         final int mode = mViewMode.getMode();
    454         if (mode == ViewMode.SEARCH_RESULTS_LIST
    455                 || (mode == ViewMode.SEARCH_RESULTS_CONVERSATION
    456                         && Utils.showTwoPaneSearchResults(mActivity.getApplicationContext()))) {
    457             mActivity.finish();
    458         }
    459     }
    460 
    461     @Override
    462     public boolean shouldShowFirstConversation() {
    463         return Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
    464                 && shouldEnterSearchConvMode();
    465     }
    466 
    467     @Override
    468     public void onUndoAvailable(ToastBarOperation op) {
    469         final int mode = mViewMode.getMode();
    470         final ConversationListFragment convList = getConversationListFragment();
    471 
    472         repositionToastBar(op);
    473 
    474         switch (mode) {
    475             case ViewMode.SEARCH_RESULTS_LIST:
    476             case ViewMode.CONVERSATION_LIST:
    477             case ViewMode.SEARCH_RESULTS_CONVERSATION:
    478             case ViewMode.CONVERSATION:
    479                 if (convList != null) {
    480                     mToastBar.show(getUndoClickedListener(convList.getAnimatedAdapter()),
    481                             0,
    482                             Utils.convertHtmlToPlainText
    483                                 (op.getDescription(mActivity.getActivityContext())),
    484                             true, /* showActionIcon */
    485                             R.string.undo,
    486                             true,  /* replaceVisibleToast */
    487                             op);
    488                 }
    489         }
    490     }
    491 
    492     public void repositionToastBar(ToastBarOperation op) {
    493         repositionToastBar(op.isBatchUndo());
    494     }
    495 
    496     /**
    497      * Set the toast bar's layout params to position it in the right place
    498      * depending the current view mode.
    499      *
    500      * @param convModeShowInList if we're in conversation mode, should the toast
    501      *            bar appear over the list? no effect when not in conversation mode.
    502      */
    503     private void repositionToastBar(boolean convModeShowInList) {
    504         final int mode = mViewMode.getMode();
    505         final FrameLayout.LayoutParams params =
    506                 (FrameLayout.LayoutParams) mToastBar.getLayoutParams();
    507         switch (mode) {
    508             case ViewMode.SEARCH_RESULTS_LIST:
    509             case ViewMode.CONVERSATION_LIST:
    510                 params.width = mLayout.computeConversationListWidth() - params.leftMargin
    511                         - params.rightMargin;
    512                 params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
    513                 mToastBar.setLayoutParams(params);
    514                 break;
    515             case ViewMode.SEARCH_RESULTS_CONVERSATION:
    516             case ViewMode.CONVERSATION:
    517                 if (convModeShowInList && !mLayout.isConversationListCollapsed()) {
    518                     // Show undo bar in the conversation list.
    519                     params.gravity = Gravity.BOTTOM | Gravity.LEFT;
    520                     params.width = mLayout.computeConversationListWidth() - params.leftMargin
    521                             - params.rightMargin;
    522                     mToastBar.setLayoutParams(params);
    523                 } else {
    524                     // Show undo bar in the conversation.
    525                     params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
    526                     params.width = mLayout.computeConversationWidth() - params.leftMargin
    527                             - params.rightMargin;
    528                     mToastBar.setLayoutParams(params);
    529                 }
    530                 break;
    531         }
    532     }
    533 
    534     @Override
    535     protected void hideOrRepositionToastBar(final boolean animated) {
    536         final int oldViewMode = mViewMode.getMode();
    537         mLayout.postDelayed(new Runnable() {
    538                 @Override
    539             public void run() {
    540                 if (/* the touch did not open a conversation */oldViewMode == mViewMode.getMode() ||
    541                 /* animation has ended */!mToastBar.isAnimating()) {
    542                     mToastBar.hide(animated, false /* actionClicked */);
    543                 } else {
    544                     // the touch opened a conversation, reposition undo bar
    545                     repositionToastBar(mToastBar.getOperation());
    546                 }
    547             }
    548         },
    549         /* Give time for ViewMode to change from the touch */
    550         mContext.getResources().getInteger(R.integer.dismiss_undo_bar_delay_ms));
    551     }
    552 
    553     @Override
    554     public void onError(final Folder folder, boolean replaceVisibleToast) {
    555         repositionToastBar(true /* convModeShowInList */);
    556         showErrorToast(folder, replaceVisibleToast);
    557     }
    558 
    559     @Override
    560     public boolean isDrawerEnabled() {
    561         return mLayout.isDrawerEnabled();
    562     }
    563 
    564     @Override
    565     public int getFolderListViewChoiceMode() {
    566         // By default, we want to allow one item to be selected in the folder list
    567         return ListView.CHOICE_MODE_SINGLE;
    568     }
    569 
    570     private int mMiscellaneousViewTransactionId = -1;
    571 
    572     @Override
    573     public void launchFragment(final Fragment fragment, final int selectPosition) {
    574         final int containerViewId = TwoPaneLayout.MISCELLANEOUS_VIEW_ID;
    575 
    576         final FragmentManager fragmentManager = mActivity.getFragmentManager();
    577         if (fragmentManager.findFragmentByTag(TAG_CUSTOM_FRAGMENT) == null) {
    578             final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    579             fragmentTransaction.addToBackStack(null);
    580             fragmentTransaction.replace(containerViewId, fragment, TAG_CUSTOM_FRAGMENT);
    581             mMiscellaneousViewTransactionId = fragmentTransaction.commitAllowingStateLoss();
    582             fragmentManager.executePendingTransactions();
    583         }
    584 
    585         if (selectPosition >= 0) {
    586             getConversationListFragment().setRawSelected(selectPosition, true);
    587         }
    588     }
    589 }
    590