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