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