• 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.FragmentManager;
22 import android.app.FragmentTransaction;
23 import android.os.Bundle;
24 import android.util.Log;
25 import android.view.Menu;
26 import android.view.MenuInflater;
27 import android.view.MenuItem;
28 
29 import com.android.email.Email;
30 import com.android.email.FolderProperties;
31 import com.android.email.MessageListContext;
32 import com.android.email.Preferences;
33 import com.android.email.R;
34 import com.android.email.RefreshManager;
35 import com.android.email.RequireManualSyncDialog;
36 import com.android.email.activity.setup.AccountSettings;
37 import com.android.email.activity.setup.MailboxSettings;
38 import com.android.emailcommon.Logging;
39 import com.android.emailcommon.provider.Account;
40 import com.android.emailcommon.provider.EmailContent.Message;
41 import com.android.emailcommon.provider.HostAuth;
42 import com.android.emailcommon.provider.Mailbox;
43 import com.android.emailcommon.utility.EmailAsyncTask;
44 import com.android.emailcommon.utility.Utility;
45 import com.google.common.base.Objects;
46 import com.google.common.base.Preconditions;
47 
48 import java.util.LinkedList;
49 import java.util.List;
50 
51 /**
52  * Base class for the UI controller.
53  */
54 abstract class UIControllerBase implements MailboxListFragment.Callback,
55         MessageListFragment.Callback, MessageViewFragment.Callback  {
56     static final boolean DEBUG_FRAGMENTS = false; // DO NOT SUBMIT WITH TRUE
57 
58     static final String KEY_LIST_CONTEXT = "UIControllerBase.listContext";
59 
60     /** The owner activity */
61     final EmailActivity mActivity;
62     final FragmentManager mFragmentManager;
63 
64     protected final ActionBarController mActionBarController;
65 
66     private MessageOrderManager mOrderManager;
67     private final MessageOrderManagerCallback mMessageOrderManagerCallback =
68             new MessageOrderManagerCallback();
69 
70     final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
71 
72     final RefreshManager mRefreshManager;
73 
74     /**
75      * Fragments that are installed.
76      *
77      * A fragment is installed in {@link Fragment#onActivityCreated} and uninstalled in
78      * {@link Fragment#onDestroyView}, using {@link FragmentInstallable} callbacks.
79      *
80      * This means fragments in the back stack are *not* installed.
81      *
82      * We set callbacks to fragments only when they are installed.
83      *
84      * @see FragmentInstallable
85      */
86     private MailboxListFragment mMailboxListFragment;
87     private MessageListFragment mMessageListFragment;
88     private MessageViewFragment mMessageViewFragment;
89 
90     /**
91      * To avoid double-deleting a fragment (which will cause a runtime exception),
92      * we put a fragment in this list when we {@link FragmentTransaction#remove(Fragment)} it,
93      * and remove from the list when we actually uninstall it.
94      */
95     private final List<Fragment> mRemovedFragments = new LinkedList<Fragment>();
96 
97     /**
98      * The NfcHandler implements Near Field Communication sharing features
99      * whenever the activity is in the foreground.
100      */
101     private NfcHandler mNfcHandler;
102 
103     /**
104      * The active context for the current MessageList.
105      * In some UI layouts such as the one-pane view, the message list may not be visible, but is
106      * on the backstack. This list context will still be accessible in those cases.
107      *
108      * Should be set using {@link #setListContext(MessageListContext)}.
109      */
110     protected MessageListContext mListContext;
111 
112     private class RefreshListener implements RefreshManager.Listener {
113         private MenuItem mRefreshIcon;
114 
115         @Override
onMessagingError(final long accountId, long mailboxId, final String message)116         public void onMessagingError(final long accountId, long mailboxId, final String message) {
117             updateRefreshIcon();
118         }
119 
120         @Override
onRefreshStatusChanged(long accountId, long mailboxId)121         public void onRefreshStatusChanged(long accountId, long mailboxId) {
122             updateRefreshIcon();
123         }
124 
setRefreshIcon(MenuItem icon)125         void setRefreshIcon(MenuItem icon) {
126             mRefreshIcon = icon;
127             updateRefreshIcon();
128         }
129 
updateRefreshIcon()130         private void updateRefreshIcon() {
131             if (mRefreshIcon == null) {
132                 return;
133             }
134 
135             if (isRefreshInProgress()) {
136                 mRefreshIcon.setActionView(R.layout.action_bar_indeterminate_progress);
137             } else {
138                 mRefreshIcon.setActionView(null);
139             }
140         }
141     };
142 
143     protected final RefreshListener mRefreshListener = new RefreshListener();
144 
UIControllerBase(EmailActivity activity)145     public UIControllerBase(EmailActivity activity) {
146         mActivity = activity;
147         mFragmentManager = activity.getFragmentManager();
148         mRefreshManager = RefreshManager.getInstance(mActivity);
149         mActionBarController = createActionBarController(activity);
150         if (DEBUG_FRAGMENTS) {
151             FragmentManager.enableDebugLogging(true);
152         }
153     }
154 
155     /**
156      * Called by the base class to let a subclass create an {@link ActionBarController}.
157      */
createActionBarController(Activity activity)158     protected abstract ActionBarController createActionBarController(Activity activity);
159 
160     /** @return the layout ID for the activity. */
getLayoutId()161     public abstract int getLayoutId();
162 
163     /**
164      * Must be called just after the activity sets up the content view.  Used to initialize views.
165      *
166      * (Due to the complexity regarding class/activity initialization order, we can't do this in
167      * the constructor.)
168      */
onActivityViewReady()169     public void onActivityViewReady() {
170         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
171             Log.d(Logging.LOG_TAG, this + " onActivityViewReady");
172         }
173     }
174 
175     /**
176      * Called at the end of {@link EmailActivity#onCreate}.
177      */
onActivityCreated()178     public void onActivityCreated() {
179         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
180             Log.d(Logging.LOG_TAG, this + " onActivityCreated");
181         }
182         mRefreshManager.registerListener(mRefreshListener);
183         mActionBarController.onActivityCreated();
184         mNfcHandler = NfcHandler.register(this, mActivity);
185     }
186 
187     /**
188      * Handles the {@link android.app.Activity#onStart} callback.
189      */
onActivityStart()190     public void onActivityStart() {
191         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
192             Log.d(Logging.LOG_TAG, this + " onActivityStart");
193         }
194         if (isMessageViewInstalled()) {
195             updateMessageOrderManager();
196         }
197     }
198 
199     /**
200      * Handles the {@link android.app.Activity#onResume} callback.
201      */
onActivityResume()202     public void onActivityResume() {
203         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
204             Log.d(Logging.LOG_TAG, this + " onActivityResume");
205         }
206         refreshActionBar();
207         if (mNfcHandler != null) {
208             mNfcHandler.onAccountChanged();  // workaround for email not set on initial load
209         }
210         long accountId = getUIAccountId();
211         Preferences.getPreferences(mActivity).setLastUsedAccountId(accountId);
212         showAccountSpecificWarning(accountId);
213     }
214 
215     /**
216      * Handles the {@link android.app.Activity#onPause} callback.
217      */
onActivityPause()218     public void onActivityPause() {
219         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
220             Log.d(Logging.LOG_TAG, this + " onActivityPause");
221         }
222     }
223 
224     /**
225      * Handles the {@link android.app.Activity#onStop} callback.
226      */
onActivityStop()227     public void onActivityStop() {
228         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
229             Log.d(Logging.LOG_TAG, this + " onActivityStop");
230         }
231         stopMessageOrderManager();
232     }
233 
234     /**
235      * Handles the {@link android.app.Activity#onDestroy} callback.
236      */
onActivityDestroy()237     public void onActivityDestroy() {
238         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
239             Log.d(Logging.LOG_TAG, this + " onActivityDestroy");
240         }
241         mActionBarController.onActivityDestroy();
242         mRefreshManager.unregisterListener(mRefreshListener);
243         mTaskTracker.cancellAllInterrupt();
244     }
245 
246     /**
247      * Handles the {@link android.app.Activity#onSaveInstanceState} callback.
248      */
onSaveInstanceState(Bundle outState)249     public void onSaveInstanceState(Bundle outState) {
250         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
251             Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
252         }
253         mActionBarController.onSaveInstanceState(outState);
254         outState.putParcelable(KEY_LIST_CONTEXT, mListContext);
255     }
256 
257     /**
258      * Handles the {@link android.app.Activity#onRestoreInstanceState} callback.
259      */
onRestoreInstanceState(Bundle savedInstanceState)260     public void onRestoreInstanceState(Bundle savedInstanceState) {
261         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
262             Log.d(Logging.LOG_TAG, this + " restoreInstanceState");
263         }
264         mActionBarController.onRestoreInstanceState(savedInstanceState);
265         mListContext = savedInstanceState.getParcelable(KEY_LIST_CONTEXT);
266     }
267 
268     // MessageViewFragment$Callback
269     @Override
onMessageSetUnread()270     public void onMessageSetUnread() {
271         doAutoAdvance();
272     }
273 
274     // MessageViewFragment$Callback
275     @Override
onMessageNotExists()276     public void onMessageNotExists() {
277         doAutoAdvance();
278     }
279 
280     // MessageViewFragment$Callback
281     @Override
onRespondedToInvite(int response)282     public void onRespondedToInvite(int response) {
283         doAutoAdvance();
284     }
285 
286     // MessageViewFragment$Callback
287     @Override
onBeforeMessageGone()288     public void onBeforeMessageGone() {
289         doAutoAdvance();
290     }
291 
292     /**
293      * Install a fragment.  Must be caleld from the host activity's
294      * {@link FragmentInstallable#onInstallFragment}.
295      */
onInstallFragment(Fragment fragment)296     public final void onInstallFragment(Fragment fragment) {
297         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
298             Log.d(Logging.LOG_TAG, this + " onInstallFragment  fragment=" + fragment);
299         }
300         if (fragment instanceof MailboxListFragment) {
301             installMailboxListFragment((MailboxListFragment) fragment);
302         } else if (fragment instanceof MessageListFragment) {
303             installMessageListFragment((MessageListFragment) fragment);
304         } else if (fragment instanceof MessageViewFragment) {
305             installMessageViewFragment((MessageViewFragment) fragment);
306         } else {
307             throw new IllegalArgumentException("Tried to install unknown fragment");
308         }
309     }
310 
311     /** Install fragment */
installMailboxListFragment(MailboxListFragment fragment)312     protected void installMailboxListFragment(MailboxListFragment fragment) {
313         mMailboxListFragment = fragment;
314         mMailboxListFragment.setCallback(this);
315 
316         // TODO: consolidate this refresh with the one that the Fragment itself does. since
317         // the fragment calls setHasOptionsMenu(true) - it invalidates when it gets attached.
318         // However the timing is slightly different and leads to a delay in update if this isn't
319         // here - investigate why. same for the other installs.
320         refreshActionBar();
321     }
322 
323     /** Install fragment */
installMessageListFragment(MessageListFragment fragment)324     protected void installMessageListFragment(MessageListFragment fragment) {
325         mMessageListFragment = fragment;
326         mMessageListFragment.setCallback(this);
327         refreshActionBar();
328     }
329 
330     /** Install fragment */
installMessageViewFragment(MessageViewFragment fragment)331     protected void installMessageViewFragment(MessageViewFragment fragment) {
332         mMessageViewFragment = fragment;
333         mMessageViewFragment.setCallback(this);
334 
335         updateMessageOrderManager();
336         refreshActionBar();
337     }
338 
339     /**
340      * Uninstall a fragment.  Must be caleld from the host activity's
341      * {@link FragmentInstallable#onUninstallFragment}.
342      */
onUninstallFragment(Fragment fragment)343     public final void onUninstallFragment(Fragment fragment) {
344         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
345             Log.d(Logging.LOG_TAG, this + " onUninstallFragment  fragment=" + fragment);
346         }
347         mRemovedFragments.remove(fragment);
348         if (fragment == mMailboxListFragment) {
349             uninstallMailboxListFragment();
350         } else if (fragment == mMessageListFragment) {
351             uninstallMessageListFragment();
352         } else if (fragment == mMessageViewFragment) {
353             uninstallMessageViewFragment();
354         } else {
355             throw new IllegalArgumentException("Tried to uninstall unknown fragment");
356         }
357     }
358 
359     /** Uninstall {@link MailboxListFragment} */
uninstallMailboxListFragment()360     protected void uninstallMailboxListFragment() {
361         mMailboxListFragment.setCallback(null);
362         mMailboxListFragment = null;
363     }
364 
365     /** Uninstall {@link MessageListFragment} */
uninstallMessageListFragment()366     protected void uninstallMessageListFragment() {
367         mMessageListFragment.setCallback(null);
368         mMessageListFragment = null;
369     }
370 
371     /** Uninstall {@link MessageViewFragment} */
uninstallMessageViewFragment()372     protected void uninstallMessageViewFragment() {
373         mMessageViewFragment.setCallback(null);
374         mMessageViewFragment = null;
375     }
376 
377     /**
378      * If a {@link Fragment} is not already in {@link #mRemovedFragments},
379      * {@link FragmentTransaction#remove} it and add to the list.
380      *
381      * Do nothing if {@code fragment} is null.
382      */
removeFragment(FragmentTransaction ft, Fragment fragment)383     protected final void removeFragment(FragmentTransaction ft, Fragment fragment) {
384         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
385             Log.d(Logging.LOG_TAG, this + " removeFragment fragment=" + fragment);
386         }
387         if (fragment == null) {
388             return;
389         }
390         if (!mRemovedFragments.contains(fragment)) {
391             // Remove try/catch when b/4981556 is fixed (framework bug)
392             try {
393                 ft.remove(fragment);
394             } catch (IllegalStateException ex) {
395                 Log.e(Logging.LOG_TAG, "Swalling IllegalStateException due to known bug for "
396                         + " fragment: " + fragment, ex);
397                 Log.e(Logging.LOG_TAG, Utility.dumpFragment(fragment));
398             }
399             addFragmentToRemovalList(fragment);
400         }
401     }
402 
403     /**
404      * Remove a {@link Fragment} from {@link #mRemovedFragments}.  No-op if {@code fragment} is
405      * null.
406      *
407      * {@link #removeMailboxListFragment}, {@link #removeMessageListFragment} and
408      * {@link #removeMessageViewFragment} all call this, so subclasses don't have to do this when
409      * using them.
410      *
411      * However, unfortunately, subclasses have to call this manually when popping from the
412      * back stack to avoid double-delete.
413      */
addFragmentToRemovalList(Fragment fragment)414     protected void addFragmentToRemovalList(Fragment fragment) {
415         if (fragment != null) {
416             mRemovedFragments.add(fragment);
417         }
418     }
419 
420     /**
421      * Remove the fragment if it's installed.
422      */
removeMailboxListFragment(FragmentTransaction ft)423     protected FragmentTransaction removeMailboxListFragment(FragmentTransaction ft) {
424         removeFragment(ft, mMailboxListFragment);
425         return ft;
426     }
427 
428     /**
429      * Remove the fragment if it's installed.
430      */
removeMessageListFragment(FragmentTransaction ft)431     protected FragmentTransaction removeMessageListFragment(FragmentTransaction ft) {
432         removeFragment(ft, mMessageListFragment);
433         return ft;
434     }
435 
436     /**
437      * Remove the fragment if it's installed.
438      */
removeMessageViewFragment(FragmentTransaction ft)439     protected FragmentTransaction removeMessageViewFragment(FragmentTransaction ft) {
440         removeFragment(ft, mMessageViewFragment);
441         return ft;
442     }
443 
444     /** @return true if a {@link MailboxListFragment} is installed. */
isMailboxListInstalled()445     protected final boolean isMailboxListInstalled() {
446         return mMailboxListFragment != null;
447     }
448 
449     /** @return true if a {@link MessageListFragment} is installed. */
isMessageListInstalled()450     protected final boolean isMessageListInstalled() {
451         return mMessageListFragment != null;
452     }
453 
454     /** @return true if a {@link MessageViewFragment} is installed. */
isMessageViewInstalled()455     protected final boolean isMessageViewInstalled() {
456         return mMessageViewFragment != null;
457     }
458 
459     /** @return the installed {@link MailboxListFragment} or null. */
getMailboxListFragment()460     protected final MailboxListFragment getMailboxListFragment() {
461         return mMailboxListFragment;
462     }
463 
464     /** @return the installed {@link MessageListFragment} or null. */
getMessageListFragment()465     protected final MessageListFragment getMessageListFragment() {
466         return mMessageListFragment;
467     }
468 
469     /** @return the installed {@link MessageViewFragment} or null. */
getMessageViewFragment()470     protected final MessageViewFragment getMessageViewFragment() {
471         return mMessageViewFragment;
472     }
473 
474     /**
475      * Commit a {@link FragmentTransaction}.
476      */
commitFragmentTransaction(FragmentTransaction ft)477     protected void commitFragmentTransaction(FragmentTransaction ft) {
478         if (DEBUG_FRAGMENTS) {
479             Log.d(Logging.LOG_TAG, this + " commitFragmentTransaction: " + ft);
480         }
481         if (!ft.isEmpty()) {
482             // NB: there should be no cases in which a transaction is committed after
483             // onSaveInstanceState. Unfortunately, the "state loss" check also happens when in
484             // LoaderCallbacks.onLoadFinished, and we wish to perform transactions there. The check
485             // by the framework is conservative and prevents cases where there are transactions
486             // affecting Loader lifecycles - but we have no such cases.
487             // TODO: use asynchronous callbacks from loaders to avoid this implicit dependency
488             ft.commitAllowingStateLoss();
489             mFragmentManager.executePendingTransactions();
490         }
491     }
492 
493     /**
494      * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
495      *
496      * @see #getActualAccountId()
497      */
getUIAccountId()498     public abstract long getUIAccountId();
499 
500     /**
501      * @return true if an account is selected, or the current view is the combined view.
502      */
isAccountSelected()503     public final boolean isAccountSelected() {
504         return getUIAccountId() != Account.NO_ACCOUNT;
505     }
506 
507     /**
508      * @return if an actual account is selected.  (i.e. {@link Account#ACCOUNT_ID_COMBINED_VIEW}
509      * is not considered "actual".s)
510      */
isActualAccountSelected()511     public final boolean isActualAccountSelected() {
512         return isAccountSelected() && (getUIAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW);
513     }
514 
515     /**
516      * @return the currently selected account ID.  If the current view is the combined view,
517      * it'll return {@link Account#NO_ACCOUNT}.
518      *
519      * @see #getUIAccountId()
520      */
getActualAccountId()521     public final long getActualAccountId() {
522         return isActualAccountSelected() ? getUIAccountId() : Account.NO_ACCOUNT;
523     }
524 
525     /**
526      * Show the default view for the given account.
527      *
528      * @param accountId ID of the account to load.  Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
529      *     Must never be {@link Account#NO_ACCOUNT}.
530      * @param forceShowInbox If {@code false} and the given account is already selected, do nothing.
531      *        If {@code false}, we always change the view even if the account is selected.
532      */
switchAccount(long accountId, boolean forceShowInbox)533     public final void switchAccount(long accountId, boolean forceShowInbox) {
534 
535         if (Account.isSecurityHold(mActivity, accountId)) {
536             ActivityHelper.showSecurityHoldDialog(mActivity, accountId);
537             mActivity.finish();
538             return;
539         }
540 
541         if (accountId == getUIAccountId() && !forceShowInbox) {
542             // Do nothing if the account is already selected.  Not even going back to the inbox.
543             return;
544         }
545         if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
546             openMailbox(accountId, Mailbox.QUERY_ALL_INBOXES);
547         } else {
548             long inboxId = Mailbox.findMailboxOfType(mActivity, accountId, Mailbox.TYPE_INBOX);
549             if (inboxId == Mailbox.NO_MAILBOX) {
550                 // The account doesn't have Inbox yet... Redirect to Welcome and let it wait for
551                 // the initial sync...
552                 Log.w(Logging.LOG_TAG, "Account " + accountId +" doesn't have Inbox.  Redirecting"
553                         + " to Welcome...");
554                 Welcome.actionOpenAccountInbox(mActivity, accountId);
555                 mActivity.finish();
556             } else {
557                 openMailbox(accountId, inboxId);
558             }
559         }
560         if (mNfcHandler != null) {
561             mNfcHandler.onAccountChanged();
562         }
563         Preferences.getPreferences(mActivity).setLastUsedAccountId(accountId);
564         showAccountSpecificWarning(accountId);
565     }
566 
567     /**
568      * Returns the id of the parent mailbox used for the mailbox list fragment.
569      *
570      * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with
571      *     {@link #getMessageListMailboxId()}
572      */
getMailboxListMailboxId()573     protected long getMailboxListMailboxId() {
574         return isMailboxListInstalled() ? getMailboxListFragment().getSelectedMailboxId()
575                 : Mailbox.NO_MAILBOX;
576     }
577 
578     /**
579      * Returns the id of the mailbox used for the message list fragment.
580      *
581      * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with
582      *     {@link #getMessageListMailboxId()}
583      */
getMessageListMailboxId()584     protected long getMessageListMailboxId() {
585         return isMessageListInstalled() ? getMessageListFragment().getMailboxId()
586                 : Mailbox.NO_MAILBOX;
587     }
588 
589     /**
590      * Shortcut for {@link #open} with {@link Message#NO_MESSAGE}.
591      */
openMailbox(long accountId, long mailboxId)592     protected final void openMailbox(long accountId, long mailboxId) {
593         open(MessageListContext.forMailbox(accountId, mailboxId), Message.NO_MESSAGE);
594     }
595 
596     /**
597      * Opens a given list
598      * @param listContext the list context for the message list to open
599      * @param messageId if specified and not {@link Message#NO_MESSAGE}, will open the message
600      *     in the message list.
601      */
open(final MessageListContext listContext, final long messageId)602     public final void open(final MessageListContext listContext, final long messageId) {
603         setListContext(listContext);
604         openInternal(listContext, messageId);
605 
606         if (listContext.isSearch()) {
607             mActionBarController.enterSearchMode(listContext.getSearchParams().mFilter);
608         }
609     }
610 
611     /**
612      * Sets the internal value of the list context for the message list.
613      */
setListContext(MessageListContext listContext)614     protected void setListContext(MessageListContext listContext) {
615         if (Objects.equal(listContext, mListContext)) {
616             return;
617         }
618 
619         if (Email.DEBUG && Logging.DEBUG_LIFECYCLE) {
620             Log.i(Logging.LOG_TAG, this + " setListContext: " + listContext);
621         }
622         mListContext = listContext;
623     }
624 
openInternal( final MessageListContext listContext, final long messageId)625     protected abstract void openInternal(
626             final MessageListContext listContext, final long messageId);
627 
628     /**
629      * Performs the back action.
630      *
631      * @param isSystemBackKey <code>true</code> if the system back key was pressed.
632      * <code>false</code> if it's caused by the "home" icon click on the action bar.
633      */
onBackPressed(boolean isSystemBackKey)634     public abstract boolean onBackPressed(boolean isSystemBackKey);
635 
onSearchStarted()636     public void onSearchStarted() {
637         // Show/hide the original search icon.
638         mActivity.invalidateOptionsMenu();
639     }
640 
641     /**
642      * Must be called from {@link Activity#onSearchRequested()}.
643      * This initiates the search entry mode - see {@link #onSearchSubmit} for when the search
644      * is actually submitted.
645      */
onSearchRequested()646     public void onSearchRequested() {
647         long accountId = getActualAccountId();
648         boolean accountSearchable = false;
649         if (accountId > 0) {
650             Account account = Account.restoreAccountWithId(mActivity, accountId);
651             if (account != null) {
652                 String protocol = account.getProtocol(mActivity);
653                 accountSearchable = (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0;
654             }
655         }
656 
657         if (!accountSearchable) {
658             return;
659         }
660 
661         if (isMessageListReady()) {
662             mActionBarController.enterSearchMode(null);
663         }
664     }
665 
666     /**
667      * @return Whether or not a message list is ready and has its initial meta data loaded.
668      */
isMessageListReady()669     protected boolean isMessageListReady() {
670         return isMessageListInstalled() && getMessageListFragment().hasDataLoaded();
671     }
672 
673     /**
674      * Determines the mailbox to search, if a search was to be initiated now.
675      * This will return {@code null} if the UI is not focused on any particular mailbox to search
676      * on.
677      */
getSearchableMailbox()678     private Mailbox getSearchableMailbox() {
679         if (!isMessageListReady()) {
680             return null;
681         }
682         MessageListFragment messageList = getMessageListFragment();
683 
684         // If already in a search, future searches will search the original mailbox.
685         return mListContext.isSearch()
686                 ? messageList.getSearchedMailbox()
687                 : messageList.getMailbox();
688     }
689 
690     // TODO: this logic probably needs to be tested in the backends as well, so it may be nice
691     // to consolidate this to a centralized place, so that they don't get out of sync.
692     /**
693      * @return whether or not this account should do a global search instead when a user
694      *     initiates a search on the given mailbox.
695      */
shouldDoGlobalSearch(Account account, Mailbox mailbox)696     private static boolean shouldDoGlobalSearch(Account account, Mailbox mailbox) {
697         return ((account.mFlags & Account.FLAGS_SUPPORTS_GLOBAL_SEARCH) != 0)
698                 && (mailbox.mType == Mailbox.TYPE_INBOX);
699     }
700 
701     /**
702      * Retrieves the hint text to be shown for when a search entry is being made.
703      */
getSearchHint()704     protected String getSearchHint() {
705         if (!isMessageListReady()) {
706             return "";
707         }
708         Account account = getMessageListFragment().getAccount();
709         Mailbox mailbox = getSearchableMailbox();
710 
711         if (mailbox == null) {
712             return "";
713         }
714 
715         if (shouldDoGlobalSearch(account, mailbox)) {
716             return mActivity.getString(R.string.search_hint);
717         }
718 
719         // Regular mailbox, or IMAP - search within that mailbox.
720         String mailboxName = FolderProperties.getInstance(mActivity).getDisplayName(mailbox);
721         return String.format(
722                 mActivity.getString(R.string.search_mailbox_hint),
723                 mailboxName);
724     }
725 
726     /**
727      * Kicks off a search query, if the UI is in a state where a search is possible.
728      */
onSearchSubmit(final String queryTerm)729     protected void onSearchSubmit(final String queryTerm) {
730         final long accountId = getUIAccountId();
731         if (!Account.isNormalAccount(accountId)) {
732             return; // Invalid account to search from.
733         }
734 
735         Mailbox searchableMailbox = getSearchableMailbox();
736         if (searchableMailbox == null) {
737             return;
738         }
739         final long mailboxId = searchableMailbox.mId;
740 
741         if (Email.DEBUG) {
742             Log.d(Logging.LOG_TAG,
743                     "Submitting search: [" + queryTerm + "] in mailboxId=" + mailboxId);
744         }
745 
746         mActivity.startActivity(EmailActivity.createSearchIntent(
747                 mActivity, accountId, mailboxId, queryTerm));
748 
749 
750         // TODO: this causes a slight flicker.
751         // A new instance of the activity will sit on top. When the user exits search and
752         // returns to this activity, the search box should not be open then.
753         mActionBarController.exitSearchMode();
754     }
755 
756     /**
757      * Handles exiting of search entry mode.
758      */
onSearchExit()759     protected void onSearchExit() {
760         if ((mListContext != null) && mListContext.isSearch()) {
761             mActivity.finish();
762         } else {
763             // Re show the search icon.
764             mActivity.invalidateOptionsMenu();
765         }
766     }
767 
768     /**
769      * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback.
770      */
onCreateOptionsMenu(MenuInflater inflater, Menu menu)771     public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) {
772         inflater.inflate(R.menu.email_activity_options, menu);
773         return true;
774     }
775 
776     /**
777      * Handles the {@link android.app.Activity#onPrepareOptionsMenu} callback.
778      */
onPrepareOptionsMenu(MenuInflater inflater, Menu menu)779     public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) {
780         // Update the refresh button.
781         MenuItem item = menu.findItem(R.id.refresh);
782         if (item != null) {
783             if (isRefreshEnabled()) {
784                 item.setVisible(true);
785                 mRefreshListener.setRefreshIcon(item);
786             } else {
787                 item.setVisible(false);
788                 mRefreshListener.setRefreshIcon(null);
789             }
790         }
791 
792         // Deal with protocol-specific menu options.
793         boolean mailboxHasServerCounterpart = false;
794         boolean accountSearchable = false;
795         boolean isEas = false;
796 
797         if (isMessageListReady()) {
798             long accountId = getActualAccountId();
799             if (accountId > 0) {
800                 Account account = Account.restoreAccountWithId(mActivity, accountId);
801                 if (account != null) {
802                     String protocol = account.getProtocol(mActivity);
803                     isEas = HostAuth.SCHEME_EAS.equals(protocol);
804                     Mailbox mailbox = getMessageListFragment().getMailbox();
805                     mailboxHasServerCounterpart = (mailbox != null)
806                             && mailbox.loadsFromServer(protocol);
807                     accountSearchable = (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0;
808                 }
809             }
810         }
811 
812         boolean showSearchIcon = !mActionBarController.isInSearchMode()
813                 && accountSearchable && mailboxHasServerCounterpart;
814 
815         MenuItem search = menu.findItem(R.id.search);
816         if (search != null) {
817             search.setVisible(showSearchIcon);
818         }
819         MenuItem settings = menu.findItem(R.id.mailbox_settings);
820         if (settings != null) {
821             settings.setVisible(isEas && mailboxHasServerCounterpart);
822         }
823         return true;
824     }
825 
826     /**
827      * Handles the {@link android.app.Activity#onOptionsItemSelected} callback.
828      *
829      * @return true if the option item is handled.
830      */
onOptionsItemSelected(MenuItem item)831     public boolean onOptionsItemSelected(MenuItem item) {
832         switch (item.getItemId()) {
833             case android.R.id.home:
834                 // Comes from the action bar when the app icon on the left is pressed.
835                 // It works like a back press, but it won't close the activity.
836                 return onBackPressed(false);
837             case R.id.compose:
838                 return onCompose();
839             case R.id.refresh:
840                 onRefresh();
841                 return true;
842             case R.id.account_settings:
843                 return onAccountSettings();
844             case R.id.search:
845                 onSearchRequested();
846                 return true;
847             case R.id.mailbox_settings:
848                 final long mailboxId = getMailboxSettingsMailboxId();
849                 if (mailboxId != Mailbox.NO_MAILBOX) {
850                     MailboxSettings.start(mActivity, mailboxId);
851                 }
852                 return true;
853         }
854         return false;
855     }
856 
857     /**
858      * Opens the message compose activity.
859      */
onCompose()860     private boolean onCompose() {
861         if (!isAccountSelected()) {
862             return false; // this shouldn't really happen
863         }
864         MessageCompose.actionCompose(mActivity, getActualAccountId());
865         return true;
866     }
867 
868     /**
869      * Handles the "Settings" option item.  Opens the settings activity.
870      */
onAccountSettings()871     private boolean onAccountSettings() {
872         AccountSettings.actionSettings(mActivity, getActualAccountId());
873         return true;
874     }
875 
876     /**
877      * @return the ID of the message in focus and visible, if any. Returns
878      *     {@link Message#NO_MESSAGE} if no message is opened.
879      */
getMessageId()880     protected long getMessageId() {
881         return isMessageViewInstalled()
882                 ? getMessageViewFragment().getMessageId()
883                 : Message.NO_MESSAGE;
884     }
885 
886 
887     /**
888      * @return mailbox ID for "mailbox settings" option.
889      */
getMailboxSettingsMailboxId()890     protected abstract long getMailboxSettingsMailboxId();
891 
892     /**
893      * Performs "refesh".
894      */
onRefresh()895     protected abstract void onRefresh();
896 
897     /**
898      * @return true if refresh is in progress for the current mailbox.
899      */
isRefreshInProgress()900     protected abstract boolean isRefreshInProgress();
901 
902     /**
903      * @return true if the UI should enable the "refresh" command.
904      */
isRefreshEnabled()905     protected abstract boolean isRefreshEnabled();
906 
907     /**
908      * Refresh the action bar and menu items, including the "refreshing" icon.
909      */
refreshActionBar()910     protected void refreshActionBar() {
911         if (mActionBarController != null) {
912             mActionBarController.refresh();
913         }
914         mActivity.invalidateOptionsMenu();
915     }
916 
917     // MessageListFragment.Callback
918     @Override
onMailboxNotFound(boolean isFirstLoad)919     public void onMailboxNotFound(boolean isFirstLoad) {
920         // Something bad happened - the account or mailbox we were looking for was deleted.
921         // Just restart and let the entry flow find a good default view.
922         if (isFirstLoad) {
923             // Only show this if it's the first load (e.g. a shortcut) rather an a return to
924             // a mailbox (which might be in a just-deleted account)
925             Utility.showToast(mActivity, R.string.toast_mailbox_not_found);
926         }
927         long accountId = getUIAccountId();
928         if (accountId != Account.NO_ACCOUNT) {
929             mActivity.startActivity(Welcome.createOpenAccountInboxIntent(mActivity, accountId));
930         } else {
931             Welcome.actionStart(mActivity);
932 
933         }
934         mActivity.finish();
935     }
936 
getMessageOrderManager()937     protected final MessageOrderManager getMessageOrderManager() {
938         return mOrderManager;
939     }
940 
941     /** Perform "auto-advance. */
doAutoAdvance()942     protected final void doAutoAdvance() {
943         switch (Preferences.getPreferences(mActivity).getAutoAdvanceDirection()) {
944             case Preferences.AUTO_ADVANCE_NEWER:
945                 if (moveToNewer()) return;
946                 break;
947             case Preferences.AUTO_ADVANCE_OLDER:
948                 if (moveToOlder()) return;
949                 break;
950         }
951         if (isMessageViewInstalled()) { // We really should have the message view but just in case
952             // Go back to mailbox list.
953             // Use onBackPressed(), so we'll restore the message view state, such as scroll
954             // position.
955             // Also make sure to pass false to isSystemBackKey, so on two-pane we don't go back
956             // to the collapsed mode.
957             onBackPressed(true);
958         }
959     }
960 
961     /**
962      * Subclass must implement it to enable/disable the newer/older buttons.
963      */
updateNavigationArrows()964     protected abstract void updateNavigationArrows();
965 
moveToOlder()966     protected final boolean moveToOlder() {
967         if ((mOrderManager != null) && mOrderManager.moveToOlder()) {
968             navigateToMessage(mOrderManager.getCurrentMessageId());
969             return true;
970         }
971         return false;
972     }
973 
moveToNewer()974     protected final boolean moveToNewer() {
975         if ((mOrderManager != null) && mOrderManager.moveToNewer()) {
976             navigateToMessage(mOrderManager.getCurrentMessageId());
977             return true;
978         }
979         return false;
980     }
981 
982     /**
983      * Called when the user taps newer/older.  Subclass must implement it to open the specified
984      * message.
985      *
986      * It's a bit different from just showing the message view fragment; on one-pane we show the
987      * message view fragment but don't want to change back state.
988      */
navigateToMessage(long messageId)989     protected abstract void navigateToMessage(long messageId);
990 
991     /**
992      * Potentially create a new {@link MessageOrderManager}; if it's not already started or if
993      * the account has changed, and sync it to the current message.
994      */
updateMessageOrderManager()995     private void updateMessageOrderManager() {
996         if (!isMessageViewInstalled()) {
997             return;
998         }
999         Preconditions.checkNotNull(mListContext);
1000 
1001         if (mOrderManager == null || !mOrderManager.getListContext().equals(mListContext)) {
1002             stopMessageOrderManager();
1003             mOrderManager = new MessageOrderManager(
1004                     mActivity, mListContext, mMessageOrderManagerCallback);
1005         }
1006         mOrderManager.moveTo(getMessageId());
1007         updateNavigationArrows();
1008     }
1009 
1010     /**
1011      * Stop {@link MessageOrderManager}.
1012      */
stopMessageOrderManager()1013     protected final void stopMessageOrderManager() {
1014         if (mOrderManager != null) {
1015             mOrderManager.close();
1016             mOrderManager = null;
1017         }
1018     }
1019 
1020     private class MessageOrderManagerCallback implements MessageOrderManager.Callback {
1021         @Override
onMessagesChanged()1022         public void onMessagesChanged() {
1023             updateNavigationArrows();
1024         }
1025 
1026         @Override
onMessageNotFound()1027         public void onMessageNotFound() {
1028             doAutoAdvance();
1029         }
1030     }
1031 
1032 
showAccountSpecificWarning(long accountId)1033     private void showAccountSpecificWarning(long accountId) {
1034         if (accountId != Account.NO_ACCOUNT && accountId != Account.NO_ACCOUNT) {
1035             Account account = Account.restoreAccountWithId(mActivity, accountId);
1036             if (account != null &&
1037                     Preferences.getPreferences(mActivity)
1038                     .shouldShowRequireManualSync(mActivity, account)) {
1039                 new RequireManualSyncDialog(mActivity, account).show();
1040             }
1041         }
1042     }
1043 
1044     @Override
toString()1045     public String toString() {
1046         return getClass().getSimpleName(); // Shown on logcat
1047     }
1048 }
1049