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.animation.Animator;
     21 import android.animation.AnimatorListenerAdapter;
     22 import android.app.Activity;
     23 import android.app.Fragment;
     24 import android.app.FragmentManager;
     25 import android.app.FragmentTransaction;
     26 import android.content.Intent;
     27 import android.os.Bundle;
     28 import android.support.annotation.LayoutRes;
     29 import android.support.v4.widget.DrawerLayout;
     30 import android.view.Gravity;
     31 import android.view.KeyEvent;
     32 import android.view.View;
     33 import android.widget.ListView;
     34 
     35 import com.android.mail.ConversationListContext;
     36 import com.android.mail.R;
     37 import com.android.mail.providers.Account;
     38 import com.android.mail.providers.Conversation;
     39 import com.android.mail.providers.Folder;
     40 import com.android.mail.providers.UIProvider;
     41 import com.android.mail.utils.FolderUri;
     42 import com.android.mail.utils.Utils;
     43 
     44 /**
     45  * Controller for one-pane Mail activity. One Pane is used for phones, where screen real estate is
     46  * limited. This controller also does the layout, since the layout is simpler in the one pane case.
     47  */
     48 
     49 public final class OnePaneController extends AbstractActivityController {
     50     /** Key used to store {@link #mLastConversationListTransactionId} */
     51     private static final String CONVERSATION_LIST_TRANSACTION_KEY = "conversation-list-transaction";
     52     /** Key used to store {@link #mLastConversationTransactionId}. */
     53     private static final String CONVERSATION_TRANSACTION_KEY = "conversation-transaction";
     54     /** Key used to store {@link #mConversationListVisible}. */
     55     private static final String CONVERSATION_LIST_VISIBLE_KEY = "conversation-list-visible";
     56     /** Key used to store {@link #mConversationListNeverShown}. */
     57     private static final String CONVERSATION_LIST_NEVER_SHOWN_KEY = "conversation-list-never-shown";
     58 
     59     private static final int INVALID_ID = -1;
     60     private boolean mConversationListVisible = false;
     61     private int mLastConversationListTransactionId = INVALID_ID;
     62     private int mLastConversationTransactionId = INVALID_ID;
     63     /** Whether a conversation list for this account has ever been shown.*/
     64     private boolean mConversationListNeverShown = true;
     65 
     66     /**
     67      * Listener for pager animation to complete and then remove the TL fragment.
     68      * This is a work-around for fragment remove animation not working as intended, so we
     69      * still get feedback on conversation item tap in the transition from TL to CV.
     70      */
     71     private final AnimatorListenerAdapter mPagerAnimationListener =
     72             new AnimatorListenerAdapter() {
     73                 @Override
     74                 public void onAnimationEnd(Animator animation) {
     75                     // Make sure that while we were animating, the mode did not change back
     76                     // If it's still in conversation view mode, remove the TL fragment from behind
     77                     if (mViewMode.isConversationMode()) {
     78                         // Once the pager is done animating in, we are ready to remove the
     79                         // conversation list fragment. Since we track the fragment by either what's
     80                         // in content_pane or by the tag, we grab it and remove without animations
     81                         // since it's already covered by the conversation view and its white bg.
     82                         final FragmentManager fm = mActivity.getFragmentManager();
     83                         final FragmentTransaction ft = fm.beginTransaction();
     84                         final Fragment f = fm.findFragmentById(R.id.content_pane);
     85                         // FragmentManager#findFragmentById can return fragments that are not
     86                         // added to the activity. We want to make sure that we don't attempt to
     87                         // remove fragments that are not added to the activity, as when the
     88                         // transaction is popped off, the FragmentManager will attempt to read
     89                         // the same fragment twice.
     90                         if (f != null && f.isAdded()) {
     91                             ft.remove(f);
     92                             ft.commitAllowingStateLoss();
     93                             fm.executePendingTransactions();
     94                         }
     95                     }
     96                 }
     97             };
     98 
     99     public OnePaneController(MailActivity activity, ViewMode viewMode) {
    100         super(activity, viewMode);
    101     }
    102 
    103     @Override
    104     public void onRestoreInstanceState(Bundle inState) {
    105         super.onRestoreInstanceState(inState);
    106         if (inState == null) {
    107             return;
    108         }
    109         mLastConversationListTransactionId =
    110                 inState.getInt(CONVERSATION_LIST_TRANSACTION_KEY, INVALID_ID);
    111         mLastConversationTransactionId = inState.getInt(CONVERSATION_TRANSACTION_KEY, INVALID_ID);
    112         mConversationListVisible = inState.getBoolean(CONVERSATION_LIST_VISIBLE_KEY);
    113         mConversationListNeverShown = inState.getBoolean(CONVERSATION_LIST_NEVER_SHOWN_KEY);
    114     }
    115 
    116     @Override
    117     public void onSaveInstanceState(Bundle outState) {
    118         super.onSaveInstanceState(outState);
    119         outState.putInt(CONVERSATION_LIST_TRANSACTION_KEY, mLastConversationListTransactionId);
    120         outState.putInt(CONVERSATION_TRANSACTION_KEY, mLastConversationTransactionId);
    121         outState.putBoolean(CONVERSATION_LIST_VISIBLE_KEY, mConversationListVisible);
    122         outState.putBoolean(CONVERSATION_LIST_NEVER_SHOWN_KEY, mConversationListNeverShown);
    123     }
    124 
    125     @Override
    126     public void resetActionBarIcon() {
    127         // Calling resetActionBarIcon should never remove the up affordance
    128         // even when waiting for sync (Folder list should still show with one
    129         // account. Currently this method is blank to avoid any changes.
    130     }
    131 
    132     /**
    133      * Returns true if the candidate URI is the URI for the default inbox for the given account.
    134      * @param candidate the URI to check
    135      * @param account the account whose default Inbox the candidate might be
    136      * @return true if the candidate is indeed the default inbox for the given account.
    137      */
    138     private static boolean isDefaultInbox(FolderUri candidate, Account account) {
    139         return (candidate != null && account != null)
    140                 && candidate.equals(account.settings.defaultInbox);
    141     }
    142 
    143     /**
    144      * Returns true if the user is currently in the conversation list view, viewing the default
    145      * inbox.
    146      * @return true if user is in conversation list mode, viewing the default inbox.
    147      */
    148     private static boolean inInbox(final Account account, final ConversationListContext context) {
    149         // If we don't have valid state, then we are not in the inbox.
    150         return !(account == null || context == null || context.folder == null
    151                 || account.settings == null) && !ConversationListContext.isSearchResult(context)
    152                 && isDefaultInbox(context.folder.folderUri, account);
    153     }
    154 
    155     /**
    156      * On account change, carry out super implementation, load FolderListFragment
    157      * into drawer (to avoid repetitive calls to replaceFragment).
    158      */
    159     @Override
    160     public void changeAccount(Account account) {
    161         super.changeAccount(account);
    162         mConversationListNeverShown = true;
    163         closeDrawerIfOpen();
    164     }
    165 
    166     @Override
    167     public @LayoutRes int getContentViewResource() {
    168         return R.layout.one_pane_activity;
    169     }
    170 
    171     @Override
    172     public void onCreate(Bundle savedInstanceState) {
    173         mDrawerContainer = (DrawerLayout) mActivity.findViewById(R.id.drawer_container);
    174         mDrawerContainer.setDrawerTitle(Gravity.START,
    175                 mActivity.getActivityContext().getString(R.string.drawer_title));
    176         mDrawerContainer.setStatusBarBackground(R.color.primary_dark_color);
    177         final String drawerPulloutTag = mActivity.getString(R.string.drawer_pullout_tag);
    178         mDrawerPullout = mDrawerContainer.findViewWithTag(drawerPulloutTag);
    179         mDrawerPullout.setBackgroundResource(R.color.list_background_color);
    180 
    181         // CV is initially GONE on 1-pane (mode changes trigger visibility changes)
    182         mActivity.findViewById(R.id.conversation_pager).setVisibility(View.GONE);
    183 
    184         // The parent class sets the correct viewmode and starts the application off.
    185         super.onCreate(savedInstanceState);
    186     }
    187 
    188     @Override
    189     protected ActionableToastBar findActionableToastBar(MailActivity activity) {
    190         final ActionableToastBar tb = super.findActionableToastBar(activity);
    191 
    192         // notify the toast bar of its sibling floating action button so it can move them together
    193         // as they animate
    194         tb.setFloatingActionButton(activity.findViewById(R.id.compose_button));
    195         return tb;
    196     }
    197 
    198     @Override
    199     protected boolean isConversationListVisible() {
    200         return mConversationListVisible;
    201     }
    202 
    203     @Override
    204     public void onViewModeChanged(int newMode) {
    205         super.onViewModeChanged(newMode);
    206 
    207         // When entering conversation list mode, hide and clean up any currently visible
    208         // conversation.
    209         if (ViewMode.isListMode(newMode)) {
    210             mPagerController.hide(true /* changeVisibility */);
    211         }
    212 
    213         if (ViewMode.isAdMode(newMode)) {
    214             onConversationListVisibilityChanged(false);
    215         }
    216 
    217         // When we step away from the conversation mode, we don't have a current conversation
    218         // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
    219         if (!ViewMode.isConversationMode(newMode)) {
    220             setCurrentConversation(null);
    221         }
    222     }
    223 
    224     @Override
    225     protected void appendToString(StringBuilder sb) {
    226         sb.append(" lastConvListTransId=");
    227         sb.append(mLastConversationListTransactionId);
    228     }
    229 
    230     @Override
    231     protected void showConversationList(ConversationListContext listContext) {
    232         enableCabMode();
    233         mConversationListVisible = true;
    234         if (ConversationListContext.isSearchResult(listContext)) {
    235             mViewMode.enterSearchResultsListMode();
    236         } else {
    237             mViewMode.enterConversationListMode();
    238         }
    239         final int transition = mConversationListNeverShown
    240                 ? FragmentTransaction.TRANSIT_FRAGMENT_FADE
    241                 : FragmentTransaction.TRANSIT_FRAGMENT_OPEN;
    242         final Fragment conversationListFragment =
    243                 ConversationListFragment.newInstance(listContext);
    244 
    245         if (!inInbox(mAccount, listContext)) {
    246             // Maintain fragment transaction history so we can get back to the
    247             // fragment used to launch this list.
    248             mLastConversationListTransactionId = replaceFragment(conversationListFragment,
    249                     transition, TAG_CONVERSATION_LIST, R.id.content_pane);
    250         } else {
    251             // If going to the inbox, clear the folder list transaction history.
    252             mInbox = listContext.folder;
    253             replaceFragment(conversationListFragment, transition, TAG_CONVERSATION_LIST,
    254                     R.id.content_pane);
    255 
    256             // If we ever to to the inbox, we want to unset the transation id for any other
    257             // non-inbox folder.
    258             mLastConversationListTransactionId = INVALID_ID;
    259         }
    260 
    261         mActivity.getFragmentManager().executePendingTransactions();
    262 
    263         onConversationVisibilityChanged(false);
    264         onConversationListVisibilityChanged(true);
    265         mConversationListNeverShown = false;
    266     }
    267 
    268     /**
    269      * Override showConversation with animation parameter so that we animate in the pager when
    270      * selecting in the conversation, but don't animate on opening the app from an intent.
    271      * @param conversation
    272      * @param shouldAnimate true if we want to animate the conversation in, false otherwise
    273      */
    274     @Override
    275     protected void showConversation(Conversation conversation, boolean shouldAnimate) {
    276         super.showConversation(conversation, shouldAnimate);
    277 
    278         mConversationListVisible = false;
    279         if (conversation == null) {
    280             transitionBackToConversationListMode();
    281             return;
    282         }
    283         disableCabMode();
    284         if (ConversationListContext.isSearchResult(mConvListContext)) {
    285             mViewMode.enterSearchResultsConversationMode();
    286         } else {
    287             mViewMode.enterConversationMode();
    288         }
    289 
    290         mPagerController.show(mAccount, mFolder, conversation, true /* changeVisibility */,
    291                 shouldAnimate? mPagerAnimationListener : null);
    292         onConversationVisibilityChanged(true);
    293         onConversationListVisibilityChanged(false);
    294     }
    295 
    296     @Override
    297     public void onConversationFocused(Conversation conversation) {
    298         // Do nothing
    299     }
    300 
    301     @Override
    302     protected void showWaitForInitialization() {
    303         super.showWaitForInitialization();
    304         replaceFragment(getWaitFragment(), FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT,
    305                 R.id.content_pane);
    306     }
    307 
    308     @Override
    309     protected void hideWaitForInitialization() {
    310         transitionToInbox();
    311         super.hideWaitForInitialization();
    312     }
    313 
    314     /**
    315      * Switch to the Inbox by creating a new conversation list context that loads the inbox.
    316      */
    317     private void transitionToInbox() {
    318         // The inbox could have changed, in which case we should load it again.
    319         if (mInbox == null || !isDefaultInbox(mInbox.folderUri, mAccount)) {
    320             loadAccountInbox();
    321         } else {
    322             onFolderChanged(mInbox, false /* force */);
    323         }
    324     }
    325 
    326     @Override
    327     public boolean doesActionChangeConversationListVisibility(final int action) {
    328         if (action == R.id.archive
    329                 || action == R.id.remove_folder
    330                 || action == R.id.delete
    331                 || action == R.id.discard_drafts
    332                 || action == R.id.discard_outbox
    333                 || action == R.id.mark_important
    334                 || action == R.id.mark_not_important
    335                 || action == R.id.mute
    336                 || action == R.id.report_spam
    337                 || action == R.id.mark_not_spam
    338                 || action == R.id.report_phishing
    339                 || action == R.id.refresh
    340                 || action == R.id.change_folders) {
    341             return false;
    342         } else {
    343             return true;
    344         }
    345     }
    346 
    347     /**
    348      * Replace the content_pane with the fragment specified here. The tag is specified so that
    349      * the {@link ActivityController} can look up the fragments through the
    350      * {@link android.app.FragmentManager}.
    351      * @param fragment the new fragment to put
    352      * @param transition the transition to show
    353      * @param tag a tag for the fragment manager.
    354      * @param anchor ID of view to replace fragment in
    355      * @return transaction ID returned when the transition is committed.
    356      */
    357     private int replaceFragment(Fragment fragment, int transition, String tag, int anchor) {
    358         final FragmentManager fm = mActivity.getFragmentManager();
    359         FragmentTransaction fragmentTransaction = fm.beginTransaction();
    360         fragmentTransaction.setTransition(transition);
    361         fragmentTransaction.replace(anchor, fragment, tag);
    362         final int id = fragmentTransaction.commitAllowingStateLoss();
    363         fm.executePendingTransactions();
    364         return id;
    365     }
    366 
    367     /**
    368      * Back works as follows:
    369      * 1) If the drawer is pulled out (Or mid-drag), close it - handled.
    370      * 2) If the user is in the folder list view, go back
    371      * to the account default inbox.
    372      * 3) If the user is in a conversation list
    373      * that is not the inbox AND:
    374      *  a) they got there by going through the folder
    375      *  list view, go back to the folder list view.
    376      *  b) they got there by using some other means (account dropdown), go back to the inbox.
    377      * 4) If the user is in a conversation, go back to the conversation list they were last in.
    378      * 5) If the user is in the conversation list for the default account inbox,
    379      * back exits the app.
    380      */
    381     @Override
    382     public boolean handleBackPress() {
    383         final int mode = mViewMode.getMode();
    384 
    385         if (mode == ViewMode.SEARCH_RESULTS_LIST) {
    386             mActivity.finish();
    387         } else if (mViewMode.isListMode() && !inInbox(mAccount, mConvListContext)) {
    388             navigateUpFolderHierarchy();
    389         } else if (mViewMode.isConversationMode() || mViewMode.isAdMode()) {
    390             transitionBackToConversationListMode();
    391         } else {
    392             mActivity.finish();
    393         }
    394         mToastBar.hide(false, false /* actionClicked */);
    395         return true;
    396     }
    397 
    398     @Override
    399     public void onFolderSelected(Folder folder) {
    400         if (mViewMode.isSearchMode()) {
    401             // We are in an activity on top of the main navigation activity.
    402             // We need to return to it with a result code that indicates it should navigate to
    403             // a different folder.
    404             final Intent intent = new Intent();
    405             intent.putExtra(AbstractActivityController.EXTRA_FOLDER, folder);
    406             mActivity.setResult(Activity.RESULT_OK, intent);
    407             mActivity.finish();
    408             return;
    409         }
    410         setHierarchyFolder(folder);
    411         super.onFolderSelected(folder);
    412     }
    413 
    414     /**
    415      * Up works as follows:
    416      * 1) If the user is in a conversation list that is not the default account inbox,
    417      * a conversation, or the folder list, up follows the rules of back.
    418      * 2) If the user is in search results, up exits search
    419      * mode and returns the user to whatever view they were in when they began search.
    420      * 3) If the user is in the inbox, there is no up.
    421      */
    422     @Override
    423     public boolean handleUpPress() {
    424         final int mode = mViewMode.getMode();
    425         if (mode == ViewMode.SEARCH_RESULTS_LIST) {
    426             mActivity.finish();
    427             // Not needed, the activity is going away anyway.
    428         } else if (mode == ViewMode.CONVERSATION_LIST
    429                 || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
    430             final boolean isTopLevel = Folder.isRoot(mFolder);
    431 
    432             if (isTopLevel) {
    433                 // Show the drawer.
    434                 toggleDrawerState();
    435             } else {
    436                 navigateUpFolderHierarchy();
    437             }
    438         } else if (mode == ViewMode.CONVERSATION || mode == ViewMode.SEARCH_RESULTS_CONVERSATION
    439                 || mode == ViewMode.AD) {
    440             // Same as go back.
    441             handleBackPress();
    442         }
    443         return true;
    444     }
    445 
    446     private void transitionBackToConversationListMode() {
    447         final int mode = mViewMode.getMode();
    448         enableCabMode();
    449         mConversationListVisible = true;
    450         if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
    451             mViewMode.enterSearchResultsListMode();
    452         } else {
    453             mViewMode.enterConversationListMode();
    454         }
    455 
    456         final Folder folder = mFolder != null ? mFolder : mInbox;
    457         onFolderChanged(folder, true /* force */);
    458 
    459         onConversationVisibilityChanged(false);
    460         onConversationListVisibilityChanged(true);
    461     }
    462 
    463     @Override
    464     public boolean shouldShowFirstConversation() {
    465         return false;
    466     }
    467 
    468     @Override
    469     public void onUndoAvailable(ToastBarOperation op) {
    470         if (op != null && mAccount.supportsCapability(UIProvider.AccountCapabilities.UNDO)) {
    471             final int mode = mViewMode.getMode();
    472             final ConversationListFragment convList = getConversationListFragment();
    473             switch (mode) {
    474                 case ViewMode.SEARCH_RESULTS_CONVERSATION:
    475                 case ViewMode.CONVERSATION:
    476                     mToastBar.show(getUndoClickedListener(
    477                             convList != null ? convList.getAnimatedAdapter() : null),
    478                             Utils.convertHtmlToPlainText
    479                                 (op.getDescription(mActivity.getActivityContext())),
    480                             R.string.undo,
    481                             true /* replaceVisibleToast */,
    482                             true /* autohide */,
    483                             op);
    484                     break;
    485                 case ViewMode.SEARCH_RESULTS_LIST:
    486                 case ViewMode.CONVERSATION_LIST:
    487                     if (convList != null) {
    488                         mToastBar.show(
    489                                 getUndoClickedListener(convList.getAnimatedAdapter()),
    490                                 Utils.convertHtmlToPlainText
    491                                     (op.getDescription(mActivity.getActivityContext())),
    492                                 R.string.undo,
    493                                 true /* replaceVisibleToast */,
    494                                 true /* autohide */,
    495                                 op);
    496                     } else {
    497                         mActivity.setPendingToastOperation(op);
    498                     }
    499                     break;
    500             }
    501         }
    502     }
    503 
    504     @Override
    505     public void onError(final Folder folder, boolean replaceVisibleToast) {
    506         final int mode = mViewMode.getMode();
    507         switch (mode) {
    508             case ViewMode.SEARCH_RESULTS_LIST:
    509             case ViewMode.CONVERSATION_LIST:
    510                 showErrorToast(folder, replaceVisibleToast);
    511                 break;
    512             default:
    513                 break;
    514         }
    515     }
    516 
    517     @Override
    518     public boolean isDrawerEnabled() {
    519         // The drawer is enabled for one pane mode
    520         return true;
    521     }
    522 
    523     @Override
    524     public int getFolderListViewChoiceMode() {
    525         // By default, we do not want to allow any item to be selected in the folder list
    526         return ListView.CHOICE_MODE_NONE;
    527     }
    528 
    529     @Override
    530     public void launchFragment(final Fragment fragment, final int selectPosition) {
    531         replaceFragment(fragment, FragmentTransaction.TRANSIT_FRAGMENT_OPEN,
    532                 TAG_CUSTOM_FRAGMENT, R.id.content_pane);
    533     }
    534 
    535     @Override
    536     public boolean onInterceptKeyFromCV(int keyCode, KeyEvent keyEvent, boolean navigateAway) {
    537         // Not applicable
    538         return false;
    539     }
    540 
    541     @Override
    542     public boolean isTwoPaneLandscape() {
    543         return false;
    544     }
    545 
    546     @Override
    547     public boolean shouldShowSearchBarByDefault(int viewMode) {
    548         return viewMode == ViewMode.SEARCH_RESULTS_LIST;
    549     }
    550 
    551     @Override
    552     public boolean shouldShowSearchMenuItem() {
    553         return mViewMode.getMode() == ViewMode.CONVERSATION_LIST;
    554     }
    555 
    556     @Override
    557     public void addConversationListLayoutListener(
    558             TwoPaneLayout.ConversationListLayoutListener listener) {
    559         // Do nothing
    560     }
    561 
    562 }
    563