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