UIControllerOnePane.java revision 0413edd6261f3879894840ff017c3c933ddb586d
1/*
2 * Copyright (C) 2011 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.Fragment;
21import android.app.FragmentTransaction;
22import android.os.Bundle;
23import android.util.Log;
24import android.view.Menu;
25import android.view.MenuInflater;
26import android.view.MenuItem;
27
28import com.android.email.Email;
29import com.android.email.MessageListContext;
30import com.android.email.R;
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.Utility;
36
37import java.util.Set;
38
39
40/**
41 * UI Controller for non x-large devices.  Supports a single-pane layout.
42 *
43 * One one-pane, only at most one fragment can be installed at a time.
44 *
45 * Note: Always use {@link #commitFragmentTransaction} to operate fragment transactions,
46 * so that we can easily switch between synchronous and asynchronous transactions.
47 *
48 * Major TODOs
49 * - TODO Implement callbacks
50 */
51class UIControllerOnePane extends UIControllerBase {
52    private static final String BUNDLE_KEY_PREVIOUS_FRAGMENT
53            = "UIControllerOnePane.PREVIOUS_FRAGMENT";
54
55    // Our custom poor-man's back stack which has only one entry at maximum.
56    private Fragment mPreviousFragment;
57
58    // MailboxListFragment.Callback
59    @Override
60    public void onAccountSelected(long accountId) {
61        // It's from combined view, so "forceShowInbox" doesn't really matter.
62        // (We're always switching accounts.)
63        switchAccount(accountId, true);
64    }
65
66    // MailboxListFragment.Callback
67    @Override
68    public void onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation) {
69        if (nestedNavigation) {
70            return; // Nothing to do on 1-pane.
71        }
72        openMailbox(accountId, mailboxId);
73    }
74
75    // MailboxListFragment.Callback
76    @Override
77    public void onParentMailboxChanged() {
78        refreshActionBar();
79    }
80
81    // MessageListFragment.Callback
82    @Override
83    public void onAdvancingOpAccepted(Set<Long> affectedMessages) {
84        // Nothing to do on 1 pane.
85    }
86
87    // MessageListFragment.Callback
88    @Override
89    public void onEnterSelectionMode(boolean enter) {
90        // Noop.
91    }
92
93    // MessageListFragment.Callback
94    @Override
95    public void onListLoaded() {
96        // Noop.
97    }
98
99    // MessageListFragment.Callback
100    @Override
101    public void onMailboxNotFound() {
102        // Something bad happened - the account or mailbox we were looking for was deleted.
103        // Just restart and let the entry flow find a good default view.
104        Utility.showToast(mActivity, R.string.toast_mailbox_not_found);
105        Welcome.actionStart(mActivity);
106        mActivity.finish();
107    }
108
109    // MessageListFragment.Callback
110    @Override
111    public void onMessageOpen(
112            long messageId, long messageMailboxId, long listMailboxId, int type) {
113        if (type == MessageListFragment.Callback.TYPE_DRAFT) {
114            MessageCompose.actionEditDraft(mActivity, messageId);
115        } else {
116            open(mListContext, messageId);
117        }
118    }
119
120    // MessageListFragment.Callback
121    @Override
122    public boolean onDragStarted() {
123        // No drag&drop on 1-pane
124        return false;
125    }
126
127    // MessageListFragment.Callback
128    @Override
129    public void onDragEnded() {
130        // No drag&drop on 1-pane
131    }
132
133    // MessageViewFragment.Callback
134    @Override
135    public void onForward() {
136        MessageCompose.actionForward(mActivity, getMessageId());
137    }
138
139    // MessageViewFragment.Callback
140    @Override
141    public void onReply() {
142        MessageCompose.actionReply(mActivity, getMessageId(), false);
143    }
144
145    // MessageViewFragment.Callback
146    @Override
147    public void onReplyAll() {
148        MessageCompose.actionReply(mActivity, getMessageId(), true);
149    }
150
151    // MessageViewFragment.Callback
152    @Override
153    public void onCalendarLinkClicked(long epochEventStartTime) {
154        ActivityHelper.openCalendar(mActivity, epochEventStartTime);
155    }
156
157    // MessageViewFragment.Callback
158    @Override
159    public boolean onUrlInMessageClicked(String url) {
160        return ActivityHelper.openUrlInMessage(mActivity, url, getActualAccountId());
161    }
162
163    // MessageViewFragment.Callback
164    @Override
165    public void onLoadMessageError(String errorMessage) {
166        // TODO Auto-generated method stub
167    }
168
169    // MessageViewFragment.Callback
170    @Override
171    public void onLoadMessageFinished() {
172        // TODO Auto-generated method stub
173    }
174
175    // MessageViewFragment.Callback
176    @Override
177    public void onLoadMessageStarted() {
178        // TODO Auto-generated method stub
179    }
180
181    // This is all temporary as we'll have a different action bar controller for 1-pane.
182    private class ActionBarControllerCallback implements ActionBarController.Callback {
183        @Override
184        public int getTitleMode() {
185            if (isMailboxListInstalled()) {
186                return TITLE_MODE_ACCOUNT_WITH_ALL_FOLDERS_LABEL;
187            }
188            if (isMessageViewInstalled()) {
189                return TITLE_MODE_MESSAGE_SUBJECT;
190            }
191            return TITLE_MODE_ACCOUNT_WITH_MAILBOX;
192        }
193
194        public String getMessageSubject() {
195            if (isMessageViewInstalled() && getMessageViewFragment().isMessageOpen()) {
196                return getMessageViewFragment().getMessage().mSubject;
197            } else {
198                return null;
199            }
200        }
201
202        @Override
203        public boolean shouldShowUp() {
204            return isMessageViewInstalled()
205                     || (isMailboxListInstalled() && getMailboxListFragment().canNavigateUp());
206        }
207
208        @Override
209        public long getUIAccountId() {
210            return UIControllerOnePane.this.getUIAccountId();
211        }
212
213        @Override
214        public long getMailboxId() {
215            return UIControllerOnePane.this.getMailboxId();
216        }
217
218        @Override
219        public void onMailboxSelected(long accountId, long mailboxId) {
220            if (mailboxId == Mailbox.NO_MAILBOX) {
221                showAllMailboxes();
222            } else {
223                openMailbox(accountId, mailboxId);
224            }
225        }
226
227        @Override
228        public boolean isAccountSelected() {
229            return UIControllerOnePane.this.isAccountSelected();
230        }
231
232        @Override
233        public void onAccountSelected(long accountId) {
234            switchAccount(accountId, true); // Always go to inbox
235        }
236
237        @Override
238        public void onNoAccountsFound() {
239            Welcome.actionStart(mActivity);
240            mActivity.finish();
241        }
242
243        @Override
244        public String getSearchHint() {
245            if (!isMessageListInstalled()) {
246                return null;
247            }
248            return UIControllerOnePane.this.getSearchHint();
249        }
250
251        @Override
252        public void onSearchSubmit(String queryTerm) {
253            if (!isMessageListInstalled()) {
254                return;
255            }
256            UIControllerOnePane.this.onSearchSubmit(queryTerm);
257        }
258
259        @Override
260        public void onSearchExit() {
261            UIControllerOnePane.this.onSearchExit();
262        }
263    }
264
265    public UIControllerOnePane(EmailActivity activity) {
266        super(activity);
267    }
268
269    @Override
270    protected ActionBarController createActionBarController(Activity activity) {
271
272        // For now, we just reuse the same action bar controller used for 2-pane.
273        // We may change it later.
274
275        return new ActionBarController(activity, activity.getLoaderManager(),
276                activity.getActionBar(), new ActionBarControllerCallback());
277    }
278
279    @Override
280    public void onSaveInstanceState(Bundle outState) {
281        super.onSaveInstanceState(outState);
282        if (mPreviousFragment != null) {
283            mFragmentManager.putFragment(outState,
284                    BUNDLE_KEY_PREVIOUS_FRAGMENT, mPreviousFragment);
285        }
286    }
287
288    @Override
289    public void onRestoreInstanceState(Bundle savedInstanceState) {
290        super.onRestoreInstanceState(savedInstanceState);
291        mPreviousFragment = mFragmentManager.getFragment(savedInstanceState,
292                BUNDLE_KEY_PREVIOUS_FRAGMENT);
293    }
294
295    @Override
296    public int getLayoutId() {
297        return R.layout.email_activity_one_pane;
298    }
299
300    @Override
301    public long getUIAccountId() {
302        if (mListContext != null) {
303            return mListContext.mAccountId;
304        }
305        if (isMailboxListInstalled()) {
306            return getMailboxListFragment().getAccountId();
307        }
308        return Account.NO_ACCOUNT;
309    }
310
311    private long getMailboxId() {
312        if (mListContext != null) {
313            return mListContext.getMailboxId();
314        }
315        return Mailbox.NO_MAILBOX;
316    }
317
318    @Override
319    public boolean onBackPressed(boolean isSystemBackKey) {
320        if (Email.DEBUG) {
321            // This is VERY important -- no check for DEBUG_LIFECYCLE
322            Log.d(Logging.LOG_TAG, this + " onBackPressed: " + isSystemBackKey);
323        }
324        // The action bar controller has precedence.  Must call it first.
325        if (mActionBarController.onBackPressed(isSystemBackKey)) {
326            return true;
327        }
328        // If the mailbox list is shown and showing a nested mailbox, let it navigate up first.
329        if (isMailboxListInstalled() && getMailboxListFragment().navigateUp()) {
330            if (DEBUG_FRAGMENTS) {
331                Log.d(Logging.LOG_TAG, this + " Back: back handled by mailbox list");
332            }
333            return true;
334        }
335
336        // Custom back stack
337        if (shouldPopFromBackStack(isSystemBackKey)) {
338            if (DEBUG_FRAGMENTS) {
339                Log.d(Logging.LOG_TAG, this + " Back: Popping from back stack");
340            }
341            popFromBackStack();
342            return true;
343        }
344
345        // No entry in the back stack.
346        // If the message view is shown, show the "parent" message list.
347        // This happens when we get a deep link to a message.  (e.g. from a widget)
348        if (isMessageViewInstalled()) {
349            if (DEBUG_FRAGMENTS) {
350                Log.d(Logging.LOG_TAG, this + " Back: Message view -> Message List");
351            }
352            openMailbox(mListContext.mAccountId, mListContext.getMailboxId());
353            return true;
354        }
355        return false;
356    }
357
358    @Override
359    public void openInternal(final MessageListContext listContext, final long messageId) {
360        if (Email.DEBUG) {
361            // This is VERY important -- don't check for DEBUG_LIFECYCLE
362            Log.i(Logging.LOG_TAG, this + " open " + listContext + " messageId=" + messageId);
363        }
364
365        if (messageId != Message.NO_MESSAGE) {
366            openMessage(messageId);
367        } else {
368            showFragment(MessageListFragment.newInstance(listContext));
369        }
370    }
371
372    /**
373     * @return currently installed {@link Fragment} (1-pane has only one at most), or null if none
374     *         exists.
375     */
376    private Fragment getInstalledFragment() {
377        if (isMailboxListInstalled()) {
378            return getMailboxListFragment();
379        } else if (isMessageListInstalled()) {
380            return getMessageListFragment();
381        } else if (isMessageViewInstalled()) {
382            return getMessageViewFragment();
383        }
384        return null;
385    }
386
387    /**
388     * Show the mailbox list.
389     *
390     * This is the only way to open the mailbox list on 1-pane.
391     * {@link #open(MessageListContext, long)} will only open either the message list or the
392     * message view.
393     */
394    private void openMailboxList(long accountId) {
395        setListContext(null);
396        showFragment(MailboxListFragment.newInstance(accountId, Mailbox.NO_MAILBOX, false));
397    }
398
399    private void openMessage(long messageId) {
400        showFragment(MessageViewFragment.newInstance(messageId));
401    }
402
403    /**
404     * Use this instead of {@link FragmentTransaction#commit}.  We may switch to the asynchronous
405     * transaction some day.
406     */
407    private void commitFragmentTransaction(FragmentTransaction ft) {
408        if (!ft.isEmpty()) {
409            // NB: there should be no cases in which a transaction is committed after
410            // onSaveInstanceState. Unfortunately, the "state loss" check also happens when in
411            // LoaderCallbacks.onLoadFinished, and we wish to perform transactions there. The check
412            // by the framework is conservative and prevents cases where there are transactions
413            // affecting Loader lifecycles - but we have no such cases.
414            // TODO: use asynchronous callbacks from loaders to avoid this implicit dependency
415            ft.commitAllowingStateLoss();
416            mFragmentManager.executePendingTransactions();
417        }
418    }
419
420    /**
421     * Push the installed fragment into our custom back stack (or optionally
422     * {@link FragmentTransaction#remove} it) and {@link FragmentTransaction#add} {@code fragment}.
423     *
424     * @param fragment {@link Fragment} to be added.
425     *
426     *  TODO Delay-call the whole method and use the synchronous transaction.
427     */
428    private void showFragment(Fragment fragment) {
429        final FragmentTransaction ft = mFragmentManager.beginTransaction();
430        final Fragment installed = getInstalledFragment();
431        if ((installed instanceof MessageViewFragment)
432                && (fragment instanceof MessageViewFragment)) {
433            // Newer/older navigation, auto-advance, etc.
434            // In this case we want to keep the backstack untouched, so that after back navigation
435            // we can restore the message list, including scroll position and batch selection.
436        } else {
437            if (DEBUG_FRAGMENTS) {
438                Log.i(Logging.LOG_TAG, this + " backstack: [push] " + getInstalledFragment()
439                        + " -> " + fragment);
440            }
441            if (mPreviousFragment != null) {
442                if (DEBUG_FRAGMENTS) {
443                    Log.d(Logging.LOG_TAG, this + " showFragment: destroying previous fragment "
444                            + mPreviousFragment);
445                }
446                removeFragment(ft, mPreviousFragment);
447                mPreviousFragment = null;
448            }
449            // Remove the current fragment or push it into the backstack.
450            if (installed != null) {
451                if (installed instanceof MessageViewFragment) {
452                    // Message view should never be pushed to the backstack.
453                    if (DEBUG_FRAGMENTS) {
454                        Log.d(Logging.LOG_TAG, this + " showFragment: removing " + installed);
455                    }
456                    ft.remove(installed);
457                } else {
458                    // Other fragments should be pushed.
459                    mPreviousFragment = installed;
460                    if (DEBUG_FRAGMENTS) {
461                        Log.d(Logging.LOG_TAG, this + " showFragment: detaching "
462                                + mPreviousFragment);
463                    }
464                    ft.detach(mPreviousFragment);
465                }
466            }
467        }
468        // Show the new one
469        if (DEBUG_FRAGMENTS) {
470            Log.d(Logging.LOG_TAG, this + " showFragment: replacing with " + fragment);
471        }
472        ft.replace(R.id.fragment_placeholder, fragment);
473        ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
474        commitFragmentTransaction(ft);
475    }
476
477    /**
478     * @param isSystemBackKey <code>true</code> if the system back key was pressed.
479     *        <code>false</code> if it's caused by the "home" icon click on the action bar.
480     * @return true if we should pop from our custom back stack.
481     */
482    private boolean shouldPopFromBackStack(boolean isSystemBackKey) {
483        if (mPreviousFragment == null) {
484            return false; // Nothing in the back stack
485        }
486        if (mPreviousFragment instanceof MessageViewFragment) {
487            throw new IllegalStateException("Message view should never be in backstack");
488        }
489        final Fragment installed = getInstalledFragment();
490        if (installed == null) {
491            // If no fragment is installed right now, do nothing.
492            return false;
493        }
494
495        // Okay now we have 2 fragments; the one in the back stack and the one that's currently
496        // installed.
497        if (mPreviousFragment.getClass() == installed.getClass()) {
498            // We never want to go back to the same kind of fragment, which happens when the user
499            // is on the message list, and selects another mailbox on the action bar.
500            return false;
501        }
502
503        if (isSystemBackKey) {
504            // In other cases, the system back key should always work.
505            return true;
506        } else {
507            // Home icon press -- there are cases where we don't want it to work.
508
509            // Disallow the Message list <-> mailbox list transition
510            if ((mPreviousFragment instanceof MailboxListFragment)
511                    && (installed  instanceof MessageListFragment)) {
512                return false;
513            }
514            if ((mPreviousFragment instanceof MessageListFragment)
515                    && (installed  instanceof MailboxListFragment)) {
516                return false;
517            }
518            return true;
519        }
520    }
521
522    /**
523     * Pop from our custom back stack.
524     *
525     * TODO Delay-call the whole method and use the synchronous transaction.
526     */
527    private void popFromBackStack() {
528        if (mPreviousFragment == null) {
529            return;
530        }
531        final FragmentTransaction ft = mFragmentManager.beginTransaction();
532        final Fragment installed = getInstalledFragment();
533        if (DEBUG_FRAGMENTS) {
534            Log.i(Logging.LOG_TAG, this + " backstack: [pop] " + installed + " -> "
535                    + mPreviousFragment);
536        }
537        removeFragment(ft, installed);
538
539        // Restore listContext.
540        if (mPreviousFragment instanceof MailboxListFragment) {
541            setListContext(null);
542        } else if (mPreviousFragment instanceof MessageListFragment) {
543            setListContext(((MessageListFragment) mPreviousFragment).getListContext());
544        } else {
545            throw new IllegalStateException("Message view should never be in backstack");
546        }
547
548        ft.attach(mPreviousFragment);
549        ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE);
550        mPreviousFragment = null;
551        commitFragmentTransaction(ft);
552        return;
553    }
554
555    private void showAllMailboxes() {
556        if (!isAccountSelected()) {
557            return; // Can happen because of asynchronous fragment transactions.
558        }
559
560        openMailboxList(getUIAccountId());
561    }
562
563    @Override
564    protected void installMailboxListFragment(MailboxListFragment fragment) {
565        stopMessageOrderManager();
566        super.installMailboxListFragment(fragment);
567    }
568
569    @Override
570    protected void installMessageListFragment(MessageListFragment fragment) {
571        stopMessageOrderManager();
572        super.installMessageListFragment(fragment);
573    }
574
575    @Override
576    protected long getMailboxSettingsMailboxId() {
577        return isMessageListInstalled()
578                ? getMessageListFragment().getMailboxId()
579                : Mailbox.NO_MAILBOX;
580    }
581
582    @Override
583    public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) {
584        // First, let the base class do what it has to do.
585        super.onPrepareOptionsMenu(inflater, menu);
586
587        // Then override
588        final boolean messageViewVisible = isMessageViewInstalled();
589        if (messageViewVisible) {
590            menu.findItem(R.id.search).setVisible(false);
591            menu.findItem(R.id.compose).setVisible(false);
592            menu.findItem(R.id.refresh).setVisible(false);
593            menu.findItem(R.id.show_all_mailboxes).setVisible(false);
594            menu.findItem(R.id.mailbox_settings).setVisible(false);
595
596            final MessageOrderManager om = getMessageOrderManager();
597            menu.findItem(R.id.newer).setVisible(true);
598            menu.findItem(R.id.older).setVisible(true);
599            // orderManager shouldn't be null when the message view is installed, but just in case..
600            menu.findItem(R.id.newer).setEnabled((om != null) && om.canMoveToNewer());
601            menu.findItem(R.id.older).setEnabled((om != null) && om.canMoveToOlder());
602        }
603        return true;
604    }
605
606    @Override
607    public boolean onOptionsItemSelected(MenuItem item) {
608        switch (item.getItemId()) {
609            case R.id.newer:
610                moveToNewer();
611                return true;
612            case R.id.older:
613                moveToOlder();
614                return true;
615            case R.id.show_all_mailboxes:
616                showAllMailboxes();
617                return true;
618        }
619        return super.onOptionsItemSelected(item);
620    }
621
622    @Override
623    protected boolean isRefreshEnabled() {
624        // Refreshable only when an actual account is selected, and message view isn't shown.
625        // (i.e. only available on the mailbox list or the message view, but not on the combined
626        // one)
627        return isActualAccountSelected() && !isMessageViewInstalled();
628    }
629
630    @Override
631    protected void onRefresh() {
632        if (!isRefreshEnabled()) {
633            return;
634        }
635        if (isMessageListInstalled()) {
636            mRefreshManager.refreshMessageList(getActualAccountId(), getMailboxId(), true);
637        } else {
638            mRefreshManager.refreshMailboxList(getActualAccountId());
639        }
640    }
641
642    @Override
643    protected boolean isRefreshInProgress() {
644        if (!isRefreshEnabled()) {
645            return false;
646        }
647        if (isMessageListInstalled()) {
648            return mRefreshManager.isMessageListRefreshing(getMailboxId());
649        } else {
650            return mRefreshManager.isMailboxListRefreshing(getActualAccountId());
651        }
652    }
653
654    @Override protected void navigateToMessage(long messageId) {
655        openMessage(messageId);
656    }
657
658    @Override protected void updateNavigationArrows() {
659        refreshActionBar();
660    }
661}
662