• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
TwoPaneController(MailActivity activity, ViewMode viewMode)58     public TwoPaneController(MailActivity activity, ViewMode viewMode) {
59         super(activity, viewMode);
60     }
61 
62     /**
63      * Display the conversation list fragment.
64      */
initializeConversationListFragment()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      */
renderConversationList()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
doesActionChangeConversationListVisibility(final int action)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
isConversationListVisible()108     protected boolean isConversationListVisible() {
109         return !mLayout.isConversationListCollapsed();
110     }
111 
112     @Override
showConversationList(ConversationListContext listContext)113     public void showConversationList(ConversationListContext listContext) {
114         super.showConversationList(listContext);
115         initializeConversationListFragment();
116     }
117 
118     @Override
onCreate(Bundle savedState)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
onSaveInstanceState(Bundle outState)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
onWindowFocusChanged(boolean hasFocus)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
onFolderSelected(Folder folder)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
onViewModeChanged(int newMode)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
onConversationVisibilityChanged(boolean visible)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
onConversationListVisibilityChanged(boolean visible)215     public void onConversationListVisibilityChanged(boolean visible) {
216         super.onConversationListVisibilityChanged(visible);
217         enableOrDisableCab();
218     }
219 
220     @Override
resetActionBarIcon()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      */
enableOrDisableCab()238     private void enableOrDisableCab() {
239         if (mLayout.isConversationListCollapsed()) {
240             disableCabMode();
241         } else {
242             enableCabMode();
243         }
244     }
245 
246     @Override
onSetPopulated(ConversationSelectionSet set)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
onSetEmpty()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
showConversation(Conversation conversation, boolean inLoaderCallbacks)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
setCurrentConversation(Conversation conversation)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
showWaitForInitialization()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
hideWaitForInitialization()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
handleUpPress()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
handleBackPress()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      */
popView(boolean preventClose)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
exitSearchMode()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
shouldShowFirstConversation()462     public boolean shouldShowFirstConversation() {
463         return Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
464                 && shouldEnterSearchConvMode();
465     }
466 
467     @Override
onUndoAvailable(ToastBarOperation op)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 
repositionToastBar(ToastBarOperation op)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      */
repositionToastBar(boolean convModeShowInList)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
hideOrRepositionToastBar(final boolean animated)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
onError(final Folder folder, boolean replaceVisibleToast)554     public void onError(final Folder folder, boolean replaceVisibleToast) {
555         repositionToastBar(true /* convModeShowInList */);
556         showErrorToast(folder, replaceVisibleToast);
557     }
558 
559     @Override
isDrawerEnabled()560     public boolean isDrawerEnabled() {
561         return mLayout.isDrawerEnabled();
562     }
563 
564     @Override
getFolderListViewChoiceMode()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
launchFragment(final Fragment fragment, final int selectPosition)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