UIControllerOnePane.java revision 2a292e235cbe7ce33367ba549d4da6cb4764ffe9
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.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: Always use {@link #commitFragmentTransaction} to operate fragment transactions,
44 * so that we can easily switch between synchronous and asynchronous transactions.
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                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 asynchronous
501     * transaction some day.
502     */
503    private void commitFragmentTransaction(FragmentTransaction ft) {
504        if (!ft.isEmpty()) {
505            ft.commit();
506            mActivity.getFragmentManager().executePendingTransactions();
507        }
508    }
509
510    /**
511     * Push the installed fragment into our custom back stack (or optionally
512     * {@link FragmentTransaction#remove} it) and {@link FragmentTransaction#add} {@code fragment}.
513     *
514     * @param fragment {@link Fragment} to be added.
515     * @param clearBackStack set {@code true} to remove the currently installed fragment.
516     *        {@code false} to push it into the backstack.
517     *
518     *  TODO Delay-call the whole method and use the synchronous transaction.
519     */
520    private void showFragment(Fragment fragment, boolean clearBackStack) {
521        if (DEBUG_FRAGMENTS) {
522            if (clearBackStack) {
523                Log.i(Logging.LOG_TAG, this + " backstack: [clear] showing " + fragment);
524            } else {
525                Log.i(Logging.LOG_TAG, this + " backstack: [push] " + getInstalledFragment()
526                        + " -> " + fragment);
527            }
528        }
529        final FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction();
530        if (mPreviousFragment != null) {
531            if (DEBUG_FRAGMENTS) {
532                Log.d(Logging.LOG_TAG, this + " showFragment: destroying previous fragment "
533                        + mPreviousFragment);
534            }
535            removeFragment(ft, mPreviousFragment);
536            mPreviousFragment = null;
537        }
538        // Remove or push the current one
539        if (clearBackStack) {
540            // Really remove the currently installed one.
541            removeInstalledFragment(ft);
542        }  else {
543            // Instead of removing, detach the current one and push into our back stack.
544            mPreviousFragment = getInstalledFragment();
545            if (mPreviousFragment != null) {
546                if (DEBUG_FRAGMENTS) {
547                    Log.d(Logging.LOG_TAG, this + " showFragment: detaching " + mPreviousFragment);
548                }
549                ft.detach(mPreviousFragment);
550            }
551        }
552        // Add the new one
553        if (DEBUG_FRAGMENTS) {
554            Log.d(Logging.LOG_TAG, this + " showFragment: adding " + fragment);
555        }
556        ft.add(R.id.fragment_placeholder, fragment);
557        ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
558        commitFragmentTransaction(ft);
559    }
560
561    /**
562     * @param isSystemBackKey <code>true</code> if the system back key was pressed.
563     *        <code>false</code> if it's caused by the "home" icon click on the action bar.
564     * @return true if we should pop from our custom back stack.
565     */
566    private boolean shouldPopFromBackStack(boolean isSystemBackKey) {
567        if (mPreviousFragment == null) {
568            return false; // Nothing in the back stack
569        }
570        // Never go back to Message View
571        if (mPreviousFragment instanceof MessageViewFragment) {
572            return false;
573        }
574        final Fragment installed = getInstalledFragment();
575        if (installed == null) {
576            // If no fragment is installed right now, do nothing.
577            return false;
578        }
579
580        // Okay now we have 2 fragments; the one in the back stack and the one that's currently
581        // installed.
582        if (mPreviousFragment.getClass() == installed.getClass()) {
583            // We never want to go back to the same kind of fragment, which happens when the user
584            // is on the message list, and selects another mailbox on the action bar.
585            return false;
586        }
587
588        if (isSystemBackKey) {
589            // In other cases, the system back key should always work.
590            return true;
591        } else {
592            // Home icon press -- there are cases where we don't want it to work.
593
594            // Disallow the Message list <-> mailbox list transition
595            if ((mPreviousFragment instanceof MailboxListFragment)
596                    && (installed  instanceof MessageListFragment)) {
597                return false;
598            }
599            if ((mPreviousFragment instanceof MessageListFragment)
600                    && (installed  instanceof MailboxListFragment)) {
601                return false;
602            }
603            return true;
604        }
605    }
606
607    /**
608     * Pop from our custom back stack.
609     *
610     * TODO Delay-call the whole method and use the synchronous transaction.
611     */
612    private void popFromBackStack() {
613        if (mPreviousFragment == null) {
614            return;
615        }
616        final FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction();
617        final Fragment installed = getInstalledFragment();
618        if (DEBUG_FRAGMENTS) {
619            Log.i(Logging.LOG_TAG, this + " backstack: [pop] " + installed + " -> "
620                    + mPreviousFragment);
621        }
622        removeFragment(ft, installed);
623        ft.attach(mPreviousFragment);
624        ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE);
625        mPreviousFragment = null;
626        commitFragmentTransaction(ft);
627        return;
628    }
629
630    private void showAllMailboxes() {
631        if (!isAccountSelected()) {
632            return; // Can happen because of asynchronous fragment transactions.
633        }
634        // Don't use open(account, NO_MAILBOX, NO_MESSAGE).  This is used to open the default
635        // view, which is Inbox on the message list.  (There's actually no way to open the mainbox
636        // list with open(long,long,long))
637        showMailboxList(getUIAccountId(), Mailbox.NO_MAILBOX, false);
638    }
639
640    /*
641     * STOPSHIP Remove this -- see the base class method.
642     */
643    @Override
644    public long getMailboxSettingsMailboxId() {
645        // Mailbox settings is still experimental, and doesn't have to work on the phone.
646        Utility.showToast(mActivity, "STOPSHIP: Mailbox settings not supported on 1 pane");
647        return Mailbox.NO_MAILBOX;
648    }
649
650    /*
651     * STOPSHIP Remove this -- see the base class method.
652     */
653    @Override
654    public long getSearchMailboxId() {
655        // Search is still experimental, and doesn't have to work on the phone.
656        Utility.showToast(mActivity, "STOPSHIP: Search not supported on 1 pane");
657        return Mailbox.NO_MAILBOX;
658    }
659
660    @Override
661    protected boolean isRefreshEnabled() {
662        // Refreshable only when an actual account is selected, and message view isn't shown.
663        // (i.e. only available on the mailbox list or the message view, but not on the combined
664        // one)
665        return isActualAccountSelected() && !isMessageViewVisible();
666    }
667
668    @Override
669    public void onRefresh() {
670        if (!isRefreshEnabled()) {
671            return;
672        }
673        if (isMessageListVisible()) {
674            mRefreshManager.refreshMessageList(getActualAccountId(), getMailboxId(), true);
675        } else {
676            mRefreshManager.refreshMailboxList(getActualAccountId());
677        }
678    }
679
680    @Override
681    protected boolean isRefreshInProgress() {
682        if (!isRefreshEnabled()) {
683            return false;
684        }
685        if (isMessageListVisible()) {
686            return mRefreshManager.isMessageListRefreshing(getMailboxId());
687        } else {
688            return mRefreshManager.isMailboxListRefreshing(getActualAccountId());
689        }
690    }
691}
692