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