UIControllerOnePane.java revision f3d07fb3e6ea7f40537c8bc45daae38d2d31853e
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.R;
27import com.android.emailcommon.Logging;
28import com.android.emailcommon.provider.Account;
29import com.android.emailcommon.provider.EmailContent.Message;
30import com.android.emailcommon.provider.Mailbox;
31import com.android.emailcommon.utility.Utility;
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 with swipe!
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        open(getUIAccountId(), Mailbox.NO_MAILBOX, Message.NO_MESSAGE);
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(getUIAccountId(), getMailboxId(), 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            // STOPSHIP implement search
263        }
264
265        @Override
266        public void onSearchExit() {
267            // STOPSHIP implement search
268        }
269    }
270
271    public UIControllerOnePane(EmailActivity activity) {
272        super(activity);
273    }
274
275    @Override
276    protected ActionBarController createActionBarController(Activity activity) {
277
278        // For now, we just reuse the same action bar controller used for 2-pane.
279        // We may change it later.
280
281        return new ActionBarController(activity, activity.getLoaderManager(),
282                activity.getActionBar(), new ActionBarControllerCallback());
283    }
284
285    @Override
286    public void onSaveInstanceState(Bundle outState) {
287        super.onSaveInstanceState(outState);
288        if (mPreviousFragment != null) {
289            mFragmentManager.putFragment(outState,
290                    BUNDLE_KEY_PREVIOUS_FRAGMENT, mPreviousFragment);
291        }
292    }
293
294    @Override
295    public void onRestoreInstanceState(Bundle savedInstanceState) {
296        super.onRestoreInstanceState(savedInstanceState);
297        mPreviousFragment = mFragmentManager.getFragment(savedInstanceState,
298                BUNDLE_KEY_PREVIOUS_FRAGMENT);
299    }
300
301    @Override
302    public int getLayoutId() {
303        return R.layout.email_activity_one_pane;
304    }
305
306    @Override
307    public void onActivityViewReady() {
308        super.onActivityViewReady();
309    }
310
311    @Override
312    public void onActivityCreated() {
313        super.onActivityCreated();
314    }
315
316    @Override
317    public long getUIAccountId() {
318        // Get it from the visible fragment.
319        if (isMailboxListInstalled()) {
320            return getMailboxListFragment().getAccountId();
321        }
322        if (isMessageListInstalled()) {
323            return getMessageListFragment().getAccountId();
324        }
325        if (isMessageViewInstalled()) {
326            return getMessageViewFragment().getOpenerAccountId();
327        }
328        return Account.NO_ACCOUNT;
329    }
330
331    private long getMailboxId() {
332        // Get it from the visible fragment.
333        if (isMessageListInstalled()) {
334            return getMessageListFragment().getMailboxId();
335        }
336        if (isMessageViewInstalled()) {
337            return getMessageViewFragment().getOpenerMailboxId();
338        }
339        return Mailbox.NO_MAILBOX;
340    }
341
342    @Override
343    public boolean onBackPressed(boolean isSystemBackKey) {
344        if (Email.DEBUG) {
345            // This is VERY important -- no check for DEBUG_LIFECYCLE
346            Log.d(Logging.LOG_TAG, this + " onBackPressed: " + isSystemBackKey);
347        }
348        // Super's method has precedence.  Must call it first.
349        if (super.onBackPressed(isSystemBackKey)) {
350            return true;
351        }
352        // If the mailbox list is shown and showing a nested mailbox, let it navigate up first.
353        if (isMailboxListInstalled() && getMailboxListFragment().navigateUp()) {
354            if (DEBUG_FRAGMENTS) {
355                Log.d(Logging.LOG_TAG, this + " Back: back handled by mailbox list");
356            }
357            return true;
358        }
359
360        // Custom back stack
361        if (shouldPopFromBackStack(isSystemBackKey)) {
362            if (DEBUG_FRAGMENTS) {
363                Log.d(Logging.LOG_TAG, this + " Back: Popping from back stack");
364            }
365            popFromBackStack();
366            return true;
367        }
368
369        // No entry in the back stack.
370        // If the message view is shown, show the "parent" message list.
371        // This happens when we get a deep link to a message.  (e.g. from a widget)
372        if (isMessageViewInstalled()) {
373            if (DEBUG_FRAGMENTS) {
374                Log.d(Logging.LOG_TAG, this + " Back: Message view -> Message List");
375            }
376            openMailbox(getMessageViewFragment().getOpenerAccountId(),
377                    getMessageViewFragment().getOpenerMailboxId());
378            return true;
379        }
380        return false;
381    }
382
383    @Override
384    public void open(final long accountId, final long mailboxId, final long messageId) {
385        if (Email.DEBUG) {
386            // This is VERY important -- no check for DEBUG_LIFECYCLE
387            Log.i(Logging.LOG_TAG, this + " open accountId=" + accountId
388                    + " mailboxId=" + mailboxId + " messageId=" + messageId);
389        }
390        if (accountId == Account.NO_ACCOUNT) {
391            throw new IllegalArgumentException();
392        }
393
394        if ((getUIAccountId() == accountId) && (getMailboxId() == mailboxId)
395                && (getMessageId() == messageId)) {
396            return;
397        }
398
399        final boolean accountChanging = (getUIAccountId() != accountId);
400        if (messageId != Message.NO_MESSAGE) {
401            showMessageView(accountId, mailboxId, messageId, accountChanging);
402        } else {
403            if (mailboxId == Mailbox.NO_MAILBOX) {
404                Log.e(Logging.LOG_TAG, this + " unspecified mailbox.");
405                return;
406            }
407            showMessageList(accountId, mailboxId, accountChanging);
408        }
409    }
410
411    /**
412     * @return currently installed {@link Fragment} (1-pane has only one at most), or null if none
413     *         exists.
414     */
415    private Fragment getInstalledFragment() {
416        if (isMailboxListInstalled()) {
417            return getMailboxListFragment();
418        } else if (isMessageListInstalled()) {
419            return getMessageListFragment();
420        } else if (isMessageViewInstalled()) {
421            return getMessageViewFragment();
422        }
423        return null;
424    }
425
426    /**
427     * Remove currently installed {@link Fragment} (1-pane has only one at most), or no-op if none
428     *         exists.
429     */
430    private void removeInstalledFragment(FragmentTransaction ft) {
431        removeFragment(ft, getInstalledFragment());
432    }
433
434    private void showMailboxList(long accountId, long mailboxId, boolean clearBackStack) {
435        showFragment(MailboxListFragment.newInstance(accountId, mailboxId, false), clearBackStack);
436    }
437
438    private void showMessageList(long accountId, long mailboxId, boolean clearBackStack) {
439        showFragment(MessageListFragment.newInstance(accountId, mailboxId), clearBackStack);
440    }
441
442    private void showMessageView(long accountId, long mailboxId, long messageId,
443            boolean clearBackStack) {
444        showFragment(MessageViewFragment.newInstance(accountId, mailboxId, messageId),
445                clearBackStack);
446    }
447
448    /**
449     * Use this instead of {@link FragmentTransaction#commit}.  We may switch to the asynchronous
450     * transaction some day.
451     */
452    private void commitFragmentTransaction(FragmentTransaction ft) {
453        if (!ft.isEmpty()) {
454            ft.commit();
455            mFragmentManager.executePendingTransactions();
456        }
457    }
458
459    /**
460     * Push the installed fragment into our custom back stack (or optionally
461     * {@link FragmentTransaction#remove} it) and {@link FragmentTransaction#add} {@code fragment}.
462     *
463     * @param fragment {@link Fragment} to be added.
464     * @param clearBackStack set {@code true} to remove the currently installed fragment.
465     *        {@code false} to push it into the backstack.
466     *
467     *  TODO Delay-call the whole method and use the synchronous transaction.
468     */
469    private void showFragment(Fragment fragment, boolean clearBackStack) {
470        if (DEBUG_FRAGMENTS) {
471            if (clearBackStack) {
472                Log.i(Logging.LOG_TAG, this + " backstack: [clear] showing " + fragment);
473            } else {
474                Log.i(Logging.LOG_TAG, this + " backstack: [push] " + getInstalledFragment()
475                        + " -> " + fragment);
476            }
477        }
478        final FragmentTransaction ft = mFragmentManager.beginTransaction();
479        if (mPreviousFragment != null) {
480            if (DEBUG_FRAGMENTS) {
481                Log.d(Logging.LOG_TAG, this + " showFragment: destroying previous fragment "
482                        + mPreviousFragment);
483            }
484            removeFragment(ft, mPreviousFragment);
485            mPreviousFragment = null;
486        }
487        // Remove or push the current one
488        if (clearBackStack) {
489            // Really remove the currently installed one.
490            removeInstalledFragment(ft);
491        }  else {
492            // Instead of removing, detach the current one and push into our back stack.
493            mPreviousFragment = getInstalledFragment();
494            if (mPreviousFragment != null) {
495                if (DEBUG_FRAGMENTS) {
496                    Log.d(Logging.LOG_TAG, this + " showFragment: detaching " + mPreviousFragment);
497                }
498                ft.detach(mPreviousFragment);
499            }
500        }
501        // Add the new one
502        if (DEBUG_FRAGMENTS) {
503            Log.d(Logging.LOG_TAG, this + " showFragment: adding " + fragment);
504        }
505        ft.add(R.id.fragment_placeholder, fragment);
506        ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
507        commitFragmentTransaction(ft);
508    }
509
510    /**
511     * @param isSystemBackKey <code>true</code> if the system back key was pressed.
512     *        <code>false</code> if it's caused by the "home" icon click on the action bar.
513     * @return true if we should pop from our custom back stack.
514     */
515    private boolean shouldPopFromBackStack(boolean isSystemBackKey) {
516        if (mPreviousFragment == null) {
517            return false; // Nothing in the back stack
518        }
519        // Never go back to Message View
520        if (mPreviousFragment instanceof MessageViewFragment) {
521            return false;
522        }
523        final Fragment installed = getInstalledFragment();
524        if (installed == null) {
525            // If no fragment is installed right now, do nothing.
526            return false;
527        }
528
529        // Okay now we have 2 fragments; the one in the back stack and the one that's currently
530        // installed.
531        if (mPreviousFragment.getClass() == installed.getClass()) {
532            // We never want to go back to the same kind of fragment, which happens when the user
533            // is on the message list, and selects another mailbox on the action bar.
534            return false;
535        }
536
537        if (isSystemBackKey) {
538            // In other cases, the system back key should always work.
539            return true;
540        } else {
541            // Home icon press -- there are cases where we don't want it to work.
542
543            // Disallow the Message list <-> mailbox list transition
544            if ((mPreviousFragment instanceof MailboxListFragment)
545                    && (installed  instanceof MessageListFragment)) {
546                return false;
547            }
548            if ((mPreviousFragment instanceof MessageListFragment)
549                    && (installed  instanceof MailboxListFragment)) {
550                return false;
551            }
552            return true;
553        }
554    }
555
556    /**
557     * Pop from our custom back stack.
558     *
559     * TODO Delay-call the whole method and use the synchronous transaction.
560     */
561    private void popFromBackStack() {
562        if (mPreviousFragment == null) {
563            return;
564        }
565        final FragmentTransaction ft = mFragmentManager.beginTransaction();
566        final Fragment installed = getInstalledFragment();
567        if (DEBUG_FRAGMENTS) {
568            Log.i(Logging.LOG_TAG, this + " backstack: [pop] " + installed + " -> "
569                    + mPreviousFragment);
570        }
571        removeFragment(ft, installed);
572        ft.attach(mPreviousFragment);
573        ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE);
574        mPreviousFragment = null;
575        commitFragmentTransaction(ft);
576        return;
577    }
578
579    private void showAllMailboxes() {
580        if (!isAccountSelected()) {
581            return; // Can happen because of asynchronous fragment transactions.
582        }
583        // Don't use open(account, NO_MAILBOX, NO_MESSAGE).  This is used to open the default
584        // view, which is Inbox on the message list.  (There's actually no way to open the mainbox
585        // list with open(long,long,long))
586        showMailboxList(getUIAccountId(), Mailbox.NO_MAILBOX, false);
587    }
588
589    /*
590     * STOPSHIP Remove this -- see the base class method.
591     */
592    @Override
593    public long getMailboxSettingsMailboxId() {
594        return isMessageListInstalled()
595                ? getMessageListFragment().getMailboxId()
596                : Mailbox.NO_MAILBOX;
597    }
598
599    @Override
600    protected boolean canSearch() {
601        return isMessageListInstalled();
602    }
603
604    @Override
605    protected boolean isRefreshEnabled() {
606        // Refreshable only when an actual account is selected, and message view isn't shown.
607        // (i.e. only available on the mailbox list or the message view, but not on the combined
608        // one)
609        return isActualAccountSelected() && !isMessageViewInstalled();
610    }
611
612    @Override
613    public void onRefresh() {
614        if (!isRefreshEnabled()) {
615            return;
616        }
617        if (isMessageListInstalled()) {
618            mRefreshManager.refreshMessageList(getActualAccountId(), getMailboxId(), true);
619        } else {
620            mRefreshManager.refreshMailboxList(getActualAccountId());
621        }
622    }
623
624    @Override
625    protected boolean isRefreshInProgress() {
626        if (!isRefreshEnabled()) {
627            return false;
628        }
629        if (isMessageListInstalled()) {
630            return mRefreshManager.isMessageListRefreshing(getMailboxId());
631        } else {
632            return mRefreshManager.isMailboxListRefreshing(getActualAccountId());
633        }
634    }
635}
636