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 
OnePaneController(MailActivity activity, ViewMode viewMode)99     public OnePaneController(MailActivity activity, ViewMode viewMode) {
100         super(activity, viewMode);
101     }
102 
103     @Override
onRestoreInstanceState(Bundle inState)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
onSaveInstanceState(Bundle outState)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
resetActionBarIcon()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      */
isDefaultInbox(FolderUri candidate, Account account)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      */
inInbox(final Account account, final ConversationListContext context)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
changeAccount(Account account)160     public void changeAccount(Account account) {
161         super.changeAccount(account);
162         mConversationListNeverShown = true;
163         closeDrawerIfOpen();
164     }
165 
166     @Override
getContentViewResource()167     public @LayoutRes int getContentViewResource() {
168         return R.layout.one_pane_activity;
169     }
170 
171     @Override
onCreate(Bundle savedInstanceState)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
findActionableToastBar(MailActivity activity)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
isConversationListVisible()199     protected boolean isConversationListVisible() {
200         return mConversationListVisible;
201     }
202 
203     @Override
onViewModeChanged(int newMode)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
appendToString(StringBuilder sb)225     protected void appendToString(StringBuilder sb) {
226         sb.append(" lastConvListTransId=");
227         sb.append(mLastConversationListTransactionId);
228     }
229 
230     @Override
showConversationList(ConversationListContext listContext)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
showConversation(Conversation conversation, boolean shouldAnimate)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
onConversationFocused(Conversation conversation)297     public void onConversationFocused(Conversation conversation) {
298         // Do nothing
299     }
300 
301     @Override
showWaitForInitialization()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
hideWaitForInitialization()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      */
transitionToInbox()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
doesActionChangeConversationListVisibility(final int action)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      */
replaceFragment(Fragment fragment, int transition, String tag, int anchor)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
handleBackPress()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
onFolderSelected(Folder folder)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
handleUpPress()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 
transitionBackToConversationListMode()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
shouldShowFirstConversation()464     public boolean shouldShowFirstConversation() {
465         return false;
466     }
467 
468     @Override
onUndoAvailable(ToastBarOperation op)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
onError(final Folder folder, boolean replaceVisibleToast)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
isDrawerEnabled()518     public boolean isDrawerEnabled() {
519         // The drawer is enabled for one pane mode
520         return true;
521     }
522 
523     @Override
getFolderListViewChoiceMode()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
launchFragment(final Fragment fragment, final int selectPosition)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
onInterceptKeyFromCV(int keyCode, KeyEvent keyEvent, boolean navigateAway)536     public boolean onInterceptKeyFromCV(int keyCode, KeyEvent keyEvent, boolean navigateAway) {
537         // Not applicable
538         return false;
539     }
540 
541     @Override
isTwoPaneLandscape()542     public boolean isTwoPaneLandscape() {
543         return false;
544     }
545 
546     @Override
shouldShowSearchBarByDefault(int viewMode)547     public boolean shouldShowSearchBarByDefault(int viewMode) {
548         return viewMode == ViewMode.SEARCH_RESULTS_LIST;
549     }
550 
551     @Override
shouldShowSearchMenuItem()552     public boolean shouldShowSearchMenuItem() {
553         return mViewMode.getMode() == ViewMode.CONVERSATION_LIST;
554     }
555 
556     @Override
addConversationListLayoutListener( TwoPaneLayout.ConversationListLayoutListener listener)557     public void addConversationListLayoutListener(
558             TwoPaneLayout.ConversationListLayoutListener listener) {
559         // Do nothing
560     }
561 
562 }
563