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