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