UIControllerTwoPane.java revision a355a4abe2abb9d09c1d2cfb61d62c2006d19289
1/*
2 * Copyright (C) 2010 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
17package com.android.email.activity;
18
19import android.app.Activity;
20import android.app.FragmentTransaction;
21import android.content.Context;
22import android.os.Bundle;
23import android.util.Log;
24
25import com.android.email.Clock;
26import com.android.email.Controller;
27import com.android.email.Email;
28import com.android.email.Preferences;
29import com.android.email.R;
30import com.android.email.RefreshManager;
31import com.android.email.activity.MailboxFinder.Callback;
32import com.android.email.activity.setup.AccountSecurity;
33import com.android.emailcommon.Logging;
34import com.android.emailcommon.provider.Account;
35import com.android.emailcommon.provider.EmailContent.Message;
36import com.android.emailcommon.provider.Mailbox;
37import com.android.emailcommon.utility.EmailAsyncTask;
38import com.google.common.annotations.VisibleForTesting;
39
40import java.util.Set;
41
42/**
43 * UI Controller for x-large devices.  Supports a multi-pane layout.
44 *
45 * Note: Always use {@link #commitFragmentTransaction} to operate fragment transactions,
46 * so that we can easily switch between synchronous and asynchronous transactions.
47 */
48class UIControllerTwoPane extends UIControllerBase implements
49        MailboxFinder.Callback,
50        ThreePaneLayout.Callback,
51        MailboxListFragment.Callback,
52        MessageListFragment.Callback,
53        MessageViewFragment.Callback {
54    @VisibleForTesting
55    static final int MAILBOX_REFRESH_MIN_INTERVAL = 30 * 1000; // in milliseconds
56
57    @VisibleForTesting
58    static final int INBOX_AUTO_REFRESH_MIN_INTERVAL = 10 * 1000; // in milliseconds
59
60    // Other UI elements
61    private ThreePaneLayout mThreePane;
62
63    private MessageCommandButtonView mMessageCommandButtons;
64
65    private MessageOrderManager mOrderManager;
66    private final MessageOrderManagerCallback mMessageOrderManagerCallback =
67        new MessageOrderManagerCallback();
68
69    /**
70     * The mailbox name selected on the mailbox list.
71     * Passed via {@link #onCurrentMailboxUpdated}.
72     */
73    private String mCurrentMailboxName;
74
75    /**
76     * The unread count for the mailbox selected on the mailbox list.
77     * Passed via {@link #onCurrentMailboxUpdated}.
78     *
79     * 0 if the mailbox doesn't have the concept of "unread".  e.g. Drafts.
80     */
81    private int mCurrentMailboxUnreadCount;
82
83    public UIControllerTwoPane(EmailActivity activity) {
84        super(activity);
85    }
86
87    @Override
88    public int getLayoutId() {
89        return R.layout.email_activity_two_pane;
90    }
91
92    // MailboxFinder$Callback
93    @Override
94    public void onAccountNotFound() {
95        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
96            Log.d(Logging.LOG_TAG, this + " onAccountNotFound()");
97        }
98        // Shouldn't happen
99    }
100
101    // MailboxFinder$Callback
102    @Override
103    public void onAccountSecurityHold(long accountId) {
104        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
105            Log.d(Logging.LOG_TAG, this + " onAccountSecurityHold()");
106        }
107        mActivity.startActivity(AccountSecurity.actionUpdateSecurityIntent(mActivity, accountId,
108                true));
109    }
110
111    // MailboxFinder$Callback
112    @Override
113    public void onMailboxFound(long accountId, long mailboxId) {
114        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
115            Log.d(Logging.LOG_TAG, this + " onMailboxFound()");
116        }
117        updateMessageList(accountId, mailboxId, true);
118    }
119
120    // MailboxFinder$Callback
121    @Override
122    public void onMailboxNotFound(long accountId) {
123        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
124            Log.d(Logging.LOG_TAG, this + " onMailboxNotFound()");
125        }
126        Log.e(Logging.LOG_TAG, "unable to find mailbox for account " + accountId);
127    }
128
129    // ThreePaneLayoutCallback
130    @Override
131    public void onVisiblePanesChanged(int previousVisiblePanes) {
132        refreshActionBar();
133
134        // If the right pane is gone, remove the message view.
135        final int visiblePanes = mThreePane.getVisiblePanes();
136
137        if (((visiblePanes & ThreePaneLayout.PANE_RIGHT) == 0) &&
138                ((previousVisiblePanes & ThreePaneLayout.PANE_RIGHT) != 0)) {
139            // Message view just got hidden
140            unselectMessage();
141        }
142        // Disable CAB when the message list is not visible.
143        if (isMessageListInstalled()) {
144            getMessageListFragment().onHidden((visiblePanes & ThreePaneLayout.PANE_MIDDLE) == 0);
145        }
146    }
147
148    // MailboxListFragment$Callback
149    @Override
150    public void onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation) {
151        if ((accountId == Account.NO_ACCOUNT) || (mailboxId == Mailbox.NO_MAILBOX)) {
152            throw new IllegalArgumentException();
153        }
154        if (getMessageListMailboxId() != mailboxId) {
155            updateMessageList(accountId, mailboxId, true);
156        }
157    }
158
159    // MailboxListFragment$Callback
160    @Override
161    public void onAccountSelected(long accountId) {
162        switchAccount(accountId);
163    }
164
165    // MailboxListFragment$Callback
166    @Override
167    public void onCurrentMailboxUpdated(long mailboxId, String mailboxName, int unreadCount) {
168        mCurrentMailboxName = mailboxName;
169        mCurrentMailboxUnreadCount = unreadCount;
170        refreshActionBar();
171    }
172
173    // MailboxListFragment$Callback
174    @Override
175    public void onParentMailboxChanged() {
176        refreshActionBar();
177    }
178
179    // MessageListFragment$Callback
180    @Override
181    public void onMessageOpen(long messageId, long messageMailboxId, long listMailboxId,
182            int type) {
183        if (type == MessageListFragment.Callback.TYPE_DRAFT) {
184            MessageCompose.actionEditDraft(mActivity, messageId);
185        } else {
186            if (getMessageId() != messageId) {
187                updateMessageView(messageId);
188                mThreePane.showRightPane();
189            }
190        }
191    }
192
193    // MessageListFragment$Callback
194    @Override
195    public void onMailboxNotFound() {
196        Log.e(Logging.LOG_TAG, "unable to find mailbox");
197    }
198
199    // MessageListFragment$Callback
200    @Override
201    public void onEnterSelectionMode(boolean enter) {
202    }
203
204    // MessageListFragment$Callback
205    /**
206     * Apply the auto-advance policy upon initation of a batch command that could potentially
207     * affect the currently selected conversation.
208     */
209    @Override
210    public void onAdvancingOpAccepted(Set<Long> affectedMessages) {
211        if (!isMessageViewInstalled()) {
212            // Do nothing if message view is not visible.
213            return;
214        }
215
216        int autoAdvanceDir = Preferences.getPreferences(mActivity).getAutoAdvanceDirection();
217        if ((autoAdvanceDir == Preferences.AUTO_ADVANCE_MESSAGE_LIST) || (mOrderManager == null)) {
218            if (affectedMessages.contains(getMessageId())) {
219                goBackToMailbox();
220            }
221            return;
222        }
223
224        // Navigate to the first unselected item in the appropriate direction.
225        switch (autoAdvanceDir) {
226            case Preferences.AUTO_ADVANCE_NEWER:
227                while (affectedMessages.contains(mOrderManager.getCurrentMessageId())) {
228                    if (!mOrderManager.moveToNewer()) {
229                        goBackToMailbox();
230                        return;
231                    }
232                }
233                updateMessageView(mOrderManager.getCurrentMessageId());
234                break;
235
236            case Preferences.AUTO_ADVANCE_OLDER:
237                while (affectedMessages.contains(mOrderManager.getCurrentMessageId())) {
238                    if (!mOrderManager.moveToOlder()) {
239                        goBackToMailbox();
240                        return;
241                    }
242                }
243                updateMessageView(mOrderManager.getCurrentMessageId());
244                break;
245        }
246    }
247
248    // MessageListFragment$Callback
249    @Override
250    public void onListLoaded() {
251    }
252
253    // MessageListFragment$Callback
254    @Override
255    public boolean onDragStarted() {
256        Log.w(Logging.LOG_TAG, "Drag started");
257
258        if ((mThreePane.getVisiblePanes() & ThreePaneLayout.PANE_LEFT) == 0) {
259            // Mailbox list hidden.  D&D not allowed.
260            return false;
261        }
262
263        // STOPSHIP Save the current mailbox list
264
265        return true;
266    }
267
268    // MessageListFragment$Callback
269    @Override
270    public void onDragEnded() {
271        Log.w(Logging.LOG_TAG, "Drag ended");
272
273        // STOPSHIP Restore the saved mailbox list
274    }
275
276    // MessageViewFragment$Callback
277    @Override
278    public void onMessageShown() {
279        updateMessageOrderManager();
280        updateNavigationArrows();
281    }
282
283    // MessageViewFragment$Callback
284    @Override
285    public boolean onUrlInMessageClicked(String url) {
286        return ActivityHelper.openUrlInMessage(mActivity, url, getActualAccountId());
287    }
288
289    // MessageViewFragment$Callback
290    @Override
291    public void onMessageSetUnread() {
292        goBackToMailbox();
293    }
294
295    // MessageViewFragment$Callback
296    @Override
297    public void onMessageNotExists() {
298        goBackToMailbox();
299    }
300
301    // MessageViewFragment$Callback
302    @Override
303    public void onLoadMessageStarted() {
304    }
305
306    // MessageViewFragment$Callback
307    @Override
308    public void onLoadMessageFinished() {
309    }
310
311    // MessageViewFragment$Callback
312    @Override
313    public void onLoadMessageError(String errorMessage) {
314    }
315
316    // MessageViewFragment$Callback
317    @Override
318    public void onRespondedToInvite(int response) {
319        onCurrentMessageGone();
320    }
321
322    // MessageViewFragment$Callback
323    @Override
324    public void onCalendarLinkClicked(long epochEventStartTime) {
325        ActivityHelper.openCalendar(mActivity, epochEventStartTime);
326    }
327
328    // MessageViewFragment$Callback
329    @Override
330    public void onBeforeMessageGone() {
331        onCurrentMessageGone();
332    }
333
334    // MessageViewFragment$Callback
335    @Override
336    public void onForward() {
337        MessageCompose.actionForward(mActivity, getMessageId());
338    }
339
340    // MessageViewFragment$Callback
341    @Override
342    public void onReply() {
343        MessageCompose.actionReply(mActivity, getMessageId(), false);
344    }
345
346    // MessageViewFragment$Callback
347    @Override
348    public void onReplyAll() {
349        MessageCompose.actionReply(mActivity, getMessageId(), true);
350    }
351
352    /**
353     * Must be called just after the activity sets up the content view.
354     */
355    @Override
356    public void onActivityViewReady() {
357        super.onActivityViewReady();
358
359        // Set up content
360        mThreePane = (ThreePaneLayout) mActivity.findViewById(R.id.three_pane);
361        mThreePane.setCallback(this);
362
363        mMessageCommandButtons = mThreePane.getMessageCommandButtons();
364        mMessageCommandButtons.setCallback(new CommandButtonCallback());
365    }
366
367    @Override
368    protected ActionBarController createActionBarController(Activity activity) {
369        return new ActionBarController(activity, activity.getLoaderManager(),
370                activity.getActionBar(), new ActionBarControllerCallback());
371    }
372
373    /**
374     * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
375     *
376     * @see #getActualAccountId()
377     */
378    @Override
379    public long getUIAccountId() {
380        return isMailboxListInstalled() ? getMailboxListFragment().getAccountId()
381                :Account.NO_ACCOUNT;
382    }
383
384    /**
385     * Returns the id of the parent mailbox used for the mailbox list fragment.
386     *
387     * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with
388     *     {@link #getMessageListMailboxId()}
389     */
390    private long getMailboxListMailboxId() {
391        return isMailboxListInstalled() ? getMailboxListFragment().getSelectedMailboxId()
392                : Mailbox.NO_MAILBOX;
393    }
394
395    /**
396     * Returns the id of the mailbox used for the message list fragment.
397     *
398     * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with
399     *     {@link #getMessageListMailboxId()}
400     */
401    private long getMessageListMailboxId() {
402        return isMessageListInstalled() ? getMessageListFragment().getMailboxId()
403                : Mailbox.NO_MAILBOX;
404    }
405
406    /*
407     * STOPSHIP Remove this -- see the base class method.
408     */
409    @Override
410    public long getMailboxSettingsMailboxId() {
411        return getMessageListMailboxId();
412    }
413
414    /**
415     * @return true if refresh is in progress for the current mailbox.
416     */
417    @Override
418    protected boolean isRefreshInProgress() {
419        long messageListMailboxId = getMessageListMailboxId();
420        return (messageListMailboxId >= 0)
421                && mRefreshManager.isMessageListRefreshing(messageListMailboxId);
422    }
423
424    /**
425     * @return true if the UI should enable the "refresh" command.
426     */
427    @Override
428    protected boolean isRefreshEnabled() {
429        // - Don't show for combined inboxes, but
430        // - Show even for non-refreshable mailboxes, in which case we refresh the mailbox list
431        return getActualAccountId() != Account.NO_ACCOUNT;
432    }
433
434    /**
435     * Called by the host activity at the end of {@link Activity#onCreate}.
436     */
437    @Override
438    public void onActivityCreated() {
439        super.onActivityCreated();
440    }
441
442    /** {@inheritDoc} */
443    @Override
444    public void onActivityStart() {
445        super.onActivityStart();
446        if (isMessageViewInstalled()) {
447            updateMessageOrderManager();
448        }
449    }
450
451    /** {@inheritDoc} */
452    @Override
453    public void onActivityPause() {
454        super.onActivityPause();
455    }
456
457    /** {@inheritDoc} */
458    @Override
459    public void onActivityStop() {
460        stopMessageOrderManager();
461        super.onActivityStop();
462    }
463
464    /** {@inheritDoc} */
465    @Override
466    public void onActivityDestroy() {
467        super.onActivityDestroy();
468    }
469
470    /** {@inheritDoc} */
471    @Override
472    public void onSaveInstanceState(Bundle outState) {
473        super.onSaveInstanceState(outState);
474    }
475
476    /** {@inheritDoc} */
477    @Override
478    public void onRestoreInstanceState(Bundle savedInstanceState) {
479        super.onRestoreInstanceState(savedInstanceState);
480    }
481
482    @Override
483    protected Callback getInboxLookupCallback() {
484        return this;
485    }
486
487    @Override
488    protected void installMessageListFragment(MessageListFragment fragment) {
489        super.installMessageListFragment(fragment);
490
491        if (isMailboxListInstalled()) {
492            getMailboxListFragment().setHighlightedMailbox(fragment.getMailboxId());
493        }
494    }
495
496    @Override
497    protected void installMessageViewFragment(MessageViewFragment fragment) {
498        super.installMessageViewFragment(fragment);
499
500        if (isMessageListInstalled()) {
501            getMessageListFragment().setSelectedMessage(fragment.getMessageId());
502        }
503    }
504
505    @Override
506    protected void uninstallMessageViewFragment() {
507        // Don't need it when there's no message view.
508        stopMessageOrderManager();
509        super.uninstallMessageViewFragment();
510    }
511
512    /**
513     * Commit a {@link FragmentTransaction}.
514     */
515    private void commitFragmentTransaction(FragmentTransaction ft) {
516        if (DEBUG_FRAGMENTS) {
517            Log.d(Logging.LOG_TAG, this + " commitFragmentTransaction: " + ft);
518        }
519        if (!ft.isEmpty()) {
520            ft.commit();
521            mFragmentManager.executePendingTransactions();
522        }
523    }
524
525    @Override
526    public void open(long accountId, long mailboxId, long messageId) {
527        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
528            Log.d(Logging.LOG_TAG, this + " open accountId=" + accountId
529                    + " mailboxId=" + mailboxId + " messageId=" + messageId);
530        }
531        final FragmentTransaction ft = mFragmentManager.beginTransaction();
532        if (accountId == Account.NO_ACCOUNT) {
533            throw new IllegalArgumentException();
534        } else if (mailboxId == Mailbox.NO_MAILBOX) {
535            updateMailboxList(ft, accountId, Mailbox.NO_MAILBOX, true);
536
537            // Show the appropriate message list
538            if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
539                // When opening the Combined view, the right pane will be "combined inbox".
540                updateMessageList(ft, accountId, Mailbox.QUERY_ALL_INBOXES, true);
541            } else {
542                // Try to find the inbox for the account
543                startInboxLookup(accountId);
544            }
545            mThreePane.showLeftPane();
546        } else if (messageId == Message.NO_MESSAGE) {
547            updateMailboxList(ft, accountId, mailboxId, true);
548            updateMessageList(ft, accountId, mailboxId, true);
549
550            // TODO: This is a total hack. Do something smarter to see if this should open a
551            // search view.
552            if (mailboxId == Controller.getInstance(mActivity).getSearchMailbox(accountId).mId) {
553                mThreePane.showRightPane();
554            } else {
555                mThreePane.showLeftPane();
556            }
557        } else {
558            updateMailboxList(ft, accountId, mailboxId, true);
559            updateMessageList(ft, accountId, mailboxId, true);
560            updateMessageView(ft, messageId);
561
562            mThreePane.showRightPane();
563        }
564        commitFragmentTransaction(ft);
565    }
566
567    /**
568     * Loads the given account and optionally selects the given mailbox and message. If the
569     * specified account is already selected, no actions will be performed unless
570     * <code>forceReload</code> is <code>true</code>.
571     *
572     * @param ft {@link FragmentTransaction} to use.
573     * @param accountId ID of the account to load. Must never be {@link Account#NO_ACCOUNT}.
574     * @param mailboxId ID of the mailbox to use as the "selected".  Pass
575     *     {@link Mailbox#NO_MAILBOX} to show the root mailboxes.
576     * @param clearDependentPane if true, the message list and the message view will be cleared
577     */
578    private void updateMailboxList(FragmentTransaction ft,
579            long accountId, long mailboxId, boolean clearDependentPane) {
580        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
581            Log.d(Logging.LOG_TAG, this + " updateMailboxList accountId=" + accountId
582                    + " mailboxId=" + mailboxId);
583        }
584        if (accountId == Account.NO_ACCOUNT) {
585            throw new IllegalArgumentException();
586        }
587
588        if ((getUIAccountId() != accountId) || (getMailboxListMailboxId() != mailboxId)) {
589            removeMailboxListFragment(ft);
590            ft.add(mThreePane.getLeftPaneId(),
591                    MailboxListFragment.newInstance(accountId, mailboxId, true));
592        }
593        if (clearDependentPane) {
594            removeMessageListFragment(ft);
595            removeMessageViewFragment(ft);
596        }
597    }
598
599    /**
600     * Shortcut to call {@link #updateMailboxList(FragmentTransaction, long, long, boolean)} and
601     * commit.
602     */
603    @SuppressWarnings("unused")
604    private void updateMailboxList(long accountId, long mailboxId, boolean clearDependentPane) {
605        FragmentTransaction ft = mFragmentManager.beginTransaction();
606        updateMailboxList(ft, accountId, mailboxId, clearDependentPane);
607        commitFragmentTransaction(ft);
608    }
609
610    /**
611     * Go back to a mailbox list view. If a message view is currently active, it will
612     * be hidden.
613     */
614    private void goBackToMailbox() {
615        if (isMessageViewInstalled()) {
616            mThreePane.showLeftPane(); // Show mailbox list
617        }
618    }
619
620    /**
621     * Show the message list fragment for the given mailbox.
622     *
623     * @param ft {@link FragmentTransaction} to use.
624     * @param accountId ID of the owner account for the mailbox.  Must never be
625     *     {@link Account#NO_ACCOUNT}.
626     * @param mailboxId ID of the mailbox to load. Must never be {@link Mailbox#NO_MAILBOX}.
627     * @param clearDependentPane if true, the message view will be cleared
628     */
629    private void updateMessageList(FragmentTransaction ft,
630            long accountId, long mailboxId, boolean clearDependentPane) {
631        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
632            Log.d(Logging.LOG_TAG, this + " updateMessageList mMailboxId=" + mailboxId);
633        }
634        if (mailboxId == Mailbox.NO_MAILBOX) {
635            throw new IllegalArgumentException();
636        }
637
638        stopInboxLookup();
639
640        if (mailboxId != getMessageListMailboxId()) {
641            removeMessageListFragment(ft);
642            ft.add(mThreePane.getMiddlePaneId(), MessageListFragment.newInstance(
643                    accountId, mailboxId));
644        }
645        if (clearDependentPane) {
646            removeMessageViewFragment(ft);
647        }
648    }
649
650    /**
651     * Shortcut to call {@link #updateMessageList(FragmentTransaction, long, long, boolean)} and
652     * commit.
653     */
654    private void updateMessageList(long accountId, long mailboxId, boolean clearDependentPane) {
655        FragmentTransaction ft = mFragmentManager.beginTransaction();
656        updateMessageList(ft, accountId, mailboxId, clearDependentPane);
657        commitFragmentTransaction(ft);
658    }
659
660    /**
661     * Show a message on the message view.
662     *
663     * @param ft {@link FragmentTransaction} to use.
664     * @param messageId ID of the mailbox to load. Must never be {@link Message#NO_MESSAGE}.
665     */
666    private void updateMessageView(FragmentTransaction ft, long messageId) {
667        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
668            Log.d(Logging.LOG_TAG, this + " updateMessageView messageId=" + messageId);
669        }
670        if (messageId == Message.NO_MESSAGE) {
671            throw new IllegalArgumentException();
672        }
673
674        if (messageId == getMessageId()) {
675            return; // nothing to do.
676        }
677
678        removeMessageViewFragment(ft);
679
680        ft.add(mThreePane.getRightPaneId(), MessageViewFragment.newInstance(
681                getUIAccountId(), getMessageListMailboxId(), messageId));
682    }
683
684    /**
685     * Shortcut to call {@link #updateMessageView(FragmentTransaction, long)} and commit.
686     */
687    private void updateMessageView(long messageId) {
688        FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction();
689        updateMessageView(ft, messageId);
690        commitFragmentTransaction(ft);
691    }
692
693    /**
694     * Remove the message view if shown.
695     */
696    private void unselectMessage() {
697        commitFragmentTransaction(removeMessageViewFragment(
698                mActivity.getFragmentManager().beginTransaction()));
699        if (isMessageListInstalled()) {
700            getMessageListFragment().setSelectedMessage(Message.NO_MESSAGE);
701        }
702    }
703
704    private class CommandButtonCallback implements MessageCommandButtonView.Callback {
705        @Override
706        public void onMoveToNewer() {
707            moveToNewer();
708        }
709
710        @Override
711        public void onMoveToOlder() {
712            moveToOlder();
713        }
714    }
715
716    private void onCurrentMessageGone() {
717        switch (Preferences.getPreferences(mActivity).getAutoAdvanceDirection()) {
718            case Preferences.AUTO_ADVANCE_NEWER:
719                if (moveToNewer()) return;
720                break;
721            case Preferences.AUTO_ADVANCE_OLDER:
722                if (moveToOlder()) return;
723                break;
724        }
725        // Last message in the box or AUTO_ADVANCE_MESSAGE_LIST.  Go back to message list.
726        goBackToMailbox();
727    }
728
729    /**
730     * Potentially create a new {@link MessageOrderManager}; if it's not already started or if
731     * the account has changed, and sync it to the current message.
732     */
733    private void updateMessageOrderManager() {
734        if (!isMessageViewInstalled()) {
735            return;
736        }
737        final long mailboxId = getMessageListMailboxId();
738        if (mOrderManager == null || mOrderManager.getMailboxId() != mailboxId) {
739            stopMessageOrderManager();
740            mOrderManager =
741                new MessageOrderManager(mActivity, mailboxId, mMessageOrderManagerCallback);
742        }
743        mOrderManager.moveTo(getMessageId());
744    }
745
746    private class MessageOrderManagerCallback implements MessageOrderManager.Callback {
747        @Override
748        public void onMessagesChanged() {
749            updateNavigationArrows();
750        }
751
752        @Override
753        public void onMessageNotFound() {
754            // Current message gone.
755            goBackToMailbox();
756        }
757    }
758
759    /**
760     * Stop {@link MessageOrderManager}.
761     */
762    private void stopMessageOrderManager() {
763        if (mOrderManager != null) {
764            mOrderManager.close();
765            mOrderManager = null;
766        }
767    }
768
769    /**
770     * Disable/enable the move-to-newer/older buttons.
771     */
772    private void updateNavigationArrows() {
773        if (mOrderManager == null) {
774            // shouldn't happen, but just in case
775            mMessageCommandButtons.enableNavigationButtons(false, false, 0, 0);
776        } else {
777            mMessageCommandButtons.enableNavigationButtons(
778                    mOrderManager.canMoveToNewer(), mOrderManager.canMoveToOlder(),
779                    mOrderManager.getCurrentPosition(), mOrderManager.getTotalMessageCount());
780        }
781    }
782
783    private boolean moveToOlder() {
784        if ((mOrderManager != null) && mOrderManager.moveToOlder()) {
785            updateMessageView(mOrderManager.getCurrentMessageId());
786            return true;
787        }
788        return false;
789    }
790
791    private boolean moveToNewer() {
792        if ((mOrderManager != null) && mOrderManager.moveToNewer()) {
793            updateMessageView(mOrderManager.getCurrentMessageId());
794            return true;
795        }
796        return false;
797    }
798
799    /** {@inheritDoc} */
800    @Override
801    public boolean onBackPressed(boolean isSystemBackKey) {
802        // Super's method has precedence.  Must call it first.
803        if (super.onBackPressed(isSystemBackKey)) {
804            return true;
805        }
806        if (mThreePane.onBackPressed(isSystemBackKey)) {
807            return true;
808        }
809        if (isMailboxListInstalled() && getMailboxListFragment().navigateUp()) {
810            return true;
811        }
812        return false;
813    }
814
815    @Override protected boolean canSearch() {
816        // Search is always enabled on two-pane. (if the account supports it)
817        return true;
818    }
819
820    /**
821     * Handles the "refresh" option item.  Opens the settings activity.
822     * TODO used by experimental code in the activity -- otherwise can be private.
823     */
824    @Override
825    public void onRefresh() {
826        // Cancel previously running instance if any.
827        new RefreshTask(mTaskTracker, mActivity, getActualAccountId(),
828                getMessageListMailboxId()).cancelPreviousAndExecuteParallel();
829    }
830
831    /**
832     * Class to handle refresh.
833     *
834     * When the user press "refresh",
835     * <ul>
836     *   <li>Refresh the current mailbox, if it's refreshable.  (e.g. don't refresh combined inbox,
837     *       drafts, etc.
838     *   <li>Refresh the mailbox list, if it hasn't been refreshed in the last
839     *       {@link #MAILBOX_REFRESH_MIN_INTERVAL}.
840     *   <li>Refresh inbox, if it's not the current mailbox and it hasn't been refreshed in the last
841     *       {@link #INBOX_AUTO_REFRESH_MIN_INTERVAL}.
842     * </ul>
843     */
844    @VisibleForTesting
845    static class RefreshTask extends EmailAsyncTask<Void, Void, Boolean> {
846        private final Clock mClock;
847        private final Context mContext;
848        private final long mAccountId;
849        private final long mMailboxId;
850        private final RefreshManager mRefreshManager;
851        @VisibleForTesting
852        long mInboxId;
853
854        public RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId,
855                long mailboxId) {
856            this(tracker, context, accountId, mailboxId, Clock.INSTANCE,
857                    RefreshManager.getInstance(context));
858        }
859
860        @VisibleForTesting
861        RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId,
862                long mailboxId, Clock clock, RefreshManager refreshManager) {
863            super(tracker);
864            mClock = clock;
865            mContext = context;
866            mRefreshManager = refreshManager;
867            mAccountId = accountId;
868            mMailboxId = mailboxId;
869        }
870
871        /**
872         * Do DB access on a worker thread.
873         */
874        @Override
875        protected Boolean doInBackground(Void... params) {
876            mInboxId = Account.getInboxId(mContext, mAccountId);
877            return Mailbox.isRefreshable(mContext, mMailboxId);
878        }
879
880        /**
881         * Do the actual refresh.
882         */
883        @Override
884        protected void onPostExecute(Boolean isCurrentMailboxRefreshable) {
885            if (isCancelled() || isCurrentMailboxRefreshable == null) {
886                return;
887            }
888            if (isCurrentMailboxRefreshable) {
889                mRefreshManager.refreshMessageList(mAccountId, mMailboxId, false);
890            }
891            // Refresh mailbox list
892            if (mAccountId != Account.NO_ACCOUNT) {
893                if (shouldRefreshMailboxList()) {
894                    mRefreshManager.refreshMailboxList(mAccountId);
895                }
896            }
897            // Refresh inbox
898            if (shouldAutoRefreshInbox()) {
899                mRefreshManager.refreshMessageList(mAccountId, mInboxId, false);
900            }
901        }
902
903        /**
904         * @return true if the mailbox list of the current account hasn't been refreshed
905         * in the last {@link #MAILBOX_REFRESH_MIN_INTERVAL}.
906         */
907        @VisibleForTesting
908        boolean shouldRefreshMailboxList() {
909            if (mRefreshManager.isMailboxListRefreshing(mAccountId)) {
910                return false;
911            }
912            final long nextRefreshTime = mRefreshManager.getLastMailboxListRefreshTime(mAccountId)
913                    + MAILBOX_REFRESH_MIN_INTERVAL;
914            if (nextRefreshTime > mClock.getTime()) {
915                return false;
916            }
917            return true;
918        }
919
920        /**
921         * @return true if the inbox of the current account hasn't been refreshed
922         * in the last {@link #INBOX_AUTO_REFRESH_MIN_INTERVAL}.
923         */
924        @VisibleForTesting
925        boolean shouldAutoRefreshInbox() {
926            if (mInboxId == mMailboxId) {
927                return false; // Current ID == inbox.  No need to auto-refresh.
928            }
929            if (mRefreshManager.isMessageListRefreshing(mInboxId)) {
930                return false;
931            }
932            final long nextRefreshTime = mRefreshManager.getLastMessageListRefreshTime(mInboxId)
933                    + INBOX_AUTO_REFRESH_MIN_INTERVAL;
934            if (nextRefreshTime > mClock.getTime()) {
935                return false;
936            }
937            return true;
938        }
939    }
940
941    private class ActionBarControllerCallback implements ActionBarController.Callback {
942        @Override
943        public String getCurrentMailboxName() {
944            return mCurrentMailboxName;
945        }
946
947        @Override
948        public int getCurrentMailboxUnreadCount() {
949            return mCurrentMailboxUnreadCount;
950        }
951
952        @Override
953        public long getUIAccountId() {
954            return UIControllerTwoPane.this.getUIAccountId();
955        }
956
957        @Override
958        public boolean isAccountSelected() {
959            return UIControllerTwoPane.this.isAccountSelected();
960        }
961
962        @Override
963        public void onAccountSelected(long accountId) {
964            switchAccount(accountId);
965        }
966
967        @Override
968        public void onMailboxSelected(long mailboxId) {
969            openMailbox(getUIAccountId(), mailboxId);
970        }
971
972        @Override
973        public void onNoAccountsFound() {
974            Welcome.actionStart(mActivity);
975            mActivity.finish();
976        }
977
978        @Override
979        public boolean shouldShowMailboxName() {
980            // Show when the left pane is hidden.
981            return (mThreePane.getVisiblePanes() & ThreePaneLayout.PANE_LEFT) == 0;
982        }
983
984        @Override
985        public boolean shouldShowUp() {
986            final int visiblePanes = mThreePane.getVisiblePanes();
987            final boolean leftPaneHidden = ((visiblePanes & ThreePaneLayout.PANE_LEFT) == 0);
988            return leftPaneHidden
989                    || (isMailboxListInstalled() && !getMailboxListFragment().isRoot());
990        }
991
992        @Override
993        public void onSearchSubmit(final String queryTerm) {
994            final long accountId = getUIAccountId();
995            if (!Account.isNormalAccount(accountId)) {
996                return; // Invalid account to search from.
997            }
998
999            // TODO: do a global search for EAS inbox.
1000            final long mailboxId = getMessageListMailboxId();
1001
1002            if (Email.DEBUG) {
1003                Log.d(Logging.LOG_TAG, "Submitting search: " + queryTerm);
1004            }
1005
1006            mActivity.startActivity(EmailActivity.createSearchIntent(
1007                    mActivity, accountId, mailboxId, queryTerm));
1008        }
1009
1010        @Override
1011        public void onSearchExit() {
1012            // STOPSHIP If the activity is a "search" instance, finish() it.
1013        }
1014    }
1015}
1016