UIControllerTwoPane.java revision 1b4b6cf560a5ce9968a103b7c25cb402cdb7d396
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.activity;
18
19import android.app.Activity;
20import android.app.FragmentTransaction;
21import android.content.Context;
22import android.os.Bundle;
23import android.util.Log;
24
25import com.android.email.Clock;
26import com.android.email.Email;
27import com.android.email.MessageListContext;
28import com.android.email.Preferences;
29import com.android.email.R;
30import com.android.email.RefreshManager;
31import com.android.emailcommon.Logging;
32import com.android.emailcommon.provider.Account;
33import com.android.emailcommon.provider.EmailContent.Message;
34import com.android.emailcommon.provider.Mailbox;
35import com.android.emailcommon.utility.EmailAsyncTask;
36import com.google.common.annotations.VisibleForTesting;
37
38import java.util.Set;
39
40/**
41 * UI Controller for x-large devices.  Supports a multi-pane layout.
42 *
43 * Note: Always use {@link #commitFragmentTransaction} to operate fragment transactions,
44 * so that we can easily switch between synchronous and asynchronous transactions.
45 */
46class UIControllerTwoPane extends UIControllerBase implements ThreePaneLayout.Callback {
47    @VisibleForTesting
48    static final int MAILBOX_REFRESH_MIN_INTERVAL = 30 * 1000; // in milliseconds
49
50    @VisibleForTesting
51    static final int INBOX_AUTO_REFRESH_MIN_INTERVAL = 10 * 1000; // in milliseconds
52
53    // Other UI elements
54    private ThreePaneLayout mThreePane;
55
56    private MessageCommandButtonView mMessageCommandButtons;
57
58    public UIControllerTwoPane(EmailActivity activity) {
59        super(activity);
60    }
61
62    @Override
63    public int getLayoutId() {
64        return R.layout.email_activity_two_pane;
65    }
66
67    // ThreePaneLayoutCallback
68    @Override
69    public void onVisiblePanesChanged(int previousVisiblePanes) {
70        // If the right pane is gone, remove the message view.
71        final int visiblePanes = mThreePane.getVisiblePanes();
72
73        if (((visiblePanes & ThreePaneLayout.PANE_RIGHT) == 0) &&
74                ((previousVisiblePanes & ThreePaneLayout.PANE_RIGHT) != 0)) {
75            // Message view just got hidden
76            unselectMessage();
77        }
78        // Disable CAB when the message list is not visible.
79        if (isMessageListInstalled()) {
80            getMessageListFragment().onHidden((visiblePanes & ThreePaneLayout.PANE_MIDDLE) == 0);
81        }
82        refreshActionBar();
83    }
84
85    // MailboxListFragment$Callback
86    @Override
87    public void onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation) {
88        setListContext(MessageListContext.forMailbox(accountId, mailboxId));
89        if (getMessageListMailboxId() != mListContext.getMailboxId()) {
90            updateMessageList(true);
91        }
92    }
93
94    // MailboxListFragment$Callback
95    @Override
96    public void onAccountSelected(long accountId) {
97        // It's from combined view, so "forceShowInbox" doesn't really matter.
98        // (We're always switching accounts.)
99        switchAccount(accountId, true);
100    }
101
102    // MailboxListFragment$Callback
103    @Override
104    public void onParentMailboxChanged() {
105        refreshActionBar();
106    }
107
108    // MessageListFragment$Callback
109    @Override
110    public void onMessageOpen(long messageId, long messageMailboxId, long listMailboxId,
111            int type) {
112        if (type == MessageListFragment.Callback.TYPE_DRAFT) {
113            MessageCompose.actionEditDraft(mActivity, messageId);
114        } else {
115            if (getMessageId() != messageId) {
116                navigateToMessage(messageId);
117                mThreePane.showRightPane();
118            }
119        }
120    }
121
122    // MessageListFragment$Callback
123    @Override
124    public void onMailboxNotFound() {
125        Log.e(Logging.LOG_TAG, "unable to find mailbox");
126    }
127
128    // MessageListFragment$Callback
129    @Override
130    public void onEnterSelectionMode(boolean enter) {
131    }
132
133    // MessageListFragment$Callback
134    /**
135     * Apply the auto-advance policy upon initation of a batch command that could potentially
136     * affect the currently selected conversation.
137     */
138    @Override
139    public void onAdvancingOpAccepted(Set<Long> affectedMessages) {
140        if (!isMessageViewInstalled()) {
141            // Do nothing if message view is not visible.
142            return;
143        }
144
145        final MessageOrderManager orderManager = getMessageOrderManager();
146        int autoAdvanceDir = Preferences.getPreferences(mActivity).getAutoAdvanceDirection();
147        if ((autoAdvanceDir == Preferences.AUTO_ADVANCE_MESSAGE_LIST) || (orderManager == null)) {
148            if (affectedMessages.contains(getMessageId())) {
149                goBackToMailbox();
150            }
151            return;
152        }
153
154        // Navigate to the first unselected item in the appropriate direction.
155        switch (autoAdvanceDir) {
156            case Preferences.AUTO_ADVANCE_NEWER:
157                while (affectedMessages.contains(orderManager.getCurrentMessageId())) {
158                    if (!orderManager.moveToNewer()) {
159                        goBackToMailbox();
160                        return;
161                    }
162                }
163                navigateToMessage(orderManager.getCurrentMessageId());
164                break;
165
166            case Preferences.AUTO_ADVANCE_OLDER:
167                while (affectedMessages.contains(orderManager.getCurrentMessageId())) {
168                    if (!orderManager.moveToOlder()) {
169                        goBackToMailbox();
170                        return;
171                    }
172                }
173                navigateToMessage(orderManager.getCurrentMessageId());
174                break;
175        }
176    }
177
178    // MessageListFragment$Callback
179    @Override
180    public void onListLoaded() {
181    }
182
183    // MessageListFragment$Callback
184    @Override
185    public boolean onDragStarted() {
186        Log.w(Logging.LOG_TAG, "Drag started");
187
188        if (((mListContext != null) && mListContext.isSearch())
189                || !mThreePane.isLeftPaneVisible()) {
190            // D&D not allowed.
191            return false;
192        }
193
194        // STOPSHIP Save the current mailbox list
195
196        return true;
197    }
198
199    // MessageListFragment$Callback
200    @Override
201    public void onDragEnded() {
202        Log.w(Logging.LOG_TAG, "Drag ended");
203
204        // STOPSHIP Restore the saved mailbox list
205    }
206
207
208    // MessageViewFragment$Callback
209    @Override
210    public boolean onUrlInMessageClicked(String url) {
211        return ActivityHelper.openUrlInMessage(mActivity, url, getActualAccountId());
212    }
213
214    // MessageViewFragment$Callback
215    @Override
216    public void onLoadMessageStarted() {
217    }
218
219    // MessageViewFragment$Callback
220    @Override
221    public void onLoadMessageFinished() {
222    }
223
224    // MessageViewFragment$Callback
225    @Override
226    public void onLoadMessageError(String errorMessage) {
227    }
228
229    // MessageViewFragment$Callback
230    @Override
231    public void onCalendarLinkClicked(long epochEventStartTime) {
232        ActivityHelper.openCalendar(mActivity, epochEventStartTime);
233    }
234
235    // MessageViewFragment$Callback
236    @Override
237    public void onForward() {
238        MessageCompose.actionForward(mActivity, getMessageId());
239    }
240
241    // MessageViewFragment$Callback
242    @Override
243    public void onReply() {
244        MessageCompose.actionReply(mActivity, getMessageId(), false);
245    }
246
247    // MessageViewFragment$Callback
248    @Override
249    public void onReplyAll() {
250        MessageCompose.actionReply(mActivity, getMessageId(), true);
251    }
252
253    /**
254     * Must be called just after the activity sets up the content view.
255     */
256    @Override
257    public void onActivityViewReady() {
258        super.onActivityViewReady();
259
260        // Set up content
261        mThreePane = (ThreePaneLayout) mActivity.findViewById(R.id.three_pane);
262        mThreePane.setCallback(this);
263
264        mMessageCommandButtons = mThreePane.getMessageCommandButtons();
265        mMessageCommandButtons.setCallback(new CommandButtonCallback());
266    }
267
268    @Override
269    protected ActionBarController createActionBarController(Activity activity) {
270        return new ActionBarController(activity, activity.getLoaderManager(),
271                activity.getActionBar(), new ActionBarControllerCallback());
272    }
273
274    /**
275     * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
276     *
277     * @see #getActualAccountId()
278     */
279    @Override
280    public long getUIAccountId() {
281        return isMailboxListInstalled() ? getMailboxListFragment().getAccountId()
282                :Account.NO_ACCOUNT;
283    }
284
285
286    /*
287     * STOPSHIP Remove this -- see the base class method.
288     */
289    @Override
290    public long getMailboxSettingsMailboxId() {
291        return getMessageListMailboxId();
292    }
293
294    /**
295     * @return true if refresh is in progress for the current mailbox.
296     */
297    @Override
298    protected boolean isRefreshInProgress() {
299        long messageListMailboxId = getMessageListMailboxId();
300        return (messageListMailboxId >= 0)
301                && mRefreshManager.isMessageListRefreshing(messageListMailboxId);
302    }
303
304    /**
305     * @return true if the UI should enable the "refresh" command.
306     */
307    @Override
308    protected boolean isRefreshEnabled() {
309        // - Don't show for combined inboxes, but
310        // - Show even for non-refreshable mailboxes, in which case we refresh the mailbox list
311        return getActualAccountId() != Account.NO_ACCOUNT;
312    }
313
314
315    /** {@inheritDoc} */
316    @Override
317    public void onSaveInstanceState(Bundle outState) {
318        super.onSaveInstanceState(outState);
319    }
320
321    /** {@inheritDoc} */
322    @Override
323    public void onRestoreInstanceState(Bundle savedInstanceState) {
324        super.onRestoreInstanceState(savedInstanceState);
325    }
326
327    @Override
328    protected void installMessageListFragment(MessageListFragment fragment) {
329        super.installMessageListFragment(fragment);
330
331        if (isMailboxListInstalled()) {
332            getMailboxListFragment().setHighlightedMailbox(fragment.getMailboxId());
333        }
334    }
335
336    @Override
337    protected void installMessageViewFragment(MessageViewFragment fragment) {
338        super.installMessageViewFragment(fragment);
339
340        if (isMessageListInstalled()) {
341            getMessageListFragment().setSelectedMessage(fragment.getMessageId());
342        }
343    }
344
345
346    /**
347     * Commit a {@link FragmentTransaction}.
348     */
349    private void commitFragmentTransaction(FragmentTransaction ft) {
350        if (DEBUG_FRAGMENTS) {
351            Log.d(Logging.LOG_TAG, this + " commitFragmentTransaction: " + ft);
352        }
353        if (!ft.isEmpty()) {
354            // STOPSHIP Don't use AllowingStateLoss.  See b/4519430
355            ft.commitAllowingStateLoss();
356            mFragmentManager.executePendingTransactions();
357        }
358    }
359
360    @Override
361    public void openInternal(final MessageListContext listContext, final long messageId) {
362        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
363            Log.d(Logging.LOG_TAG, this + " open " + listContext);
364        }
365
366        final FragmentTransaction ft = mFragmentManager.beginTransaction();
367        updateMailboxList(ft, true);
368        updateMessageList(ft, true);
369
370        if (messageId != Message.NO_MESSAGE) {
371            updateMessageView(ft, messageId);
372            mThreePane.showRightPane();
373        } else if (mListContext.isSearch()) {
374            mThreePane.showRightPane();
375        } else {
376            mThreePane.showLeftPane();
377        }
378        commitFragmentTransaction(ft);
379    }
380
381    /**
382     * Loads the given account and optionally selects the given mailbox and message. If the
383     * specified account is already selected, no actions will be performed unless
384     * <code>forceReload</code> is <code>true</code>.
385     *
386     * @param ft {@link FragmentTransaction} to use.
387     * @param clearDependentPane if true, the message list and the message view will be cleared
388     */
389    private void updateMailboxList(FragmentTransaction ft, boolean clearDependentPane) {
390        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
391            Log.d(Logging.LOG_TAG, this + " updateMailboxList " + mListContext);
392        }
393
394        long accountId = mListContext.mAccountId;
395        long mailboxId = mListContext.getMailboxId();
396        if ((getUIAccountId() != accountId) || (getMailboxListMailboxId() != mailboxId)) {
397            removeMailboxListFragment(ft);
398            ft.add(mThreePane.getLeftPaneId(),
399                    MailboxListFragment.newInstance(accountId, mailboxId, true));
400        }
401        if (clearDependentPane) {
402            removeMessageListFragment(ft);
403            removeMessageViewFragment(ft);
404        }
405    }
406
407    /**
408     * Go back to a mailbox list view. If a message view is currently active, it will
409     * be hidden.
410     */
411    private void goBackToMailbox() {
412        if (isMessageViewInstalled()) {
413            mThreePane.showLeftPane(); // Show mailbox list
414        }
415    }
416
417    /**
418     * Show the message list fragment for the given mailbox.
419     *
420     * @param ft {@link FragmentTransaction} to use.
421     */
422    private void updateMessageList(FragmentTransaction ft, boolean clearDependentPane) {
423        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
424            Log.d(Logging.LOG_TAG, this + " updateMessageList " + mListContext);
425        }
426
427        if (mListContext.getMailboxId() != getMessageListMailboxId()) {
428            removeMessageListFragment(ft);
429            ft.add(mThreePane.getMiddlePaneId(), MessageListFragment.newInstance(mListContext));
430        }
431        if (clearDependentPane) {
432            removeMessageViewFragment(ft);
433        }
434    }
435
436    /**
437     * Shortcut to call {@link #updateMessageList(FragmentTransaction, boolean)} and
438     * commit.
439     */
440    private void updateMessageList(boolean clearDependentPane) {
441        FragmentTransaction ft = mFragmentManager.beginTransaction();
442        updateMessageList(ft, clearDependentPane);
443        commitFragmentTransaction(ft);
444    }
445
446    /**
447     * Show a message on the message view.
448     *
449     * @param ft {@link FragmentTransaction} to use.
450     * @param messageId ID of the mailbox to load. Must never be {@link Message#NO_MESSAGE}.
451     */
452    private void updateMessageView(FragmentTransaction ft, long messageId) {
453        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
454            Log.d(Logging.LOG_TAG, this + " updateMessageView messageId=" + messageId);
455        }
456        if (messageId == Message.NO_MESSAGE) {
457            throw new IllegalArgumentException();
458        }
459
460        if (messageId == getMessageId()) {
461            return; // nothing to do.
462        }
463
464        removeMessageViewFragment(ft);
465
466        ft.add(mThreePane.getRightPaneId(), MessageViewFragment.newInstance(messageId));
467    }
468
469    /**
470     * Shortcut to call {@link #updateMessageView(FragmentTransaction, long)} and commit.
471     */
472    @Override protected void navigateToMessage(long messageId) {
473        FragmentTransaction ft = mFragmentManager.beginTransaction();
474        updateMessageView(ft, messageId);
475        commitFragmentTransaction(ft);
476    }
477
478    /**
479     * Remove the message view if shown.
480     */
481    private void unselectMessage() {
482        commitFragmentTransaction(removeMessageViewFragment(mFragmentManager.beginTransaction()));
483        if (isMessageListInstalled()) {
484            getMessageListFragment().setSelectedMessage(Message.NO_MESSAGE);
485        }
486        stopMessageOrderManager();
487    }
488
489    private class CommandButtonCallback implements MessageCommandButtonView.Callback {
490        @Override
491        public void onMoveToNewer() {
492            moveToNewer();
493        }
494
495        @Override
496        public void onMoveToOlder() {
497            moveToOlder();
498        }
499    }
500
501    /**
502     * Disable/enable the move-to-newer/older buttons.
503     */
504    @Override protected void updateNavigationArrows() {
505        final MessageOrderManager orderManager = getMessageOrderManager();
506        if (orderManager == null) {
507            // shouldn't happen, but just in case
508            mMessageCommandButtons.enableNavigationButtons(false, false, 0, 0);
509        } else {
510            mMessageCommandButtons.enableNavigationButtons(
511                    orderManager.canMoveToNewer(), orderManager.canMoveToOlder(),
512                    orderManager.getCurrentPosition(), orderManager.getTotalMessageCount());
513        }
514    }
515
516    /** {@inheritDoc} */
517    @Override
518    public boolean onBackPressed(boolean isSystemBackKey) {
519        if (!mThreePane.isPaneCollapsible()) {
520            if (mActionBarController.onBackPressed(isSystemBackKey)) {
521                return true;
522            }
523
524            if (mThreePane.showLeftPane()) {
525                return true;
526            }
527        } else {
528            // If it's not the system back key, always attempt to uncollapse the left pane first.
529            if (!isSystemBackKey && mThreePane.uncollapsePane()) {
530                return true;
531            }
532
533            if (mActionBarController.onBackPressed(isSystemBackKey)) {
534                return true;
535            }
536
537            if (mThreePane.showLeftPane()) {
538                return true;
539            }
540        }
541
542        if (isMailboxListInstalled() && getMailboxListFragment().navigateUp()) {
543            return true;
544        }
545        return false;
546    }
547
548    /**
549     * Handles the "refresh" option item.  Opens the settings activity.
550     * TODO used by experimental code in the activity -- otherwise can be private.
551     */
552    @Override
553    public void onRefresh() {
554        // Cancel previously running instance if any.
555        new RefreshTask(mTaskTracker, mActivity, getActualAccountId(),
556                getMessageListMailboxId()).cancelPreviousAndExecuteParallel();
557    }
558
559    /**
560     * Class to handle refresh.
561     *
562     * When the user press "refresh",
563     * <ul>
564     *   <li>Refresh the current mailbox, if it's refreshable.  (e.g. don't refresh combined inbox,
565     *       drafts, etc.
566     *   <li>Refresh the mailbox list, if it hasn't been refreshed in the last
567     *       {@link #MAILBOX_REFRESH_MIN_INTERVAL}.
568     *   <li>Refresh inbox, if it's not the current mailbox and it hasn't been refreshed in the last
569     *       {@link #INBOX_AUTO_REFRESH_MIN_INTERVAL}.
570     * </ul>
571     */
572    @VisibleForTesting
573    static class RefreshTask extends EmailAsyncTask<Void, Void, Boolean> {
574        private final Clock mClock;
575        private final Context mContext;
576        private final long mAccountId;
577        private final long mMailboxId;
578        private final RefreshManager mRefreshManager;
579        @VisibleForTesting
580        long mInboxId;
581
582        public RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId,
583                long mailboxId) {
584            this(tracker, context, accountId, mailboxId, Clock.INSTANCE,
585                    RefreshManager.getInstance(context));
586        }
587
588        @VisibleForTesting
589        RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId,
590                long mailboxId, Clock clock, RefreshManager refreshManager) {
591            super(tracker);
592            mClock = clock;
593            mContext = context;
594            mRefreshManager = refreshManager;
595            mAccountId = accountId;
596            mMailboxId = mailboxId;
597        }
598
599        /**
600         * Do DB access on a worker thread.
601         */
602        @Override
603        protected Boolean doInBackground(Void... params) {
604            mInboxId = Account.getInboxId(mContext, mAccountId);
605            return Mailbox.isRefreshable(mContext, mMailboxId);
606        }
607
608        /**
609         * Do the actual refresh.
610         */
611        @Override
612        protected void onSuccess(Boolean isCurrentMailboxRefreshable) {
613            if (isCurrentMailboxRefreshable == null) {
614                return;
615            }
616            if (isCurrentMailboxRefreshable) {
617                mRefreshManager.refreshMessageList(mAccountId, mMailboxId, false);
618            }
619            // Refresh mailbox list
620            if (mAccountId != Account.NO_ACCOUNT) {
621                if (shouldRefreshMailboxList()) {
622                    mRefreshManager.refreshMailboxList(mAccountId);
623                }
624            }
625            // Refresh inbox
626            if (shouldAutoRefreshInbox()) {
627                mRefreshManager.refreshMessageList(mAccountId, mInboxId, false);
628            }
629        }
630
631        /**
632         * @return true if the mailbox list of the current account hasn't been refreshed
633         * in the last {@link #MAILBOX_REFRESH_MIN_INTERVAL}.
634         */
635        @VisibleForTesting
636        boolean shouldRefreshMailboxList() {
637            if (mRefreshManager.isMailboxListRefreshing(mAccountId)) {
638                return false;
639            }
640            final long nextRefreshTime = mRefreshManager.getLastMailboxListRefreshTime(mAccountId)
641                    + MAILBOX_REFRESH_MIN_INTERVAL;
642            if (nextRefreshTime > mClock.getTime()) {
643                return false;
644            }
645            return true;
646        }
647
648        /**
649         * @return true if the inbox of the current account hasn't been refreshed
650         * in the last {@link #INBOX_AUTO_REFRESH_MIN_INTERVAL}.
651         */
652        @VisibleForTesting
653        boolean shouldAutoRefreshInbox() {
654            if (mInboxId == mMailboxId) {
655                return false; // Current ID == inbox.  No need to auto-refresh.
656            }
657            if (mRefreshManager.isMessageListRefreshing(mInboxId)) {
658                return false;
659            }
660            final long nextRefreshTime = mRefreshManager.getLastMessageListRefreshTime(mInboxId)
661                    + INBOX_AUTO_REFRESH_MIN_INTERVAL;
662            if (nextRefreshTime > mClock.getTime()) {
663                return false;
664            }
665            return true;
666        }
667    }
668
669    private class ActionBarControllerCallback implements ActionBarController.Callback {
670
671        @Override
672        public long getUIAccountId() {
673            return UIControllerTwoPane.this.getUIAccountId();
674        }
675
676        @Override
677        public long getMailboxId() {
678            return getMessageListMailboxId();
679        }
680
681        @Override
682        public boolean isAccountSelected() {
683            return UIControllerTwoPane.this.isAccountSelected();
684        }
685
686        @Override
687        public void onAccountSelected(long accountId) {
688            switchAccount(accountId, false);
689        }
690
691        @Override
692        public void onMailboxSelected(long accountId, long mailboxId) {
693            openMailbox(accountId, mailboxId);
694        }
695
696        @Override
697        public void onNoAccountsFound() {
698            Welcome.actionStart(mActivity);
699            mActivity.finish();
700        }
701
702        @Override
703        public int getTitleMode() {
704            if (mThreePane.isLeftPaneVisible()) {
705                // Mailbox list visible
706                return TITLE_MODE_ACCOUNT_NAME_ONLY;
707            } else {
708                // Mailbox list hidden
709                return TITLE_MODE_ACCOUNT_WITH_MAILBOX;
710            }
711        }
712
713        public String getMessageSubject() {
714            if (isMessageViewInstalled() && getMessageViewFragment().isMessageOpen()) {
715                return getMessageViewFragment().getMessage().mSubject;
716            } else {
717                return null;
718            }
719        }
720
721        @Override
722        public boolean shouldShowUp() {
723            final int visiblePanes = mThreePane.getVisiblePanes();
724            final boolean leftPaneHidden = ((visiblePanes & ThreePaneLayout.PANE_LEFT) == 0);
725            return leftPaneHidden
726                    || (isMailboxListInstalled() && getMailboxListFragment().canNavigateUp());
727        }
728
729        @Override
730        public String getSearchHint() {
731            return UIControllerTwoPane.this.getSearchHint();
732        }
733
734        @Override
735        public void onSearchSubmit(final String queryTerm) {
736            UIControllerTwoPane.this.onSearchSubmit(queryTerm);
737        }
738
739        @Override
740        public void onSearchExit() {
741            UIControllerTwoPane.this.onSearchExit();
742        }
743    }
744}
745