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