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