• 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 
UIControllerOnePane(EmailActivity activity)258     public UIControllerOnePane(EmailActivity activity) {
259         super(activity);
260     }
261 
262     @Override
createActionBarController(Activity activity)263     protected ActionBarController createActionBarController(Activity activity) {
264 
265         // For now, we just reuse the same action bar controller used for 2-pane.
266         // We may change it later.
267 
268         return new ActionBarController(activity, activity.getLoaderManager(),
269                 activity.getActionBar(), new ActionBarControllerCallback());
270     }
271 
272     @Override
onSaveInstanceState(Bundle outState)273     public void onSaveInstanceState(Bundle outState) {
274         super.onSaveInstanceState(outState);
275         if (mPreviousFragment != null) {
276             mFragmentManager.putFragment(outState,
277                     BUNDLE_KEY_PREVIOUS_FRAGMENT, mPreviousFragment);
278         }
279     }
280 
281     @Override
onRestoreInstanceState(Bundle savedInstanceState)282     public void onRestoreInstanceState(Bundle savedInstanceState) {
283         super.onRestoreInstanceState(savedInstanceState);
284         mPreviousFragment = mFragmentManager.getFragment(savedInstanceState,
285                 BUNDLE_KEY_PREVIOUS_FRAGMENT);
286     }
287 
288     @Override
getLayoutId()289     public int getLayoutId() {
290         return R.layout.email_activity_one_pane;
291     }
292 
293     @Override
getUIAccountId()294     public long getUIAccountId() {
295         if (mListContext != null) {
296             return mListContext.mAccountId;
297         }
298         if (isMailboxListInstalled()) {
299             return getMailboxListFragment().getAccountId();
300         }
301         return Account.NO_ACCOUNT;
302     }
303 
getMailboxId()304     private long getMailboxId() {
305         if (mListContext != null) {
306             return mListContext.getMailboxId();
307         }
308         return Mailbox.NO_MAILBOX;
309     }
310 
311     @Override
onBackPressed(boolean isSystemBackKey)312     public boolean onBackPressed(boolean isSystemBackKey) {
313         if (Email.DEBUG) {
314             // This is VERY important -- no check for DEBUG_LIFECYCLE
315             Log.d(Logging.LOG_TAG, this + " onBackPressed: " + isSystemBackKey);
316         }
317         // The action bar controller has precedence.  Must call it first.
318         if (mActionBarController.onBackPressed(isSystemBackKey)) {
319             return true;
320         }
321         // If the mailbox list is shown and showing a nested mailbox, let it navigate up first.
322         if (isMailboxListInstalled() && getMailboxListFragment().navigateUp()) {
323             if (DEBUG_FRAGMENTS) {
324                 Log.d(Logging.LOG_TAG, this + " Back: back handled by mailbox list");
325             }
326             return true;
327         }
328 
329         // Custom back stack
330         if (shouldPopFromBackStack(isSystemBackKey)) {
331             if (DEBUG_FRAGMENTS) {
332                 Log.d(Logging.LOG_TAG, this + " Back: Popping from back stack");
333             }
334             popFromBackStack();
335             return true;
336         }
337 
338         // No entry in the back stack.
339         if (isMessageViewInstalled()) {
340             if (DEBUG_FRAGMENTS) {
341                 Log.d(Logging.LOG_TAG, this + " Back: Message view -> Message List");
342             }
343             // If the message view is shown, show the "parent" message list.
344             // This happens when we get a deep link to a message.  (e.g. from a widget)
345             openMailbox(mListContext.mAccountId, mListContext.getMailboxId());
346             return true;
347         } else if (isMailboxListInstalled()) {
348             // If the mailbox list is shown, always go back to the inbox.
349             switchAccount(getMailboxListFragment().getAccountId(), true /* force show inbox */);
350             return true;
351         } else if (isMessageListInstalled() && !isInboxShown()) {
352             // Non-inbox list. Go to inbox.
353             switchAccount(mListContext.mAccountId, true /* force show inbox */);
354             return true;
355         }
356         return false;
357     }
358 
359     @Override
openInternal(final MessageListContext listContext, final long messageId)360     public void openInternal(final MessageListContext listContext, final long messageId) {
361         if (Email.DEBUG) {
362             // This is VERY important -- don't check for DEBUG_LIFECYCLE
363             Log.i(Logging.LOG_TAG, this + " open " + listContext + " messageId=" + messageId);
364         }
365 
366         if (messageId != Message.NO_MESSAGE) {
367             openMessage(messageId);
368         } else {
369             showFragment(MessageListFragment.newInstance(listContext));
370         }
371     }
372 
373     /**
374      * @return currently installed {@link Fragment} (1-pane has only one at most), or null if none
375      *         exists.
376      */
getInstalledFragment()377     private Fragment getInstalledFragment() {
378         if (isMailboxListInstalled()) {
379             return getMailboxListFragment();
380         } else if (isMessageListInstalled()) {
381             return getMessageListFragment();
382         } else if (isMessageViewInstalled()) {
383             return getMessageViewFragment();
384         }
385         return null;
386     }
387 
388     /**
389      * Show the mailbox list.
390      *
391      * This is the only way to open the mailbox list on 1-pane.
392      * {@link #open(MessageListContext, long)} will only open either the message list or the
393      * message view.
394      */
openMailboxList(long accountId)395     private void openMailboxList(long accountId) {
396         setListContext(null);
397         showFragment(MailboxListFragment.newInstance(accountId, Mailbox.NO_MAILBOX, false));
398     }
399 
openMessage(long messageId)400     private void openMessage(long messageId) {
401         showFragment(MessageViewFragment.newInstance(messageId));
402     }
403 
404     /**
405      * Push the installed fragment into our custom back stack (or optionally
406      * {@link FragmentTransaction#remove} it) and {@link FragmentTransaction#add} {@code fragment}.
407      *
408      * @param fragment {@link Fragment} to be added.
409      *
410      *  TODO Delay-call the whole method and use the synchronous transaction.
411      */
showFragment(Fragment fragment)412     private void showFragment(Fragment fragment) {
413         final FragmentTransaction ft = mFragmentManager.beginTransaction();
414         final Fragment installed = getInstalledFragment();
415         if ((installed instanceof MessageViewFragment)
416                 && (fragment instanceof MessageViewFragment)) {
417             // Newer/older navigation, auto-advance, etc.
418             // In this case we want to keep the backstack untouched, so that after back navigation
419             // we can restore the message list, including scroll position and batch selection.
420         } else {
421             if (DEBUG_FRAGMENTS) {
422                 Log.i(Logging.LOG_TAG, this + " backstack: [push] " + getInstalledFragment()
423                         + " -> " + fragment);
424             }
425             if (mPreviousFragment != null) {
426                 if (DEBUG_FRAGMENTS) {
427                     Log.d(Logging.LOG_TAG, this + " showFragment: destroying previous fragment "
428                             + mPreviousFragment);
429                 }
430                 removeFragment(ft, mPreviousFragment);
431                 mPreviousFragment = null;
432             }
433             // Remove the current fragment or push it into the backstack.
434             if (installed != null) {
435                 if (installed instanceof MessageViewFragment) {
436                     // Message view should never be pushed to the backstack.
437                     if (DEBUG_FRAGMENTS) {
438                         Log.d(Logging.LOG_TAG, this + " showFragment: removing " + installed);
439                     }
440                     ft.remove(installed);
441                 } else {
442                     // Other fragments should be pushed.
443                     mPreviousFragment = installed;
444                     if (DEBUG_FRAGMENTS) {
445                         Log.d(Logging.LOG_TAG, this + " showFragment: detaching "
446                                 + mPreviousFragment);
447                     }
448                     ft.detach(mPreviousFragment);
449                 }
450             }
451         }
452         // Show the new one
453         if (DEBUG_FRAGMENTS) {
454             Log.d(Logging.LOG_TAG, this + " showFragment: replacing with " + fragment);
455         }
456         ft.replace(R.id.fragment_placeholder, fragment);
457         ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
458         commitFragmentTransaction(ft);
459     }
460 
461     /**
462      * @param isSystemBackKey <code>true</code> if the system back key was pressed.
463      *        <code>false</code> if it's caused by the "home" icon click on the action bar.
464      * @return true if we should pop from our custom back stack.
465      */
shouldPopFromBackStack(boolean isSystemBackKey)466     private boolean shouldPopFromBackStack(boolean isSystemBackKey) {
467         if (mPreviousFragment == null) {
468             return false; // Nothing in the back stack
469         }
470         if (mPreviousFragment instanceof MessageViewFragment) {
471             throw new IllegalStateException("Message view should never be in backstack");
472         }
473         final Fragment installed = getInstalledFragment();
474         if (installed == null) {
475             // If no fragment is installed right now, do nothing.
476             return false;
477         }
478 
479         // Okay now we have 2 fragments; the one in the back stack and the one that's currently
480         // installed.
481         if (isInboxShown()) {
482             // Inbox is the top level list - never go back from here.
483             return false;
484         }
485 
486         // Disallow the MailboxList--> non-inbox MessageList transition as the Mailbox list
487         // is always considered "higher" than a non-inbox MessageList
488         if ((mPreviousFragment instanceof MessageListFragment)
489                 && (!((MessageListFragment) mPreviousFragment).isInboxList())
490                 && (installed  instanceof MailboxListFragment)) {
491             return false;
492         }
493         return true;
494     }
495 
496     /**
497      * Pop from our custom back stack.
498      *
499      * TODO Delay-call the whole method and use the synchronous transaction.
500      */
popFromBackStack()501     private void popFromBackStack() {
502         if (mPreviousFragment == null) {
503             return;
504         }
505         final FragmentTransaction ft = mFragmentManager.beginTransaction();
506         final Fragment installed = getInstalledFragment();
507         if (DEBUG_FRAGMENTS) {
508             Log.i(Logging.LOG_TAG, this + " backstack: [pop] " + installed + " -> "
509                     + mPreviousFragment);
510         }
511         removeFragment(ft, installed);
512 
513         // Restore listContext.
514         if (mPreviousFragment instanceof MailboxListFragment) {
515             setListContext(null);
516         } else if (mPreviousFragment instanceof MessageListFragment) {
517             setListContext(((MessageListFragment) mPreviousFragment).getListContext());
518         } else {
519             throw new IllegalStateException("Message view should never be in backstack");
520         }
521 
522         ft.attach(mPreviousFragment);
523         ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE);
524         mPreviousFragment = null;
525         commitFragmentTransaction(ft);
526         return;
527     }
528 
showAllMailboxes()529     private void showAllMailboxes() {
530         if (!isAccountSelected()) {
531             return; // Can happen because of asynchronous fragment transactions.
532         }
533 
534         openMailboxList(getUIAccountId());
535     }
536 
537     @Override
installMailboxListFragment(MailboxListFragment fragment)538     protected void installMailboxListFragment(MailboxListFragment fragment) {
539         stopMessageOrderManager();
540         super.installMailboxListFragment(fragment);
541     }
542 
543     @Override
installMessageListFragment(MessageListFragment fragment)544     protected void installMessageListFragment(MessageListFragment fragment) {
545         stopMessageOrderManager();
546         super.installMessageListFragment(fragment);
547     }
548 
549     @Override
getMailboxSettingsMailboxId()550     protected long getMailboxSettingsMailboxId() {
551         return isMessageListInstalled()
552                 ? getMessageListFragment().getMailboxId()
553                 : Mailbox.NO_MAILBOX;
554     }
555 
556     /**
557      * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback.
558      */
onCreateOptionsMenu(MenuInflater inflater, Menu menu)559     public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) {
560         if (isMessageListInstalled()) {
561             inflater.inflate(R.menu.message_list_fragment_option, menu);
562             return true;
563         }
564         if (isMessageViewInstalled()) {
565             inflater.inflate(R.menu.message_view_fragment_option, menu);
566             return true;
567         }
568         return false;
569     }
570 
571     @Override
onPrepareOptionsMenu(MenuInflater inflater, Menu menu)572     public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) {
573         // First, let the base class do what it has to do.
574         super.onPrepareOptionsMenu(inflater, menu);
575 
576         final boolean messageViewVisible = isMessageViewInstalled();
577         if (messageViewVisible) {
578             final MessageOrderManager om = getMessageOrderManager();
579             menu.findItem(R.id.newer).setVisible(true);
580             menu.findItem(R.id.older).setVisible(true);
581             // orderManager shouldn't be null when the message view is installed, but just in case..
582             menu.findItem(R.id.newer).setEnabled((om != null) && om.canMoveToNewer());
583             menu.findItem(R.id.older).setEnabled((om != null) && om.canMoveToOlder());
584         }
585         return true;
586     }
587 
588     @Override
onOptionsItemSelected(MenuItem item)589     public boolean onOptionsItemSelected(MenuItem item) {
590         switch (item.getItemId()) {
591             case R.id.newer:
592                 moveToNewer();
593                 return true;
594             case R.id.older:
595                 moveToOlder();
596                 return true;
597             case R.id.show_all_mailboxes:
598                 showAllMailboxes();
599                 return true;
600         }
601         return super.onOptionsItemSelected(item);
602     }
603 
604     @Override
isRefreshEnabled()605     protected boolean isRefreshEnabled() {
606         // Refreshable only when an actual account is selected, and message view isn't shown.
607         // (i.e. only available on the mailbox list or the message view, but not on the combined
608         // one)
609         if (!isActualAccountSelected() || isMessageViewInstalled()) {
610             return false;
611         }
612         return isMailboxListInstalled() || (mListContext.getMailboxId() > 0);
613     }
614 
615     @Override
onRefresh()616     protected void onRefresh() {
617         if (!isRefreshEnabled()) {
618             return;
619         }
620         if (isMessageListInstalled()) {
621             mRefreshManager.refreshMessageList(getActualAccountId(), getMailboxId(), true);
622         } else {
623             mRefreshManager.refreshMailboxList(getActualAccountId());
624         }
625     }
626 
627     @Override
isRefreshInProgress()628     protected boolean isRefreshInProgress() {
629         if (!isRefreshEnabled()) {
630             return false;
631         }
632         if (isMessageListInstalled()) {
633             return mRefreshManager.isMessageListRefreshing(getMailboxId());
634         } else {
635             return mRefreshManager.isMailboxListRefreshing(getActualAccountId());
636         }
637     }
638 
navigateToMessage(long messageId)639     @Override protected void navigateToMessage(long messageId) {
640         openMessage(messageId);
641     }
642 
updateNavigationArrows()643     @Override protected void updateNavigationArrows() {
644         refreshActionBar();
645     }
646 }
647