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.net.Uri;
     24 import android.os.Bundle;
     25 import android.support.v4.widget.DrawerLayout;
     26 import android.widget.ListView;
     27 
     28 import com.android.mail.ConversationListContext;
     29 import com.android.mail.R;
     30 import com.android.mail.providers.Account;
     31 import com.android.mail.providers.Conversation;
     32 import com.android.mail.providers.Folder;
     33 import com.android.mail.providers.UIProvider;
     34 import com.android.mail.utils.FolderUri;
     35 import com.android.mail.utils.Utils;
     36 
     37 /**
     38  * Controller for one-pane Mail activity. One Pane is used for phones, where screen real estate is
     39  * limited. This controller also does the layout, since the layout is simpler in the one pane case.
     40  */
     41 
     42 public final class OnePaneController extends AbstractActivityController {
     43     /** Key used to store {@link #mLastConversationListTransactionId} */
     44     private static final String CONVERSATION_LIST_TRANSACTION_KEY = "conversation-list-transaction";
     45     /** Key used to store {@link #mLastConversationTransactionId}. */
     46     private static final String CONVERSATION_TRANSACTION_KEY = "conversation-transaction";
     47     /** Key used to store {@link #mConversationListVisible}. */
     48     private static final String CONVERSATION_LIST_VISIBLE_KEY = "conversation-list-visible";
     49     /** Key used to store {@link #mConversationListNeverShown}. */
     50     private static final String CONVERSATION_LIST_NEVER_SHOWN_KEY = "conversation-list-never-shown";
     51 
     52     private static final int INVALID_ID = -1;
     53     private boolean mConversationListVisible = false;
     54     private int mLastConversationListTransactionId = INVALID_ID;
     55     private int mLastConversationTransactionId = INVALID_ID;
     56     /** Whether a conversation list for this account has ever been shown.*/
     57     private boolean mConversationListNeverShown = true;
     58 
     59     public OnePaneController(MailActivity activity, ViewMode viewMode) {
     60         super(activity, viewMode);
     61     }
     62 
     63     @Override
     64     public void onRestoreInstanceState(Bundle inState) {
     65         super.onRestoreInstanceState(inState);
     66         if (inState == null) {
     67             return;
     68         }
     69         mLastConversationListTransactionId =
     70                 inState.getInt(CONVERSATION_LIST_TRANSACTION_KEY, INVALID_ID);
     71         mLastConversationTransactionId = inState.getInt(CONVERSATION_TRANSACTION_KEY, INVALID_ID);
     72         mConversationListVisible = inState.getBoolean(CONVERSATION_LIST_VISIBLE_KEY);
     73         mConversationListNeverShown = inState.getBoolean(CONVERSATION_LIST_NEVER_SHOWN_KEY);
     74     }
     75 
     76     @Override
     77     public void onSaveInstanceState(Bundle outState) {
     78         super.onSaveInstanceState(outState);
     79         outState.putInt(CONVERSATION_LIST_TRANSACTION_KEY, mLastConversationListTransactionId);
     80         outState.putInt(CONVERSATION_TRANSACTION_KEY, mLastConversationTransactionId);
     81         outState.putBoolean(CONVERSATION_LIST_VISIBLE_KEY, mConversationListVisible);
     82         outState.putBoolean(CONVERSATION_LIST_NEVER_SHOWN_KEY, mConversationListNeverShown);
     83     }
     84 
     85     @Override
     86     public void resetActionBarIcon() {
     87         // Calling resetActionBarIcon should never remove the up affordance
     88         // even when waiting for sync (Folder list should still show with one
     89         // account. Currently this method is blank to avoid any changes.
     90     }
     91 
     92     /**
     93      * Returns true if the candidate URI is the URI for the default inbox for the given account.
     94      * @param candidate the URI to check
     95      * @param account the account whose default Inbox the candidate might be
     96      * @return true if the candidate is indeed the default inbox for the given account.
     97      */
     98     private static boolean isDefaultInbox(FolderUri candidate, Account account) {
     99         return (candidate != null && account != null)
    100                 && candidate.equals(account.settings.defaultInbox);
    101     }
    102 
    103     /**
    104      * Returns true if the user is currently in the conversation list view, viewing the default
    105      * inbox.
    106      * @return true if user is in conversation list mode, viewing the default inbox.
    107      */
    108     private static boolean inInbox(final Account account, final ConversationListContext context) {
    109         // If we don't have valid state, then we are not in the inbox.
    110         return !(account == null || context == null || context.folder == null
    111                 || account.settings == null) && !ConversationListContext.isSearchResult(context)
    112                 && isDefaultInbox(context.folder.folderUri, account);
    113     }
    114 
    115     /**
    116      * On account change, carry out super implementation, load FolderListFragment
    117      * into drawer (to avoid repetitive calls to replaceFragment).
    118      */
    119     @Override
    120     public void changeAccount(Account account) {
    121         super.changeAccount(account);
    122         mConversationListNeverShown = true;
    123         closeDrawerIfOpen();
    124     }
    125 
    126     @Override
    127     public boolean onCreate(Bundle savedInstanceState) {
    128         mActivity.setContentView(R.layout.one_pane_activity);
    129         mDrawerContainer = (DrawerLayout) mActivity.findViewById(R.id.drawer_container);
    130         mDrawerPullout = mDrawerContainer.findViewById(R.id.drawer_pullout);
    131         mDrawerPullout.setBackgroundResource(R.color.list_background_color);
    132 
    133         // The parent class sets the correct viewmode and starts the application off.
    134         return super.onCreate(savedInstanceState);
    135     }
    136 
    137     @Override
    138     protected boolean isConversationListVisible() {
    139         return mConversationListVisible;
    140     }
    141 
    142     @Override
    143     public void onViewModeChanged(int newMode) {
    144         super.onViewModeChanged(newMode);
    145 
    146         // When entering conversation list mode, hide and clean up any currently visible
    147         // conversation.
    148         if (ViewMode.isListMode(newMode)) {
    149             mPagerController.hide(true /* changeVisibility */);
    150         }
    151         // When we step away from the conversation mode, we don't have a current conversation
    152         // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
    153         if (!ViewMode.isConversationMode(newMode)) {
    154             setCurrentConversation(null);
    155         }
    156     }
    157 
    158     @Override
    159     public String toString() {
    160         final StringBuilder sb = new StringBuilder(super.toString());
    161         sb.append(" lastConvListTransId=");
    162         sb.append(mLastConversationListTransactionId);
    163         sb.append("}");
    164         return sb.toString();
    165     }
    166 
    167     @Override
    168     public void showConversationList(ConversationListContext listContext) {
    169         super.showConversationList(listContext);
    170         enableCabMode();
    171         mConversationListVisible = true;
    172         if (ConversationListContext.isSearchResult(listContext)) {
    173             mViewMode.enterSearchResultsListMode();
    174         } else {
    175             mViewMode.enterConversationListMode();
    176         }
    177         final int transition = mConversationListNeverShown
    178                 ? FragmentTransaction.TRANSIT_FRAGMENT_FADE
    179                 : FragmentTransaction.TRANSIT_FRAGMENT_OPEN;
    180         final Fragment conversationListFragment =
    181                 ConversationListFragment.newInstance(listContext);
    182 
    183         if (!inInbox(mAccount, listContext)) {
    184             // Maintain fragment transaction history so we can get back to the
    185             // fragment used to launch this list.
    186             mLastConversationListTransactionId = replaceFragment(conversationListFragment,
    187                     transition, TAG_CONVERSATION_LIST, R.id.content_pane);
    188         } else {
    189             // If going to the inbox, clear the folder list transaction history.
    190             mInbox = listContext.folder;
    191             replaceFragment(conversationListFragment, transition, TAG_CONVERSATION_LIST,
    192                     R.id.content_pane);
    193 
    194             // If we ever to to the inbox, we want to unset the transation id for any other
    195             // non-inbox folder.
    196             mLastConversationListTransactionId = INVALID_ID;
    197         }
    198 
    199         mActivity.getFragmentManager().executePendingTransactions();
    200 
    201         onConversationVisibilityChanged(false);
    202         onConversationListVisibilityChanged(true);
    203         mConversationListNeverShown = false;
    204     }
    205 
    206     @Override
    207     protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
    208         super.showConversation(conversation, inLoaderCallbacks);
    209         mConversationListVisible = false;
    210         if (conversation == null) {
    211             transitionBackToConversationListMode();
    212             return;
    213         }
    214         disableCabMode();
    215         if (ConversationListContext.isSearchResult(mConvListContext)) {
    216             mViewMode.enterSearchResultsConversationMode();
    217         } else {
    218             mViewMode.enterConversationMode();
    219         }
    220         final FragmentManager fm = mActivity.getFragmentManager();
    221         final FragmentTransaction ft = fm.beginTransaction();
    222         // Switching to conversation view is an incongruous transition:
    223         // we are not replacing a fragment with another fragment as
    224         // usual. Instead, reveal the heretofore inert conversation
    225         // ViewPager and just remove the previously visible fragment
    226         // e.g. conversation list, or possibly label list?).
    227         final Fragment f = fm.findFragmentById(R.id.content_pane);
    228         // FragmentManager#findFragmentById can return fragments that are not added to the activity.
    229         // We want to make sure that we don't attempt to remove fragments that are not added to the
    230         // activity, as when the transaction is popped off, the FragmentManager will attempt to
    231         // readd the same fragment twice
    232         if (f != null && f.isAdded()) {
    233             ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
    234             ft.remove(f);
    235             ft.commitAllowingStateLoss();
    236             fm.executePendingTransactions();
    237         }
    238         mPagerController.show(mAccount, mFolder, conversation, true /* changeVisibility */);
    239         onConversationVisibilityChanged(true);
    240         onConversationListVisibilityChanged(false);
    241     }
    242 
    243     @Override
    244     public void showWaitForInitialization() {
    245         super.showWaitForInitialization();
    246         replaceFragment(getWaitFragment(), FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT,
    247                 R.id.content_pane);
    248     }
    249 
    250     @Override
    251     protected void hideWaitForInitialization() {
    252         transitionToInbox();
    253         super.hideWaitForInitialization();
    254     }
    255 
    256     @Override
    257     public boolean doesActionChangeConversationListVisibility(final int action) {
    258         if (action == R.id.archive
    259                 || action == R.id.remove_folder
    260                 || action == R.id.delete
    261                 || action == R.id.discard_drafts
    262                 || action == R.id.mark_important
    263                 || action == R.id.mark_not_important
    264                 || action == R.id.mute
    265                 || action == R.id.report_spam
    266                 || action == R.id.mark_not_spam
    267                 || action == R.id.report_phishing
    268                 || action == R.id.refresh
    269                 || action == R.id.change_folders) {
    270             return false;
    271         } else {
    272             return true;
    273         }
    274     }
    275 
    276     /**
    277      * Replace the content_pane with the fragment specified here. The tag is specified so that
    278      * the {@link ActivityController} can look up the fragments through the
    279      * {@link android.app.FragmentManager}.
    280      * @param fragment the new fragment to put
    281      * @param transition the transition to show
    282      * @param tag a tag for the fragment manager.
    283      * @param anchor ID of view to replace fragment in
    284      * @return transaction ID returned when the transition is committed.
    285      */
    286     private int replaceFragment(Fragment fragment, int transition, String tag, int anchor) {
    287         final FragmentManager fm = mActivity.getFragmentManager();
    288         FragmentTransaction fragmentTransaction = fm.beginTransaction();
    289         fragmentTransaction.setTransition(transition);
    290         fragmentTransaction.replace(anchor, fragment, tag);
    291         final int id = fragmentTransaction.commitAllowingStateLoss();
    292         fm.executePendingTransactions();
    293         return id;
    294     }
    295 
    296     /**
    297      * Back works as follows:
    298      * 1) If the drawer is pulled out (Or mid-drag), close it - handled.
    299      * 2) If the user is in the folder list view, go back
    300      * to the account default inbox.
    301      * 3) If the user is in a conversation list
    302      * that is not the inbox AND:
    303      *  a) they got there by going through the folder
    304      *  list view, go back to the folder list view.
    305      *  b) they got there by using some other means (account dropdown), go back to the inbox.
    306      * 4) If the user is in a conversation, go back to the conversation list they were last in.
    307      * 5) If the user is in the conversation list for the default account inbox,
    308      * back exits the app.
    309      */
    310     @Override
    311     public boolean handleBackPress() {
    312         final int mode = mViewMode.getMode();
    313 
    314         if (mode == ViewMode.SEARCH_RESULTS_LIST) {
    315             mActivity.finish();
    316         } else if (mViewMode.isListMode() && !inInbox(mAccount, mConvListContext)) {
    317             navigateUpFolderHierarchy();
    318         } else if (mViewMode.isConversationMode() || mViewMode.isAdMode()) {
    319             transitionBackToConversationListMode();
    320         } else {
    321             mActivity.finish();
    322         }
    323         mToastBar.hide(false, false /* actionClicked */);
    324         return true;
    325     }
    326 
    327     /**
    328      * Switch to the Inbox by creating a new conversation list context that loads the inbox.
    329      */
    330     private void transitionToInbox() {
    331         // The inbox could have changed, in which case we should load it again.
    332         if (mInbox == null || !isDefaultInbox(mInbox.folderUri, mAccount)) {
    333             loadAccountInbox();
    334         } else {
    335             onFolderChanged(mInbox, false /* force */);
    336         }
    337     }
    338 
    339     @Override
    340     public void onFolderSelected(Folder folder) {
    341         setHierarchyFolder(folder);
    342         super.onFolderSelected(folder);
    343     }
    344 
    345     /**
    346      * Up works as follows:
    347      * 1) If the user is in a conversation list that is not the default account inbox,
    348      * a conversation, or the folder list, up follows the rules of back.
    349      * 2) If the user is in search results, up exits search
    350      * mode and returns the user to whatever view they were in when they began search.
    351      * 3) If the user is in the inbox, there is no up.
    352      */
    353     @Override
    354     public boolean handleUpPress() {
    355         final int mode = mViewMode.getMode();
    356         if (mode == ViewMode.SEARCH_RESULTS_LIST) {
    357             mActivity.finish();
    358             // Not needed, the activity is going away anyway.
    359         } else if (mode == ViewMode.CONVERSATION_LIST
    360                 || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
    361             final boolean isTopLevel = (mFolder == null) || (mFolder.parent == Uri.EMPTY);
    362 
    363             if (isTopLevel) {
    364                 // Show the drawer.
    365                 toggleDrawerState();
    366             } else {
    367                 navigateUpFolderHierarchy();
    368             }
    369         } else if (mode == ViewMode.CONVERSATION || mode == ViewMode.SEARCH_RESULTS_CONVERSATION
    370                 || mode == ViewMode.AD) {
    371             // Same as go back.
    372             handleBackPress();
    373         }
    374         return true;
    375     }
    376 
    377     private void transitionBackToConversationListMode() {
    378         final int mode = mViewMode.getMode();
    379         enableCabMode();
    380         mConversationListVisible = true;
    381         if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
    382             mViewMode.enterSearchResultsListMode();
    383         } else {
    384             mViewMode.enterConversationListMode();
    385         }
    386 
    387         final Folder folder = mFolder != null ? mFolder : mInbox;
    388         onFolderChanged(folder, true /* force */);
    389 
    390         onConversationVisibilityChanged(false);
    391         onConversationListVisibilityChanged(true);
    392     }
    393 
    394     @Override
    395     public boolean shouldShowFirstConversation() {
    396         return false;
    397     }
    398 
    399     @Override
    400     public void onUndoAvailable(ToastBarOperation op) {
    401         if (op != null && mAccount.supportsCapability(UIProvider.AccountCapabilities.UNDO)) {
    402             final int mode = mViewMode.getMode();
    403             final ConversationListFragment convList = getConversationListFragment();
    404             switch (mode) {
    405                 case ViewMode.SEARCH_RESULTS_CONVERSATION:
    406                 case ViewMode.CONVERSATION:
    407                     mToastBar.show(getUndoClickedListener(
    408                             convList != null ? convList.getAnimatedAdapter() : null),
    409                             0,
    410                             Utils.convertHtmlToPlainText
    411                                 (op.getDescription(mActivity.getActivityContext())),
    412                             true, /* showActionIcon */
    413                             R.string.undo,
    414                             true,  /* replaceVisibleToast */
    415                             op);
    416                     break;
    417                 case ViewMode.SEARCH_RESULTS_LIST:
    418                 case ViewMode.CONVERSATION_LIST:
    419                     if (convList != null) {
    420                         mToastBar.show(
    421                                 getUndoClickedListener(convList.getAnimatedAdapter()),
    422                                 0,
    423                                 Utils.convertHtmlToPlainText
    424                                     (op.getDescription(mActivity.getActivityContext())),
    425                                 true, /* showActionIcon */
    426                                 R.string.undo,
    427                                 true,  /* replaceVisibleToast */
    428                                 op);
    429                     } else {
    430                         mActivity.setPendingToastOperation(op);
    431                     }
    432                     break;
    433             }
    434         }
    435     }
    436 
    437     @Override
    438     protected void hideOrRepositionToastBar(boolean animated) {
    439         mToastBar.hide(animated, false /* actionClicked */);
    440     }
    441 
    442     @Override
    443     public void onError(final Folder folder, boolean replaceVisibleToast) {
    444         final int mode = mViewMode.getMode();
    445         switch (mode) {
    446             case ViewMode.SEARCH_RESULTS_LIST:
    447             case ViewMode.CONVERSATION_LIST:
    448                 showErrorToast(folder, replaceVisibleToast);
    449                 break;
    450             default:
    451                 break;
    452         }
    453     }
    454 
    455     @Override
    456     public boolean isDrawerEnabled() {
    457         // The drawer is enabled for one pane mode
    458         return true;
    459     }
    460 
    461     @Override
    462     public int getFolderListViewChoiceMode() {
    463         // By default, we do not want to allow any item to be selected in the folder list
    464         return ListView.CHOICE_MODE_NONE;
    465     }
    466 
    467     @Override
    468     public void launchFragment(final Fragment fragment, final int selectPosition) {
    469         replaceFragment(fragment, FragmentTransaction.TRANSIT_FRAGMENT_OPEN,
    470                 TAG_CUSTOM_FRAGMENT, R.id.content_pane);
    471     }
    472 }
    473