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