UIControllerOnePane.java revision 513486cfb5d446f098b1c97d16932e51e316d1a7
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    private boolean isInboxShown() {
159        if (!isMessageListInstalled()) {
160            return false;
161        }
162        return getMessageListFragment().isInboxList();
163    }
164
165    // This is all temporary as we'll have a different action bar controller for 1-pane.
166    private class ActionBarControllerCallback implements ActionBarController.Callback {
167        @Override
168        public int getTitleMode() {
169            if (isMailboxListInstalled()) {
170                return TITLE_MODE_ACCOUNT_WITH_ALL_FOLDERS_LABEL;
171            }
172            if (isMessageViewInstalled()) {
173                return TITLE_MODE_MESSAGE_SUBJECT;
174            }
175            return TITLE_MODE_ACCOUNT_WITH_MAILBOX;
176        }
177
178        public String getMessageSubject() {
179            if (isMessageViewInstalled() && getMessageViewFragment().isMessageOpen()) {
180                return getMessageViewFragment().getMessage().mSubject;
181            } else {
182                return null;
183            }
184        }
185
186        @Override
187        public boolean shouldShowUp() {
188            return isMessageViewInstalled()
189                    || (isMessageListInstalled() && !isInboxShown())
190                    || isMailboxListInstalled();
191        }
192
193        @Override
194        public long getUIAccountId() {
195            return UIControllerOnePane.this.getUIAccountId();
196        }
197
198        @Override
199        public long getMailboxId() {
200            return UIControllerOnePane.this.getMailboxId();
201        }
202
203        @Override
204        public void onMailboxSelected(long accountId, long mailboxId) {
205            if (mailboxId == Mailbox.NO_MAILBOX) {
206                showAllMailboxes();
207            } else {
208                openMailbox(accountId, mailboxId);
209            }
210        }
211
212        @Override
213        public boolean isAccountSelected() {
214            return UIControllerOnePane.this.isAccountSelected();
215        }
216
217        @Override
218        public void onAccountSelected(long accountId) {
219            switchAccount(accountId, true); // Always go to inbox
220        }
221
222        @Override
223        public void onNoAccountsFound() {
224            Welcome.actionStart(mActivity);
225            mActivity.finish();
226        }
227
228        @Override
229        public String getSearchHint() {
230            if (!isMessageListInstalled()) {
231                return null;
232            }
233            return UIControllerOnePane.this.getSearchHint();
234        }
235
236        @Override
237        public void onSearchStarted() {
238            if (!isMessageListInstalled()) {
239                return;
240            }
241            UIControllerOnePane.this.onSearchStarted();
242        }
243
244        @Override
245        public void onSearchSubmit(String queryTerm) {
246            if (!isMessageListInstalled()) {
247                return;
248            }
249            UIControllerOnePane.this.onSearchSubmit(queryTerm);
250        }
251
252        @Override
253        public void onSearchExit() {
254            UIControllerOnePane.this.onSearchExit();
255        }
256    }
257
258    public UIControllerOnePane(EmailActivity activity) {
259        super(activity);
260    }
261
262    @Override
263    protected ActionBarController createActionBarController(Activity activity) {
264
265        // For now, we just reuse the same action bar controller used for 2-pane.
266        // We may change it later.
267
268        return new ActionBarController(activity, activity.getLoaderManager(),
269                activity.getActionBar(), new ActionBarControllerCallback());
270    }
271
272    @Override
273    public void onSaveInstanceState(Bundle outState) {
274        super.onSaveInstanceState(outState);
275        if (mPreviousFragment != null) {
276            mFragmentManager.putFragment(outState,
277                    BUNDLE_KEY_PREVIOUS_FRAGMENT, mPreviousFragment);
278        }
279    }
280
281    @Override
282    public void onRestoreInstanceState(Bundle savedInstanceState) {
283        super.onRestoreInstanceState(savedInstanceState);
284        mPreviousFragment = mFragmentManager.getFragment(savedInstanceState,
285                BUNDLE_KEY_PREVIOUS_FRAGMENT);
286    }
287
288    @Override
289    public int getLayoutId() {
290        return R.layout.email_activity_one_pane;
291    }
292
293    @Override
294    public long getUIAccountId() {
295        if (mListContext != null) {
296            return mListContext.mAccountId;
297        }
298        if (isMailboxListInstalled()) {
299            return getMailboxListFragment().getAccountId();
300        }
301        return Account.NO_ACCOUNT;
302    }
303
304    private long getMailboxId() {
305        if (mListContext != null) {
306            return mListContext.getMailboxId();
307        }
308        return Mailbox.NO_MAILBOX;
309    }
310
311    @Override
312    public boolean onBackPressed(boolean isSystemBackKey) {
313        if (Email.DEBUG) {
314            // This is VERY important -- no check for DEBUG_LIFECYCLE
315            Log.d(Logging.LOG_TAG, this + " onBackPressed: " + isSystemBackKey);
316        }
317        // The action bar controller has precedence.  Must call it first.
318        if (mActionBarController.onBackPressed(isSystemBackKey)) {
319            return true;
320        }
321        // If the mailbox list is shown and showing a nested mailbox, let it navigate up first.
322        if (isMailboxListInstalled() && getMailboxListFragment().navigateUp()) {
323            if (DEBUG_FRAGMENTS) {
324                Log.d(Logging.LOG_TAG, this + " Back: back handled by mailbox list");
325            }
326            return true;
327        }
328
329        // Custom back stack
330        if (shouldPopFromBackStack(isSystemBackKey)) {
331            if (DEBUG_FRAGMENTS) {
332                Log.d(Logging.LOG_TAG, this + " Back: Popping from back stack");
333            }
334            popFromBackStack();
335            return true;
336        }
337
338        // No entry in the back stack.
339        if (isMessageViewInstalled()) {
340            if (DEBUG_FRAGMENTS) {
341                Log.d(Logging.LOG_TAG, this + " Back: Message view -> Message List");
342            }
343            // If the message view is shown, show the "parent" message list.
344            // This happens when we get a deep link to a message.  (e.g. from a widget)
345            openMailbox(mListContext.mAccountId, mListContext.getMailboxId());
346            return true;
347        } else if (isMailboxListInstalled()) {
348            // If the mailbox list is shown, always go back to the inbox.
349            switchAccount(getMailboxListFragment().getAccountId(), true /* force show inbox */);
350            return true;
351        } else if (isMessageListInstalled() && !isInboxShown()) {
352            // Non-inbox list. Go to inbox.
353            switchAccount(mListContext.mAccountId, true /* force show inbox */);
354            return true;
355        }
356        return false;
357    }
358
359    @Override
360    public void openInternal(final MessageListContext listContext, final long messageId) {
361        if (Email.DEBUG) {
362            // This is VERY important -- don't check for DEBUG_LIFECYCLE
363            Log.i(Logging.LOG_TAG, this + " open " + listContext + " messageId=" + messageId);
364        }
365
366        if (messageId != Message.NO_MESSAGE) {
367            openMessage(messageId);
368        } else {
369            showFragment(MessageListFragment.newInstance(listContext));
370        }
371    }
372
373    /**
374     * @return currently installed {@link Fragment} (1-pane has only one at most), or null if none
375     *         exists.
376     */
377    private Fragment getInstalledFragment() {
378        if (isMailboxListInstalled()) {
379            return getMailboxListFragment();
380        } else if (isMessageListInstalled()) {
381            return getMessageListFragment();
382        } else if (isMessageViewInstalled()) {
383            return getMessageViewFragment();
384        }
385        return null;
386    }
387
388    /**
389     * Show the mailbox list.
390     *
391     * This is the only way to open the mailbox list on 1-pane.
392     * {@link #open(MessageListContext, long)} will only open either the message list or the
393     * message view.
394     */
395    private void openMailboxList(long accountId) {
396        setListContext(null);
397        showFragment(MailboxListFragment.newInstance(accountId, Mailbox.NO_MAILBOX, false));
398    }
399
400    private void openMessage(long messageId) {
401        showFragment(MessageViewFragment.newInstance(messageId));
402    }
403
404    /**
405     * Push the installed fragment into our custom back stack (or optionally
406     * {@link FragmentTransaction#remove} it) and {@link FragmentTransaction#add} {@code fragment}.
407     *
408     * @param fragment {@link Fragment} to be added.
409     *
410     *  TODO Delay-call the whole method and use the synchronous transaction.
411     */
412    private void showFragment(Fragment fragment) {
413        final FragmentTransaction ft = mFragmentManager.beginTransaction();
414        final Fragment installed = getInstalledFragment();
415        if ((installed instanceof MessageViewFragment)
416                && (fragment instanceof MessageViewFragment)) {
417            // Newer/older navigation, auto-advance, etc.
418            // In this case we want to keep the backstack untouched, so that after back navigation
419            // we can restore the message list, including scroll position and batch selection.
420        } else {
421            if (DEBUG_FRAGMENTS) {
422                Log.i(Logging.LOG_TAG, this + " backstack: [push] " + getInstalledFragment()
423                        + " -> " + fragment);
424            }
425            if (mPreviousFragment != null) {
426                if (DEBUG_FRAGMENTS) {
427                    Log.d(Logging.LOG_TAG, this + " showFragment: destroying previous fragment "
428                            + mPreviousFragment);
429                }
430                removeFragment(ft, mPreviousFragment);
431                mPreviousFragment = null;
432            }
433            // Remove the current fragment or push it into the backstack.
434            if (installed != null) {
435                if (installed instanceof MessageViewFragment) {
436                    // Message view should never be pushed to the backstack.
437                    if (DEBUG_FRAGMENTS) {
438                        Log.d(Logging.LOG_TAG, this + " showFragment: removing " + installed);
439                    }
440                    ft.remove(installed);
441                } else {
442                    // Other fragments should be pushed.
443                    mPreviousFragment = installed;
444                    if (DEBUG_FRAGMENTS) {
445                        Log.d(Logging.LOG_TAG, this + " showFragment: detaching "
446                                + mPreviousFragment);
447                    }
448                    ft.detach(mPreviousFragment);
449                }
450            }
451        }
452        // Show the new one
453        if (DEBUG_FRAGMENTS) {
454            Log.d(Logging.LOG_TAG, this + " showFragment: replacing with " + fragment);
455        }
456        ft.replace(R.id.fragment_placeholder, fragment);
457        ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
458        commitFragmentTransaction(ft);
459    }
460
461    /**
462     * @param isSystemBackKey <code>true</code> if the system back key was pressed.
463     *        <code>false</code> if it's caused by the "home" icon click on the action bar.
464     * @return true if we should pop from our custom back stack.
465     */
466    private boolean shouldPopFromBackStack(boolean isSystemBackKey) {
467        if (mPreviousFragment == null) {
468            return false; // Nothing in the back stack
469        }
470        if (mPreviousFragment instanceof MessageViewFragment) {
471            throw new IllegalStateException("Message view should never be in backstack");
472        }
473        final Fragment installed = getInstalledFragment();
474        if (installed == null) {
475            // If no fragment is installed right now, do nothing.
476            return false;
477        }
478
479        // Okay now we have 2 fragments; the one in the back stack and the one that's currently
480        // installed.
481        if (isInboxShown()) {
482            // Inbox is the top level list - never go back from here.
483            return false;
484        }
485
486        // Disallow the MailboxList--> non-inbox MessageList transition as the Mailbox list
487        // is always considered "higher" than a non-inbox MessageList
488        if ((mPreviousFragment instanceof MessageListFragment)
489                && (!((MessageListFragment) mPreviousFragment).isInboxList())
490                && (installed  instanceof MailboxListFragment)) {
491            return false;
492        }
493        return true;
494    }
495
496    /**
497     * Pop from our custom back stack.
498     *
499     * TODO Delay-call the whole method and use the synchronous transaction.
500     */
501    private void popFromBackStack() {
502        if (mPreviousFragment == null) {
503            return;
504        }
505        final FragmentTransaction ft = mFragmentManager.beginTransaction();
506        final Fragment installed = getInstalledFragment();
507        if (DEBUG_FRAGMENTS) {
508            Log.i(Logging.LOG_TAG, this + " backstack: [pop] " + installed + " -> "
509                    + mPreviousFragment);
510        }
511        removeFragment(ft, installed);
512
513        // Restore listContext.
514        if (mPreviousFragment instanceof MailboxListFragment) {
515            setListContext(null);
516        } else if (mPreviousFragment instanceof MessageListFragment) {
517            setListContext(((MessageListFragment) mPreviousFragment).getListContext());
518        } else {
519            throw new IllegalStateException("Message view should never be in backstack");
520        }
521
522        ft.attach(mPreviousFragment);
523        ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE);
524        mPreviousFragment = null;
525        commitFragmentTransaction(ft);
526        return;
527    }
528
529    private void showAllMailboxes() {
530        if (!isAccountSelected()) {
531            return; // Can happen because of asynchronous fragment transactions.
532        }
533
534        openMailboxList(getUIAccountId());
535    }
536
537    @Override
538    protected void installMailboxListFragment(MailboxListFragment fragment) {
539        stopMessageOrderManager();
540        super.installMailboxListFragment(fragment);
541    }
542
543    @Override
544    protected void installMessageListFragment(MessageListFragment fragment) {
545        stopMessageOrderManager();
546        super.installMessageListFragment(fragment);
547    }
548
549    @Override
550    protected long getMailboxSettingsMailboxId() {
551        return isMessageListInstalled()
552                ? getMessageListFragment().getMailboxId()
553                : Mailbox.NO_MAILBOX;
554    }
555
556    @Override
557    public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) {
558        // First, let the base class do what it has to do.
559        super.onPrepareOptionsMenu(inflater, menu);
560
561        // Then override
562        final boolean messageListVisible = isMessageListInstalled();
563        if (!messageListVisible) {
564            menu.findItem(R.id.search).setVisible(false);
565            menu.findItem(R.id.compose).setVisible(false);
566            menu.findItem(R.id.refresh).setVisible(false);
567            menu.findItem(R.id.show_all_mailboxes).setVisible(false);
568            menu.findItem(R.id.mailbox_settings).setVisible(false);
569        }
570
571        final boolean messageViewVisible = isMessageViewInstalled();
572        if (messageViewVisible) {
573            final MessageOrderManager om = getMessageOrderManager();
574            menu.findItem(R.id.newer).setVisible(true);
575            menu.findItem(R.id.older).setVisible(true);
576            // orderManager shouldn't be null when the message view is installed, but just in case..
577            menu.findItem(R.id.newer).setEnabled((om != null) && om.canMoveToNewer());
578            menu.findItem(R.id.older).setEnabled((om != null) && om.canMoveToOlder());
579        }
580        return true;
581    }
582
583    @Override
584    public boolean onOptionsItemSelected(MenuItem item) {
585        switch (item.getItemId()) {
586            case R.id.newer:
587                moveToNewer();
588                return true;
589            case R.id.older:
590                moveToOlder();
591                return true;
592            case R.id.show_all_mailboxes:
593                showAllMailboxes();
594                return true;
595        }
596        return super.onOptionsItemSelected(item);
597    }
598
599    @Override
600    protected boolean isRefreshEnabled() {
601        // Refreshable only when an actual account is selected, and message view isn't shown.
602        // (i.e. only available on the mailbox list or the message view, but not on the combined
603        // one)
604        if (!isActualAccountSelected() || isMessageViewInstalled()) {
605            return false;
606        }
607        return isMailboxListInstalled() || (mListContext.getMailboxId() > 0);
608    }
609
610    @Override
611    protected void onRefresh() {
612        if (!isRefreshEnabled()) {
613            return;
614        }
615        if (isMessageListInstalled()) {
616            mRefreshManager.refreshMessageList(getActualAccountId(), getMailboxId(), true);
617        } else {
618            mRefreshManager.refreshMailboxList(getActualAccountId());
619        }
620    }
621
622    @Override
623    protected boolean isRefreshInProgress() {
624        if (!isRefreshEnabled()) {
625            return false;
626        }
627        if (isMessageListInstalled()) {
628            return mRefreshManager.isMessageListRefreshing(getMailboxId());
629        } else {
630            return mRefreshManager.isMailboxListRefreshing(getActualAccountId());
631        }
632    }
633
634    @Override protected void navigateToMessage(long messageId) {
635        openMessage(messageId);
636    }
637
638    @Override protected void updateNavigationArrows() {
639        refreshActionBar();
640    }
641}
642