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