• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.email.activity;
18 
19 import android.app.Activity;
20 import android.app.Fragment;
21 import android.app.FragmentTransaction;
22 import android.os.Bundle;
23 import android.util.Log;
24 import android.view.Menu;
25 import android.view.MenuInflater;
26 import android.view.MenuItem;
27 
28 import com.android.email.Email;
29 import com.android.email.MessageListContext;
30 import com.android.email.R;
31 import com.android.emailcommon.Logging;
32 import com.android.emailcommon.provider.Account;
33 import com.android.emailcommon.provider.EmailContent.Message;
34 import com.android.emailcommon.provider.Mailbox;
35 
36 import java.util.Set;
37 
38 
39 /**
40  * UI Controller for non x-large devices.  Supports a single-pane layout.
41  *
42  * One one-pane, only at most one fragment can be installed at a time.
43  *
44  * Note: Always use {@link #commitFragmentTransaction} to operate fragment transactions,
45  * so that we can easily switch between synchronous and asynchronous transactions.
46  *
47  * Major TODOs
48  * - TODO Implement callbacks
49  */
50 class UIControllerOnePane extends UIControllerBase {
51     private static final String BUNDLE_KEY_PREVIOUS_FRAGMENT
52             = "UIControllerOnePane.PREVIOUS_FRAGMENT";
53 
54     // Our custom poor-man's back stack which has only one entry at maximum.
55     private Fragment mPreviousFragment;
56 
57     // MailboxListFragment.Callback
58     @Override
onAccountSelected(long accountId)59     public void onAccountSelected(long accountId) {
60         // It's from combined view, so "forceShowInbox" doesn't really matter.
61         // (We're always switching accounts.)
62         switchAccount(accountId, true);
63     }
64 
65     // MailboxListFragment.Callback
66     @Override
onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation)67     public void onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation) {
68         if (nestedNavigation) {
69             return; // Nothing to do on 1-pane.
70         }
71         openMailbox(accountId, mailboxId);
72     }
73 
74     // MailboxListFragment.Callback
75     @Override
onParentMailboxChanged()76     public void onParentMailboxChanged() {
77         refreshActionBar();
78     }
79 
80     // MessageListFragment.Callback
81     @Override
onAdvancingOpAccepted(Set<Long> affectedMessages)82     public void onAdvancingOpAccepted(Set<Long> affectedMessages) {
83         // Nothing to do on 1 pane.
84     }
85 
86     // MessageListFragment.Callback
87     @Override
onMessageOpen( long messageId, long messageMailboxId, long listMailboxId, int type)88     public void onMessageOpen(
89             long messageId, long messageMailboxId, long listMailboxId, int type) {
90         if (type == MessageListFragment.Callback.TYPE_DRAFT) {
91             MessageCompose.actionEditDraft(mActivity, messageId);
92         } else {
93             open(mListContext, messageId);
94         }
95     }
96 
97     // MessageListFragment.Callback
98     @Override
onDragStarted()99     public boolean onDragStarted() {
100         // No drag&drop on 1-pane
101         return false;
102     }
103 
104     // MessageListFragment.Callback
105     @Override
onDragEnded()106     public void onDragEnded() {
107         // No drag&drop on 1-pane
108     }
109 
110     // MessageViewFragment.Callback
111     @Override
onForward()112     public void onForward() {
113         MessageCompose.actionForward(mActivity, getMessageId());
114     }
115 
116     // MessageViewFragment.Callback
117     @Override
onReply()118     public void onReply() {
119         MessageCompose.actionReply(mActivity, getMessageId(), false);
120     }
121 
122     // MessageViewFragment.Callback
123     @Override
onReplyAll()124     public void onReplyAll() {
125         MessageCompose.actionReply(mActivity, getMessageId(), true);
126     }
127 
128     // MessageViewFragment.Callback
129     @Override
onCalendarLinkClicked(long epochEventStartTime)130     public void onCalendarLinkClicked(long epochEventStartTime) {
131         ActivityHelper.openCalendar(mActivity, epochEventStartTime);
132     }
133 
134     // MessageViewFragment.Callback
135     @Override
onUrlInMessageClicked(String url)136     public boolean onUrlInMessageClicked(String url) {
137         return ActivityHelper.openUrlInMessage(mActivity, url, getActualAccountId());
138     }
139 
140     // MessageViewFragment.Callback
141     @Override
onLoadMessageError(String errorMessage)142     public void onLoadMessageError(String errorMessage) {
143         // TODO Auto-generated method stub
144     }
145 
146     // MessageViewFragment.Callback
147     @Override
onLoadMessageFinished()148     public void onLoadMessageFinished() {
149         // TODO Auto-generated method stub
150     }
151 
152     // MessageViewFragment.Callback
153     @Override
onLoadMessageStarted()154     public void onLoadMessageStarted() {
155         // TODO Auto-generated method stub
156     }
157 
isInboxShown()158     private boolean isInboxShown() {
159         if (!isMessageListInstalled()) {
160             return false;
161         }
162         return getMessageListFragment().isInboxList();
163     }
164 
165     // This is all temporary as we'll have a different action bar controller for 1-pane.
166     private class ActionBarControllerCallback implements ActionBarController.Callback {
167         @Override
getTitleMode()168         public int getTitleMode() {
169             if (isMailboxListInstalled()) {
170                 return TITLE_MODE_ACCOUNT_WITH_ALL_FOLDERS_LABEL;
171             }
172             if (isMessageViewInstalled()) {
173                 return TITLE_MODE_MESSAGE_SUBJECT;
174             }
175             return TITLE_MODE_ACCOUNT_WITH_MAILBOX;
176         }
177 
getMessageSubject()178         public String getMessageSubject() {
179             if (isMessageViewInstalled() && getMessageViewFragment().isMessageOpen()) {
180                 return getMessageViewFragment().getMessage().mSubject;
181             } else {
182                 return null;
183             }
184         }
185 
186         @Override
shouldShowUp()187         public boolean shouldShowUp() {
188             return isMessageViewInstalled()
189                     || (isMessageListInstalled() && !isInboxShown())
190                     || isMailboxListInstalled();
191         }
192 
193         @Override
getUIAccountId()194         public long getUIAccountId() {
195             return UIControllerOnePane.this.getUIAccountId();
196         }
197 
198         @Override
getMailboxId()199         public long getMailboxId() {
200             return UIControllerOnePane.this.getMailboxId();
201         }
202 
203         @Override
onMailboxSelected(long accountId, long mailboxId)204         public void onMailboxSelected(long accountId, long mailboxId) {
205             if (mailboxId == Mailbox.NO_MAILBOX) {
206                 showAllMailboxes();
207             } else {
208                 openMailbox(accountId, mailboxId);
209             }
210         }
211 
212         @Override
isAccountSelected()213         public boolean isAccountSelected() {
214             return UIControllerOnePane.this.isAccountSelected();
215         }
216 
217         @Override
onAccountSelected(long accountId)218         public void onAccountSelected(long accountId) {
219             switchAccount(accountId, true); // Always go to inbox
220         }
221 
222         @Override
onNoAccountsFound()223         public void onNoAccountsFound() {
224             Welcome.actionStart(mActivity);
225             mActivity.finish();
226         }
227 
228         @Override
getSearchHint()229         public String getSearchHint() {
230             if (!isMessageListInstalled()) {
231                 return null;
232             }
233             return UIControllerOnePane.this.getSearchHint();
234         }
235 
236         @Override
onSearchStarted()237         public void onSearchStarted() {
238             if (!isMessageListInstalled()) {
239                 return;
240             }
241             UIControllerOnePane.this.onSearchStarted();
242         }
243 
244         @Override
onSearchSubmit(String queryTerm)245         public void onSearchSubmit(String queryTerm) {
246             if (!isMessageListInstalled()) {
247                 return;
248             }
249             UIControllerOnePane.this.onSearchSubmit(queryTerm);
250         }
251 
252         @Override
onSearchExit()253         public void onSearchExit() {
254             UIControllerOnePane.this.onSearchExit();
255         }
256 
257         @Override
onUpPressed()258         public void onUpPressed() {
259             onBackPressed(false);
260         }
261     }
262 
UIControllerOnePane(EmailActivity activity)263     public UIControllerOnePane(EmailActivity activity) {
264         super(activity);
265     }
266 
267     @Override
createActionBarController(Activity activity)268     protected ActionBarController createActionBarController(Activity activity) {
269 
270         // For now, we just reuse the same action bar controller used for 2-pane.
271         // We may change it later.
272 
273         return new ActionBarController(activity, activity.getLoaderManager(),
274                 activity.getActionBar(), new ActionBarControllerCallback());
275     }
276 
277     @Override
onSaveInstanceState(Bundle outState)278     public void onSaveInstanceState(Bundle outState) {
279         super.onSaveInstanceState(outState);
280         if (mPreviousFragment != null) {
281             mFragmentManager.putFragment(outState,
282                     BUNDLE_KEY_PREVIOUS_FRAGMENT, mPreviousFragment);
283         }
284     }
285 
286     @Override
onRestoreInstanceState(Bundle savedInstanceState)287     public void onRestoreInstanceState(Bundle savedInstanceState) {
288         super.onRestoreInstanceState(savedInstanceState);
289         mPreviousFragment = mFragmentManager.getFragment(savedInstanceState,
290                 BUNDLE_KEY_PREVIOUS_FRAGMENT);
291     }
292 
293     @Override
getLayoutId()294     public int getLayoutId() {
295         return R.layout.email_activity_one_pane;
296     }
297 
298     @Override
getUIAccountId()299     public long getUIAccountId() {
300         if (mListContext != null) {
301             return mListContext.mAccountId;
302         }
303         if (isMailboxListInstalled()) {
304             return getMailboxListFragment().getAccountId();
305         }
306         return Account.NO_ACCOUNT;
307     }
308 
getMailboxId()309     private long getMailboxId() {
310         if (mListContext != null) {
311             return mListContext.getMailboxId();
312         }
313         return Mailbox.NO_MAILBOX;
314     }
315 
316     @Override
onBackPressed(boolean isSystemBackKey)317     public boolean onBackPressed(boolean isSystemBackKey) {
318         if (Email.DEBUG) {
319             // This is VERY important -- no check for DEBUG_LIFECYCLE
320             Log.d(Logging.LOG_TAG, this + " onBackPressed: " + isSystemBackKey);
321         }
322         // The action bar controller has precedence.  Must call it first.
323         if (mActionBarController.onBackPressed(isSystemBackKey)) {
324             return true;
325         }
326         // If the mailbox list is shown and showing a nested mailbox, let it navigate up first.
327         if (isMailboxListInstalled() && getMailboxListFragment().navigateUp()) {
328             if (DEBUG_FRAGMENTS) {
329                 Log.d(Logging.LOG_TAG, this + " Back: back handled by mailbox list");
330             }
331             return true;
332         }
333 
334         // Custom back stack
335         if (shouldPopFromBackStack(isSystemBackKey)) {
336             if (DEBUG_FRAGMENTS) {
337                 Log.d(Logging.LOG_TAG, this + " Back: Popping from back stack");
338             }
339             popFromBackStack();
340             return true;
341         }
342 
343         // No entry in the back stack.
344         if (isMessageViewInstalled()) {
345             if (DEBUG_FRAGMENTS) {
346                 Log.d(Logging.LOG_TAG, this + " Back: Message view -> Message List");
347             }
348             // If the message view is shown, show the "parent" message list.
349             // This happens when we get a deep link to a message.  (e.g. from a widget)
350             openMailbox(mListContext.mAccountId, mListContext.getMailboxId());
351             return true;
352         } else if (isMailboxListInstalled()) {
353             // If the mailbox list is shown, always go back to the inbox.
354             switchAccount(getMailboxListFragment().getAccountId(), true /* force show inbox */);
355             return true;
356         } else if (isMessageListInstalled() && !isInboxShown()) {
357             // Non-inbox list. Go to inbox.
358             switchAccount(mListContext.mAccountId, true /* force show inbox */);
359             return true;
360         }
361         return false;
362     }
363 
364     @Override
openInternal(final MessageListContext listContext, final long messageId)365     public void openInternal(final MessageListContext listContext, final long messageId) {
366         if (Email.DEBUG) {
367             // This is VERY important -- don't check for DEBUG_LIFECYCLE
368             Log.i(Logging.LOG_TAG, this + " open " + listContext + " messageId=" + messageId);
369         }
370 
371         if (messageId != Message.NO_MESSAGE) {
372             openMessage(messageId);
373         } else {
374             showFragment(MessageListFragment.newInstance(listContext));
375         }
376     }
377 
378     /**
379      * @return currently installed {@link Fragment} (1-pane has only one at most), or null if none
380      *         exists.
381      */
getInstalledFragment()382     private Fragment getInstalledFragment() {
383         if (isMailboxListInstalled()) {
384             return getMailboxListFragment();
385         } else if (isMessageListInstalled()) {
386             return getMessageListFragment();
387         } else if (isMessageViewInstalled()) {
388             return getMessageViewFragment();
389         }
390         return null;
391     }
392 
393     /**
394      * Show the mailbox list.
395      *
396      * This is the only way to open the mailbox list on 1-pane.
397      * {@link #open(MessageListContext, long)} will only open either the message list or the
398      * message view.
399      */
openMailboxList(long accountId)400     private void openMailboxList(long accountId) {
401         setListContext(null);
402         showFragment(MailboxListFragment.newInstance(accountId, Mailbox.NO_MAILBOX, false));
403     }
404 
openMessage(long messageId)405     private void openMessage(long messageId) {
406         showFragment(MessageViewFragment.newInstance(messageId));
407     }
408 
409     /**
410      * Push the installed fragment into our custom back stack (or optionally
411      * {@link FragmentTransaction#remove} it) and {@link FragmentTransaction#add} {@code fragment}.
412      *
413      * @param fragment {@link Fragment} to be added.
414      *
415      *  TODO Delay-call the whole method and use the synchronous transaction.
416      */
showFragment(Fragment fragment)417     private void showFragment(Fragment fragment) {
418         final FragmentTransaction ft = mFragmentManager.beginTransaction();
419         final Fragment installed = getInstalledFragment();
420         if ((installed instanceof MessageViewFragment)
421                 && (fragment instanceof MessageViewFragment)) {
422             // Newer/older navigation, auto-advance, etc.
423             // In this case we want to keep the backstack untouched, so that after back navigation
424             // we can restore the message list, including scroll position and batch selection.
425         } else {
426             if (DEBUG_FRAGMENTS) {
427                 Log.i(Logging.LOG_TAG, this + " backstack: [push] " + getInstalledFragment()
428                         + " -> " + fragment);
429             }
430             if (mPreviousFragment != null) {
431                 if (DEBUG_FRAGMENTS) {
432                     Log.d(Logging.LOG_TAG, this + " showFragment: destroying previous fragment "
433                             + mPreviousFragment);
434                 }
435                 removeFragment(ft, mPreviousFragment);
436                 mPreviousFragment = null;
437             }
438             // Remove the current fragment or push it into the backstack.
439             if (installed != null) {
440                 if (installed instanceof MessageViewFragment) {
441                     // Message view should never be pushed to the backstack.
442                     if (DEBUG_FRAGMENTS) {
443                         Log.d(Logging.LOG_TAG, this + " showFragment: removing " + installed);
444                     }
445                     ft.remove(installed);
446                 } else {
447                     // Other fragments should be pushed.
448                     mPreviousFragment = installed;
449                     if (DEBUG_FRAGMENTS) {
450                         Log.d(Logging.LOG_TAG, this + " showFragment: detaching "
451                                 + mPreviousFragment);
452                     }
453                     ft.detach(mPreviousFragment);
454                 }
455             }
456         }
457         // Show the new one
458         if (DEBUG_FRAGMENTS) {
459             Log.d(Logging.LOG_TAG, this + " showFragment: replacing with " + fragment);
460         }
461         ft.replace(R.id.fragment_placeholder, fragment);
462         ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
463         commitFragmentTransaction(ft);
464     }
465 
466     /**
467      * @param isSystemBackKey <code>true</code> if the system back key was pressed.
468      *        <code>false</code> if it's caused by the "home" icon click on the action bar.
469      * @return true if we should pop from our custom back stack.
470      */
shouldPopFromBackStack(boolean isSystemBackKey)471     private boolean shouldPopFromBackStack(boolean isSystemBackKey) {
472         if (mPreviousFragment == null) {
473             return false; // Nothing in the back stack
474         }
475         if (mPreviousFragment instanceof MessageViewFragment) {
476             throw new IllegalStateException("Message view should never be in backstack");
477         }
478         final Fragment installed = getInstalledFragment();
479         if (installed == null) {
480             // If no fragment is installed right now, do nothing.
481             return false;
482         }
483 
484         // Okay now we have 2 fragments; the one in the back stack and the one that's currently
485         // installed.
486         if (isInboxShown()) {
487             // Inbox is the top level list - never go back from here.
488             return false;
489         }
490 
491         // Disallow the MailboxList--> non-inbox MessageList transition as the Mailbox list
492         // is always considered "higher" than a non-inbox MessageList
493         if ((mPreviousFragment instanceof MessageListFragment)
494                 && (!((MessageListFragment) mPreviousFragment).isInboxList())
495                 && (installed  instanceof MailboxListFragment)) {
496             return false;
497         }
498         return true;
499     }
500 
501     /**
502      * Pop from our custom back stack.
503      *
504      * TODO Delay-call the whole method and use the synchronous transaction.
505      */
popFromBackStack()506     private void popFromBackStack() {
507         if (mPreviousFragment == null) {
508             return;
509         }
510         final FragmentTransaction ft = mFragmentManager.beginTransaction();
511         final Fragment installed = getInstalledFragment();
512         if (DEBUG_FRAGMENTS) {
513             Log.i(Logging.LOG_TAG, this + " backstack: [pop] " + installed + " -> "
514                     + mPreviousFragment);
515         }
516         removeFragment(ft, installed);
517 
518         // Restore listContext.
519         if (mPreviousFragment instanceof MailboxListFragment) {
520             setListContext(null);
521         } else if (mPreviousFragment instanceof MessageListFragment) {
522             setListContext(((MessageListFragment) mPreviousFragment).getListContext());
523         } else {
524             throw new IllegalStateException("Message view should never be in backstack");
525         }
526 
527         ft.attach(mPreviousFragment);
528         ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE);
529         mPreviousFragment = null;
530         commitFragmentTransaction(ft);
531         return;
532     }
533 
showAllMailboxes()534     private void showAllMailboxes() {
535         if (!isAccountSelected()) {
536             return; // Can happen because of asynchronous fragment transactions.
537         }
538 
539         openMailboxList(getUIAccountId());
540     }
541 
542     @Override
installMailboxListFragment(MailboxListFragment fragment)543     protected void installMailboxListFragment(MailboxListFragment fragment) {
544         stopMessageOrderManager();
545         super.installMailboxListFragment(fragment);
546     }
547 
548     @Override
installMessageListFragment(MessageListFragment fragment)549     protected void installMessageListFragment(MessageListFragment fragment) {
550         stopMessageOrderManager();
551         super.installMessageListFragment(fragment);
552     }
553 
554     @Override
getMailboxSettingsMailboxId()555     protected long getMailboxSettingsMailboxId() {
556         return isMessageListInstalled()
557                 ? getMessageListFragment().getMailboxId()
558                 : Mailbox.NO_MAILBOX;
559     }
560 
561     /**
562      * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback.
563      */
onCreateOptionsMenu(MenuInflater inflater, Menu menu)564     public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) {
565         if (isMessageListInstalled()) {
566             inflater.inflate(R.menu.message_list_fragment_option, menu);
567             return true;
568         }
569         if (isMessageViewInstalled()) {
570             inflater.inflate(R.menu.message_view_fragment_option, menu);
571             return true;
572         }
573         return false;
574     }
575 
576     @Override
onPrepareOptionsMenu(MenuInflater inflater, Menu menu)577     public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) {
578         // First, let the base class do what it has to do.
579         super.onPrepareOptionsMenu(inflater, menu);
580 
581         final boolean messageViewVisible = isMessageViewInstalled();
582         if (messageViewVisible) {
583             final MessageOrderManager om = getMessageOrderManager();
584             menu.findItem(R.id.newer).setVisible(true);
585             menu.findItem(R.id.older).setVisible(true);
586             // orderManager shouldn't be null when the message view is installed, but just in case..
587             menu.findItem(R.id.newer).setEnabled((om != null) && om.canMoveToNewer());
588             menu.findItem(R.id.older).setEnabled((om != null) && om.canMoveToOlder());
589         }
590         return true;
591     }
592 
593     @Override
onOptionsItemSelected(MenuItem item)594     public boolean onOptionsItemSelected(MenuItem item) {
595         switch (item.getItemId()) {
596             case R.id.newer:
597                 moveToNewer();
598                 return true;
599             case R.id.older:
600                 moveToOlder();
601                 return true;
602             case R.id.show_all_mailboxes:
603                 showAllMailboxes();
604                 return true;
605         }
606         return super.onOptionsItemSelected(item);
607     }
608 
609     @Override
isRefreshEnabled()610     protected boolean isRefreshEnabled() {
611         // Refreshable only when an actual account is selected, and message view isn't shown.
612         // (i.e. only available on the mailbox list or the message view, but not on the combined
613         // one)
614         if (!isActualAccountSelected() || isMessageViewInstalled()) {
615             return false;
616         }
617         return isMailboxListInstalled() || (mListContext.getMailboxId() > 0);
618     }
619 
620     @Override
onRefresh()621     protected void onRefresh() {
622         if (!isRefreshEnabled()) {
623             return;
624         }
625         if (isMessageListInstalled()) {
626             mRefreshManager.refreshMessageList(getActualAccountId(), getMailboxId(), true);
627         } else {
628             mRefreshManager.refreshMailboxList(getActualAccountId());
629         }
630     }
631 
632     @Override
isRefreshInProgress()633     protected boolean isRefreshInProgress() {
634         if (!isRefreshEnabled()) {
635             return false;
636         }
637         if (isMessageListInstalled()) {
638             return mRefreshManager.isMessageListRefreshing(getMailboxId());
639         } else {
640             return mRefreshManager.isMailboxListRefreshing(getActualAccountId());
641         }
642     }
643 
navigateToMessage(long messageId)644     @Override protected void navigateToMessage(long messageId) {
645         openMessage(messageId);
646     }
647 
updateNavigationArrows()648     @Override protected void updateNavigationArrows() {
649         refreshActionBar();
650     }
651 }
652