UIControllerTwoPane.java revision 1ddc2ca4a7e59189099d273bd1d364838dd7ce35
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    @Override
286    public long getMailboxSettingsMailboxId() {
287        return getMessageListMailboxId();
288    }
289
290    /**
291     * @return true if refresh is in progress for the current mailbox.
292     */
293    @Override
294    protected boolean isRefreshInProgress() {
295        long messageListMailboxId = getMessageListMailboxId();
296        return (messageListMailboxId >= 0)
297                && mRefreshManager.isMessageListRefreshing(messageListMailboxId);
298    }
299
300    /**
301     * @return true if the UI should enable the "refresh" command.
302     */
303    @Override
304    protected boolean isRefreshEnabled() {
305        // - Don't show for combined inboxes, but
306        // - Show even for non-refreshable mailboxes, in which case we refresh the mailbox list
307        return getActualAccountId() != Account.NO_ACCOUNT;
308    }
309
310
311    /** {@inheritDoc} */
312    @Override
313    public void onSaveInstanceState(Bundle outState) {
314        super.onSaveInstanceState(outState);
315    }
316
317    /** {@inheritDoc} */
318    @Override
319    public void onRestoreInstanceState(Bundle savedInstanceState) {
320        super.onRestoreInstanceState(savedInstanceState);
321    }
322
323    @Override
324    protected void installMessageListFragment(MessageListFragment fragment) {
325        super.installMessageListFragment(fragment);
326
327        if (isMailboxListInstalled()) {
328            getMailboxListFragment().setHighlightedMailbox(fragment.getMailboxId());
329        }
330    }
331
332    @Override
333    protected void installMessageViewFragment(MessageViewFragment fragment) {
334        super.installMessageViewFragment(fragment);
335
336        if (isMessageListInstalled()) {
337            getMessageListFragment().setSelectedMessage(fragment.getMessageId());
338        }
339    }
340
341
342    /**
343     * Commit a {@link FragmentTransaction}.
344     */
345    private void commitFragmentTransaction(FragmentTransaction ft) {
346        if (DEBUG_FRAGMENTS) {
347            Log.d(Logging.LOG_TAG, this + " commitFragmentTransaction: " + ft);
348        }
349        if (!ft.isEmpty()) {
350            // STOPSHIP Don't use AllowingStateLoss.  See b/4519430
351            ft.commitAllowingStateLoss();
352            mFragmentManager.executePendingTransactions();
353        }
354    }
355
356    @Override
357    public void openInternal(final MessageListContext listContext, final long messageId) {
358        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
359            Log.d(Logging.LOG_TAG, this + " open " + listContext);
360        }
361
362        final FragmentTransaction ft = mFragmentManager.beginTransaction();
363        updateMailboxList(ft, true);
364        updateMessageList(ft, true);
365
366        if (messageId != Message.NO_MESSAGE) {
367            updateMessageView(ft, messageId);
368            mThreePane.showRightPane();
369        } else if (mListContext.isSearch()) {
370            mThreePane.showRightPane();
371        } else {
372            mThreePane.showLeftPane();
373        }
374        commitFragmentTransaction(ft);
375    }
376
377    /**
378     * Loads the given account and optionally selects the given mailbox and message. If the
379     * specified account is already selected, no actions will be performed unless
380     * <code>forceReload</code> is <code>true</code>.
381     *
382     * @param ft {@link FragmentTransaction} to use.
383     * @param clearDependentPane if true, the message list and the message view will be cleared
384     */
385    private void updateMailboxList(FragmentTransaction ft, boolean clearDependentPane) {
386        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
387            Log.d(Logging.LOG_TAG, this + " updateMailboxList " + mListContext);
388        }
389
390        long accountId = mListContext.mAccountId;
391        long mailboxId = mListContext.getMailboxId();
392        if ((getUIAccountId() != accountId) || (getMailboxListMailboxId() != mailboxId)) {
393            removeMailboxListFragment(ft);
394            ft.add(mThreePane.getLeftPaneId(),
395                    MailboxListFragment.newInstance(accountId, mailboxId, true));
396        }
397        if (clearDependentPane) {
398            removeMessageListFragment(ft);
399            removeMessageViewFragment(ft);
400        }
401    }
402
403    /**
404     * Go back to a mailbox list view. If a message view is currently active, it will
405     * be hidden.
406     */
407    private void goBackToMailbox() {
408        if (isMessageViewInstalled()) {
409            mThreePane.showLeftPane(); // Show mailbox list
410        }
411    }
412
413    /**
414     * Show the message list fragment for the given mailbox.
415     *
416     * @param ft {@link FragmentTransaction} to use.
417     */
418    private void updateMessageList(FragmentTransaction ft, boolean clearDependentPane) {
419        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
420            Log.d(Logging.LOG_TAG, this + " updateMessageList " + mListContext);
421        }
422
423        if (mListContext.getMailboxId() != getMessageListMailboxId()) {
424            removeMessageListFragment(ft);
425            ft.add(mThreePane.getMiddlePaneId(), MessageListFragment.newInstance(mListContext));
426        }
427        if (clearDependentPane) {
428            removeMessageViewFragment(ft);
429        }
430    }
431
432    /**
433     * Shortcut to call {@link #updateMessageList(FragmentTransaction, boolean)} and
434     * commit.
435     */
436    private void updateMessageList(boolean clearDependentPane) {
437        FragmentTransaction ft = mFragmentManager.beginTransaction();
438        updateMessageList(ft, clearDependentPane);
439        commitFragmentTransaction(ft);
440    }
441
442    /**
443     * Show a message on the message view.
444     *
445     * @param ft {@link FragmentTransaction} to use.
446     * @param messageId ID of the mailbox to load. Must never be {@link Message#NO_MESSAGE}.
447     */
448    private void updateMessageView(FragmentTransaction ft, long messageId) {
449        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
450            Log.d(Logging.LOG_TAG, this + " updateMessageView messageId=" + messageId);
451        }
452        if (messageId == Message.NO_MESSAGE) {
453            throw new IllegalArgumentException();
454        }
455
456        if (messageId == getMessageId()) {
457            return; // nothing to do.
458        }
459
460        removeMessageViewFragment(ft);
461
462        ft.add(mThreePane.getRightPaneId(), MessageViewFragment.newInstance(messageId));
463    }
464
465    /**
466     * Shortcut to call {@link #updateMessageView(FragmentTransaction, long)} and commit.
467     */
468    @Override protected void navigateToMessage(long messageId) {
469        FragmentTransaction ft = mFragmentManager.beginTransaction();
470        updateMessageView(ft, messageId);
471        commitFragmentTransaction(ft);
472    }
473
474    /**
475     * Remove the message view if shown.
476     */
477    private void unselectMessage() {
478        commitFragmentTransaction(removeMessageViewFragment(mFragmentManager.beginTransaction()));
479        if (isMessageListInstalled()) {
480            getMessageListFragment().setSelectedMessage(Message.NO_MESSAGE);
481        }
482        stopMessageOrderManager();
483    }
484
485    private class CommandButtonCallback implements MessageCommandButtonView.Callback {
486        @Override
487        public void onMoveToNewer() {
488            moveToNewer();
489        }
490
491        @Override
492        public void onMoveToOlder() {
493            moveToOlder();
494        }
495    }
496
497    /**
498     * Disable/enable the move-to-newer/older buttons.
499     */
500    @Override protected void updateNavigationArrows() {
501        final MessageOrderManager orderManager = getMessageOrderManager();
502        if (orderManager == null) {
503            // shouldn't happen, but just in case
504            mMessageCommandButtons.enableNavigationButtons(false, false, 0, 0);
505        } else {
506            mMessageCommandButtons.enableNavigationButtons(
507                    orderManager.canMoveToNewer(), orderManager.canMoveToOlder(),
508                    orderManager.getCurrentPosition(), orderManager.getTotalMessageCount());
509        }
510    }
511
512    /** {@inheritDoc} */
513    @Override
514    public boolean onBackPressed(boolean isSystemBackKey) {
515        if (!mThreePane.isPaneCollapsible()) {
516            if (mActionBarController.onBackPressed(isSystemBackKey)) {
517                return true;
518            }
519
520            if (mThreePane.showLeftPane()) {
521                return true;
522            }
523        } else {
524            // If it's not the system back key, always attempt to uncollapse the left pane first.
525            if (!isSystemBackKey && mThreePane.uncollapsePane()) {
526                return true;
527            }
528
529            if (mActionBarController.onBackPressed(isSystemBackKey)) {
530                return true;
531            }
532
533            if (mThreePane.showLeftPane()) {
534                return true;
535            }
536        }
537
538        if (isMailboxListInstalled() && getMailboxListFragment().navigateUp()) {
539            return true;
540        }
541        return false;
542    }
543
544    @Override
545    protected void onRefresh() {
546        // Cancel previously running instance if any.
547        new RefreshTask(mTaskTracker, mActivity, getActualAccountId(),
548                getMessageListMailboxId()).cancelPreviousAndExecuteParallel();
549    }
550
551    /**
552     * Class to handle refresh.
553     *
554     * When the user press "refresh",
555     * <ul>
556     *   <li>Refresh the current mailbox, if it's refreshable.  (e.g. don't refresh combined inbox,
557     *       drafts, etc.
558     *   <li>Refresh the mailbox list, if it hasn't been refreshed in the last
559     *       {@link #MAILBOX_REFRESH_MIN_INTERVAL}.
560     *   <li>Refresh inbox, if it's not the current mailbox and it hasn't been refreshed in the last
561     *       {@link #INBOX_AUTO_REFRESH_MIN_INTERVAL}.
562     * </ul>
563     */
564    @VisibleForTesting
565    static class RefreshTask extends EmailAsyncTask<Void, Void, Boolean> {
566        private final Clock mClock;
567        private final Context mContext;
568        private final long mAccountId;
569        private final long mMailboxId;
570        private final RefreshManager mRefreshManager;
571        @VisibleForTesting
572        long mInboxId;
573
574        public RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId,
575                long mailboxId) {
576            this(tracker, context, accountId, mailboxId, Clock.INSTANCE,
577                    RefreshManager.getInstance(context));
578        }
579
580        @VisibleForTesting
581        RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId,
582                long mailboxId, Clock clock, RefreshManager refreshManager) {
583            super(tracker);
584            mClock = clock;
585            mContext = context;
586            mRefreshManager = refreshManager;
587            mAccountId = accountId;
588            mMailboxId = mailboxId;
589        }
590
591        /**
592         * Do DB access on a worker thread.
593         */
594        @Override
595        protected Boolean doInBackground(Void... params) {
596            mInboxId = Account.getInboxId(mContext, mAccountId);
597            return Mailbox.isRefreshable(mContext, mMailboxId);
598        }
599
600        /**
601         * Do the actual refresh.
602         */
603        @Override
604        protected void onSuccess(Boolean isCurrentMailboxRefreshable) {
605            if (isCurrentMailboxRefreshable == null) {
606                return;
607            }
608            if (isCurrentMailboxRefreshable) {
609                mRefreshManager.refreshMessageList(mAccountId, mMailboxId, false);
610            }
611            // Refresh mailbox list
612            if (mAccountId != Account.NO_ACCOUNT) {
613                if (shouldRefreshMailboxList()) {
614                    mRefreshManager.refreshMailboxList(mAccountId);
615                }
616            }
617            // Refresh inbox
618            if (shouldAutoRefreshInbox()) {
619                mRefreshManager.refreshMessageList(mAccountId, mInboxId, false);
620            }
621        }
622
623        /**
624         * @return true if the mailbox list of the current account hasn't been refreshed
625         * in the last {@link #MAILBOX_REFRESH_MIN_INTERVAL}.
626         */
627        @VisibleForTesting
628        boolean shouldRefreshMailboxList() {
629            if (mRefreshManager.isMailboxListRefreshing(mAccountId)) {
630                return false;
631            }
632            final long nextRefreshTime = mRefreshManager.getLastMailboxListRefreshTime(mAccountId)
633                    + MAILBOX_REFRESH_MIN_INTERVAL;
634            if (nextRefreshTime > mClock.getTime()) {
635                return false;
636            }
637            return true;
638        }
639
640        /**
641         * @return true if the inbox of the current account hasn't been refreshed
642         * in the last {@link #INBOX_AUTO_REFRESH_MIN_INTERVAL}.
643         */
644        @VisibleForTesting
645        boolean shouldAutoRefreshInbox() {
646            if (mInboxId == mMailboxId) {
647                return false; // Current ID == inbox.  No need to auto-refresh.
648            }
649            if (mRefreshManager.isMessageListRefreshing(mInboxId)) {
650                return false;
651            }
652            final long nextRefreshTime = mRefreshManager.getLastMessageListRefreshTime(mInboxId)
653                    + INBOX_AUTO_REFRESH_MIN_INTERVAL;
654            if (nextRefreshTime > mClock.getTime()) {
655                return false;
656            }
657            return true;
658        }
659    }
660
661    private class ActionBarControllerCallback implements ActionBarController.Callback {
662
663        @Override
664        public long getUIAccountId() {
665            return UIControllerTwoPane.this.getUIAccountId();
666        }
667
668        @Override
669        public long getMailboxId() {
670            return getMessageListMailboxId();
671        }
672
673        @Override
674        public boolean isAccountSelected() {
675            return UIControllerTwoPane.this.isAccountSelected();
676        }
677
678        @Override
679        public void onAccountSelected(long accountId) {
680            switchAccount(accountId, false);
681        }
682
683        @Override
684        public void onMailboxSelected(long accountId, long mailboxId) {
685            openMailbox(accountId, mailboxId);
686        }
687
688        @Override
689        public void onNoAccountsFound() {
690            Welcome.actionStart(mActivity);
691            mActivity.finish();
692        }
693
694        @Override
695        public int getTitleMode() {
696            if (mThreePane.isLeftPaneVisible()) {
697                // Mailbox list visible
698                return TITLE_MODE_ACCOUNT_NAME_ONLY;
699            } else {
700                // Mailbox list hidden
701                return TITLE_MODE_ACCOUNT_WITH_MAILBOX;
702            }
703        }
704
705        public String getMessageSubject() {
706            if (isMessageViewInstalled() && getMessageViewFragment().isMessageOpen()) {
707                return getMessageViewFragment().getMessage().mSubject;
708            } else {
709                return null;
710            }
711        }
712
713        @Override
714        public boolean shouldShowUp() {
715            final int visiblePanes = mThreePane.getVisiblePanes();
716            final boolean leftPaneHidden = ((visiblePanes & ThreePaneLayout.PANE_LEFT) == 0);
717            return leftPaneHidden
718                    || (isMailboxListInstalled() && getMailboxListFragment().canNavigateUp());
719        }
720
721        @Override
722        public String getSearchHint() {
723            return UIControllerTwoPane.this.getSearchHint();
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