UIControllerBase.java revision deb345acebce19996726262f7825c39a784b9fa8
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.FragmentManager;
22import android.app.FragmentTransaction;
23import android.os.Bundle;
24import android.util.Log;
25import android.view.Menu;
26import android.view.MenuInflater;
27import android.view.MenuItem;
28
29import com.android.email.Email;
30import com.android.email.MessageListContext;
31import com.android.email.R;
32import com.android.email.RefreshManager;
33import com.android.email.activity.setup.AccountSettings;
34import com.android.emailcommon.Logging;
35import com.android.emailcommon.provider.Account;
36import com.android.emailcommon.provider.EmailContent.Message;
37import com.android.emailcommon.provider.HostAuth;
38import com.android.emailcommon.provider.Mailbox;
39import com.android.emailcommon.utility.EmailAsyncTask;
40
41import java.util.LinkedList;
42import java.util.List;
43
44/**
45 * Base class for the UI controller.
46 *
47 * TODO Remove all the {@link MailboxFinder} stuff.  It's now done in {@link Welcome}.
48 */
49abstract class UIControllerBase implements MailboxListFragment.Callback,
50        MessageListFragment.Callback, MessageViewFragment.Callback  {
51    static final boolean DEBUG_FRAGMENTS = false; // DO NOT SUBMIT WITH TRUE
52
53    /** The owner activity */
54    final EmailActivity mActivity;
55    final FragmentManager mFragmentManager;
56
57    protected final ActionBarController mActionBarController;
58
59    final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
60
61    final RefreshManager mRefreshManager;
62
63    /**
64     * Fragments that are installed.
65     *
66     * A fragment is installed in {@link Fragment#onActivityCreated} and uninstalled in
67     * {@link Fragment#onDestroyView}, using {@link FragmentInstallable} callbacks.
68     *
69     * This means fragments in the back stack are *not* installed.
70     *
71     * We set callbacks to fragments only when they are installed.
72     *
73     * @see FragmentInstallable
74     */
75    private MailboxListFragment mMailboxListFragment;
76    private MessageListFragment mMessageListFragment;
77    private MessageViewFragment mMessageViewFragment;
78
79    /**
80     * To avoid double-deleting a fragment (which will cause a runtime exception),
81     * we put a fragment in this list when we {@link FragmentTransaction#remove(Fragment)} it,
82     * and remove from the list when we actually uninstall it.
83     */
84    private final List<Fragment> mRemovedFragments = new LinkedList<Fragment>();
85
86    /**
87     * The active context for the current MessageList.
88     * In some UI layouts such as the one-pane view, the message list may not be visible, but is
89     * on the backstack. This list context will still be accessible in those cases.
90     */
91    protected MessageListContext mListContext;
92
93    private final RefreshManager.Listener mRefreshListener
94            = new RefreshManager.Listener() {
95        @Override
96        public void onMessagingError(final long accountId, long mailboxId, final String message) {
97            refreshActionBar();
98        }
99
100        @Override
101        public void onRefreshStatusChanged(long accountId, long mailboxId) {
102            refreshActionBar();
103        }
104    };
105
106    public UIControllerBase(EmailActivity activity) {
107        mActivity = activity;
108        mFragmentManager = activity.getFragmentManager();
109        mRefreshManager = RefreshManager.getInstance(mActivity);
110        mActionBarController = createActionBarController(activity);
111        if (DEBUG_FRAGMENTS) {
112            FragmentManager.enableDebugLogging(true);
113        }
114    }
115
116    /**
117     * Called by the base class to let a subclass create an {@link ActionBarController}.
118     */
119    protected abstract ActionBarController createActionBarController(Activity activity);
120
121    /** @return the layout ID for the activity. */
122    public abstract int getLayoutId();
123
124    /**
125     * Must be called just after the activity sets up the content view.  Used to initialize views.
126     *
127     * (Due to the complexity regarding class/activity initialization order, we can't do this in
128     * the constructor.)
129     */
130    public void onActivityViewReady() {
131        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
132            Log.d(Logging.LOG_TAG, this + " onActivityViewReady");
133        }
134    }
135
136    /**
137     * Called at the end of {@link EmailActivity#onCreate}.
138     */
139    public void onActivityCreated() {
140        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
141            Log.d(Logging.LOG_TAG, this + " onActivityCreated");
142        }
143        mRefreshManager.registerListener(mRefreshListener);
144        mActionBarController.onActivityCreated();
145    }
146
147    /**
148     * Handles the {@link android.app.Activity#onStart} callback.
149     */
150    public void onActivityStart() {
151        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
152            Log.d(Logging.LOG_TAG, this + " onActivityStart");
153        }
154    }
155
156    /**
157     * Handles the {@link android.app.Activity#onResume} callback.
158     */
159    public void onActivityResume() {
160        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
161            Log.d(Logging.LOG_TAG, this + " onActivityResume");
162        }
163        refreshActionBar();
164    }
165
166    /**
167     * Handles the {@link android.app.Activity#onPause} callback.
168     */
169    public void onActivityPause() {
170        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
171            Log.d(Logging.LOG_TAG, this + " onActivityPause");
172        }
173    }
174
175    /**
176     * Handles the {@link android.app.Activity#onStop} callback.
177     */
178    public void onActivityStop() {
179        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
180            Log.d(Logging.LOG_TAG, this + " onActivityStop");
181        }
182    }
183
184    /**
185     * Handles the {@link android.app.Activity#onDestroy} callback.
186     */
187    public void onActivityDestroy() {
188        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
189            Log.d(Logging.LOG_TAG, this + " onActivityDestroy");
190        }
191        mActionBarController.onActivityDestroy();
192        mRefreshManager.unregisterListener(mRefreshListener);
193        mTaskTracker.cancellAllInterrupt();
194    }
195
196    /**
197     * Handles the {@link android.app.Activity#onSaveInstanceState} callback.
198     */
199    public void onSaveInstanceState(Bundle outState) {
200        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
201            Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
202        }
203        mActionBarController.onSaveInstanceState(outState);
204    }
205
206    /**
207     * Handles the {@link android.app.Activity#onRestoreInstanceState} callback.
208     */
209    public void onRestoreInstanceState(Bundle savedInstanceState) {
210        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
211            Log.d(Logging.LOG_TAG, this + " restoreInstanceState");
212        }
213        mActionBarController.onRestoreInstanceState(savedInstanceState);
214    }
215
216    /**
217     * Install a fragment.  Must be caleld from the host activity's
218     * {@link FragmentInstallable#onInstallFragment}.
219     */
220    public final void onInstallFragment(Fragment fragment) {
221        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
222            Log.d(Logging.LOG_TAG, this + " onInstallFragment  fragment=" + fragment);
223        }
224        if (fragment instanceof MailboxListFragment) {
225            installMailboxListFragment((MailboxListFragment) fragment);
226        } else if (fragment instanceof MessageListFragment) {
227            installMessageListFragment((MessageListFragment) fragment);
228        } else if (fragment instanceof MessageViewFragment) {
229            installMessageViewFragment((MessageViewFragment) fragment);
230        } else {
231            throw new IllegalArgumentException("Tried to install unknown fragment");
232        }
233    }
234
235    /** Install fragment */
236    protected void installMailboxListFragment(MailboxListFragment fragment) {
237        mMailboxListFragment = fragment;
238        mMailboxListFragment.setCallback(this);
239        refreshActionBar();
240    }
241
242    /** Install fragment */
243    protected void installMessageListFragment(MessageListFragment fragment) {
244        mMessageListFragment = fragment;
245        mMessageListFragment.setCallback(this);
246        refreshActionBar();
247    }
248
249    /** Install fragment */
250    protected void installMessageViewFragment(MessageViewFragment fragment) {
251        mMessageViewFragment = fragment;
252        mMessageViewFragment.setCallback(this);
253        refreshActionBar();
254    }
255
256    /**
257     * Uninstall a fragment.  Must be caleld from the host activity's
258     * {@link FragmentInstallable#onUninstallFragment}.
259     */
260    public final void onUninstallFragment(Fragment fragment) {
261        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
262            Log.d(Logging.LOG_TAG, this + " onUninstallFragment  fragment=" + fragment);
263        }
264        mRemovedFragments.remove(fragment);
265        if (fragment == mMailboxListFragment) {
266            uninstallMailboxListFragment();
267        } else if (fragment == mMessageListFragment) {
268            uninstallMessageListFragment();
269        } else if (fragment == mMessageViewFragment) {
270            uninstallMessageViewFragment();
271        } else {
272            throw new IllegalArgumentException("Tried to uninstall unknown fragment");
273        }
274    }
275
276    /** Uninstall {@link MailboxListFragment} */
277    protected void uninstallMailboxListFragment() {
278        mMailboxListFragment.setCallback(null);
279        mMailboxListFragment = null;
280    }
281
282    /** Uninstall {@link MessageListFragment} */
283    protected void uninstallMessageListFragment() {
284        mMessageListFragment.setCallback(null);
285        mMessageListFragment = null;
286    }
287
288    /** Uninstall {@link MessageViewFragment} */
289    protected void uninstallMessageViewFragment() {
290        mMessageViewFragment.setCallback(null);
291        mMessageViewFragment = null;
292    }
293
294    /**
295     * If a {@link Fragment} is not already in {@link #mRemovedFragments},
296     * {@link FragmentTransaction#remove} it and add to the list.
297     *
298     * Do nothing if {@code fragment} is null.
299     */
300    protected final void removeFragment(FragmentTransaction ft, Fragment fragment) {
301        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
302            Log.d(Logging.LOG_TAG, this + " removeFragment fragment=" + fragment);
303        }
304        if (fragment == null) {
305            return;
306        }
307        if (!mRemovedFragments.contains(fragment)) {
308            ft.remove(fragment);
309            addFragmentToRemovalList(fragment);
310        }
311    }
312
313    /**
314     * Remove a {@link Fragment} from {@link #mRemovedFragments}.  No-op if {@code fragment} is
315     * null.
316     *
317     * {@link #removeMailboxListFragment}, {@link #removeMessageListFragment} and
318     * {@link #removeMessageViewFragment} all call this, so subclasses don't have to do this when
319     * using them.
320     *
321     * However, unfortunately, subclasses have to call this manually when popping from the
322     * back stack to avoid double-delete.
323     */
324    protected void addFragmentToRemovalList(Fragment fragment) {
325        if (fragment != null) {
326            mRemovedFragments.add(fragment);
327        }
328    }
329
330    /**
331     * Remove the fragment if it's installed.
332     */
333    protected FragmentTransaction removeMailboxListFragment(FragmentTransaction ft) {
334        removeFragment(ft, mMailboxListFragment);
335        return ft;
336    }
337
338    /**
339     * Remove the fragment if it's installed.
340     */
341    protected FragmentTransaction removeMessageListFragment(FragmentTransaction ft) {
342        removeFragment(ft, mMessageListFragment);
343        return ft;
344    }
345
346    /**
347     * Remove the fragment if it's installed.
348     */
349    protected FragmentTransaction removeMessageViewFragment(FragmentTransaction ft) {
350        removeFragment(ft, mMessageViewFragment);
351        return ft;
352    }
353
354    /** @return true if a {@link MailboxListFragment} is installed. */
355    protected final boolean isMailboxListInstalled() {
356        return mMailboxListFragment != null;
357    }
358
359    /** @return true if a {@link MessageListFragment} is installed. */
360    protected final boolean isMessageListInstalled() {
361        return mMessageListFragment != null;
362    }
363
364    /** @return true if a {@link MessageViewFragment} is installed. */
365    protected final boolean isMessageViewInstalled() {
366        return mMessageViewFragment != null;
367    }
368
369    /** @return the installed {@link MailboxListFragment} or null. */
370    protected final MailboxListFragment getMailboxListFragment() {
371        return mMailboxListFragment;
372    }
373
374    /** @return the installed {@link MessageListFragment} or null. */
375    protected final MessageListFragment getMessageListFragment() {
376        return mMessageListFragment;
377    }
378
379    /** @return the installed {@link MessageViewFragment} or null. */
380    protected final MessageViewFragment getMessageViewFragment() {
381        return mMessageViewFragment;
382    }
383
384    /**
385     * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
386     *
387     * @see #getActualAccountId()
388     */
389    public abstract long getUIAccountId();
390
391    /**
392     * @return true if an account is selected, or the current view is the combined view.
393     */
394    public final boolean isAccountSelected() {
395        return getUIAccountId() != Account.NO_ACCOUNT;
396    }
397
398    /**
399     * @return if an actual account is selected.  (i.e. {@link Account#ACCOUNT_ID_COMBINED_VIEW}
400     * is not considered "actual".s)
401     */
402    public final boolean isActualAccountSelected() {
403        return isAccountSelected() && (getUIAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW);
404    }
405
406    /**
407     * @return the currently selected account ID.  If the current view is the combined view,
408     * it'll return {@link Account#NO_ACCOUNT}.
409     *
410     * @see #getUIAccountId()
411     */
412    public final long getActualAccountId() {
413        return isActualAccountSelected() ? getUIAccountId() : Account.NO_ACCOUNT;
414    }
415
416    /**
417     * Show the default view for the given account.
418     *
419     * No-op if the given account is already selected.
420     *
421     * @param accountId ID of the account to load.  Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
422     *     Must never be {@link Account#NO_ACCOUNT}.
423     */
424    public final void switchAccount(long accountId) {
425
426        // STOPSHIP Do the security hold check here too.
427
428        if (accountId == getUIAccountId()) {
429            // Do nothing if the account is already selected.  Not even going back to the inbox.
430            return;
431        }
432
433        if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
434            openMailbox(accountId, Mailbox.QUERY_ALL_INBOXES);
435        } else {
436            long inboxId = Mailbox.findMailboxOfType(mActivity, accountId, Mailbox.TYPE_INBOX);
437            if (inboxId == Mailbox.NO_MAILBOX) {
438                // The account doesn't have Inbox yet... Redirect to Welcome and let it wait for
439                // the initial sync...
440                Log.w(Logging.LOG_TAG, "Account " + accountId +" doesn't have Inbox.  Redirecting"
441                        + " to Welcome...");
442                Welcome.actionOpenAccountInbox(mActivity, accountId);
443                mActivity.finish();
444                return;
445            } else {
446                openMailbox(accountId, inboxId);
447            }
448        }
449    }
450
451    /**
452     * Returns the id of the parent mailbox used for the mailbox list fragment.
453     *
454     * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with
455     *     {@link #getMessageListMailboxId()}
456     */
457    protected long getMailboxListMailboxId() {
458        return isMailboxListInstalled() ? getMailboxListFragment().getSelectedMailboxId()
459                : Mailbox.NO_MAILBOX;
460    }
461
462    /**
463     * Returns the id of the mailbox used for the message list fragment.
464     *
465     * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with
466     *     {@link #getMessageListMailboxId()}
467     */
468    protected long getMessageListMailboxId() {
469        return isMessageListInstalled() ? getMessageListFragment().getMailboxId()
470                : Mailbox.NO_MAILBOX;
471    }
472
473    /**
474     * Shortcut for {@link #open} with {@link Message#NO_MESSAGE}.
475     */
476    protected final void openMailbox(long accountId, long mailboxId) {
477        open(MessageListContext.forMailbox(accountId, mailboxId), Message.NO_MESSAGE);
478    }
479
480    /**
481     * Opens a given list
482     * @param listContext the list context for the message list to open
483     * @param messageId if specified and not {@link Message#NO_MESSAGE}, will open the message
484     *     in the message list.
485     */
486    public final void open(final MessageListContext listContext, final long messageId) {
487        mListContext = listContext;
488        openInternal(listContext, messageId);
489
490        if (mListContext.isSearch()) {
491            mActionBarController.enterSearchMode(mListContext.getSearchParams().mFilter);
492        }
493    }
494
495    protected abstract void openInternal(
496            final MessageListContext listContext, final long messageId);
497
498    /**
499     * Performs the back action.
500     *
501     * NOTE The method in the base class has precedence.  Subclasses overriding this method MUST
502     * call super's method first.
503     *
504     * @param isSystemBackKey <code>true</code> if the system back key was pressed.
505     * <code>false</code> if it's caused by the "home" icon click on the action bar.
506     */
507    public boolean onBackPressed(boolean isSystemBackKey) {
508        if (mActionBarController.onBackPressed(isSystemBackKey)) {
509            return true;
510        }
511        return false;
512    }
513
514    /**
515     * Must be called from {@link Activity#onSearchRequested()}.
516     * This initiates the search entry mode - see {@link #onSearchSubmit} for when the search
517     * is actually submitted.
518     */
519    public void onSearchRequested() {
520        mActionBarController.enterSearchMode(null);
521    }
522
523    /** @return true if the search menu option should be enabled based on the current UI. */
524    protected boolean canSearch() {
525        return false;
526    }
527
528    /**
529     * Kicks off a search query, if the UI is in a state where a search is possible.
530     */
531    protected void onSearchSubmit(final String queryTerm) {
532        final long accountId = getUIAccountId();
533        if (!Account.isNormalAccount(accountId)) {
534            return; // Invalid account to search from.
535        }
536
537        // TODO: do a global search for EAS inbox.
538        // TODO: handle doing another search from a search result, in which case we should
539        //       search the original mailbox that was searched, and not search in the search mailbox
540        final long mailboxId = getMessageListMailboxId();
541
542        if (Email.DEBUG) {
543            Log.d(Logging.LOG_TAG, "Submitting search: " + queryTerm);
544        }
545
546        mActivity.startActivity(EmailActivity.createSearchIntent(
547                mActivity, accountId, mailboxId, queryTerm));
548
549
550        // TODO: this causes a slight flicker.
551        // A new instance of the activity will sit on top. When the user exits search and
552        // returns to this activity, the search box should not be open then.
553        mActionBarController.exitSearchMode();
554    }
555
556    /**
557     * Handles exiting of search entry mode.
558     */
559    protected void onSearchExit() {
560        if ((mListContext != null) && mListContext.isSearch()) {
561            mActivity.finish();
562        }
563    }
564
565    /**
566     * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback.
567     */
568    public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) {
569        inflater.inflate(R.menu.email_activity_options, menu);
570        return true;
571    }
572
573    /**
574     * Handles the {@link android.app.Activity#onPrepareOptionsMenu} callback.
575     */
576    public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) {
577
578        // Update the refresh button.
579        MenuItem item = menu.findItem(R.id.refresh);
580        if (isRefreshEnabled()) {
581            item.setVisible(true);
582            if (isRefreshInProgress()) {
583                item.setActionView(R.layout.action_bar_indeterminate_progress);
584            } else {
585                item.setActionView(null);
586            }
587        } else {
588            item.setVisible(false);
589        }
590
591        // Deal with protocol-specific menu options.
592        boolean isEas = false;
593        boolean accountSearchable = false;
594        long accountId = getActualAccountId();
595        if (accountId > 0) {
596            Account account = Account.restoreAccountWithId(mActivity, accountId);
597            if (account != null) {
598                String protocol = account.getProtocol(mActivity);
599                if (HostAuth.SCHEME_EAS.equals(protocol)) {
600                    isEas = true;
601                }
602                accountSearchable = (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0;
603            }
604        }
605
606        // TODO: Should use an isSyncable call to prevent drafts/outbox from allowing this
607        menu.findItem(R.id.search).setVisible(accountSearchable && canSearch());
608        menu.findItem(R.id.sync_lookback).setVisible(isEas);
609        menu.findItem(R.id.sync_frequency).setVisible(isEas);
610
611        return true;
612    }
613
614    /**
615     * Handles the {@link android.app.Activity#onOptionsItemSelected} callback.
616     *
617     * @return true if the option item is handled.
618     */
619    public boolean onOptionsItemSelected(MenuItem item) {
620        switch (item.getItemId()) {
621            case android.R.id.home:
622                // Comes from the action bar when the app icon on the left is pressed.
623                // It works like a back press, but it won't close the activity.
624                return onBackPressed(false);
625            case R.id.compose:
626                return onCompose();
627            case R.id.refresh:
628                onRefresh();
629                return true;
630            case R.id.account_settings:
631                return onAccountSettings();
632            case R.id.search:
633                onSearchRequested();
634                return true;
635        }
636        return false;
637    }
638
639    /**
640     * Opens the message compose activity.
641     */
642    private boolean onCompose() {
643        if (!isAccountSelected()) {
644            return false; // this shouldn't really happen
645        }
646        MessageCompose.actionCompose(mActivity, getActualAccountId());
647        return true;
648    }
649
650    /**
651     * Handles the "Settings" option item.  Opens the settings activity.
652     */
653    private boolean onAccountSettings() {
654        AccountSettings.actionSettings(mActivity, getActualAccountId());
655        return true;
656    }
657
658    /**
659     * @return the ID of the message in focus and visible, if any. Returns
660     *     {@link Message#NO_MESSAGE} if no message is opened.
661     */
662    protected long getMessageId() {
663        return isMessageViewInstalled()
664                ? getMessageViewFragment().getMessageId()
665                : Message.NO_MESSAGE;
666    }
667
668
669    /**
670     * STOPSHIP For experimental UI.  Remove this.
671     *
672     * @return mailbox ID for "mailbox settings" option.
673     */
674    public abstract long getMailboxSettingsMailboxId();
675
676    /**
677     * STOPSHIP For experimental UI.  Make it abstract protected.
678     *
679     * Performs "refesh".
680     */
681    public abstract void onRefresh();
682
683    /**
684     * @return true if refresh is in progress for the current mailbox.
685     */
686    protected abstract boolean isRefreshInProgress();
687
688    /**
689     * @return true if the UI should enable the "refresh" command.
690     */
691    protected abstract boolean isRefreshEnabled();
692
693    /**
694     * Refresh the action bar and menu items, including the "refreshing" icon.
695     */
696    protected void refreshActionBar() {
697        if (mActionBarController != null) {
698            mActionBarController.refresh();
699        }
700        mActivity.invalidateOptionsMenu();
701    }
702
703
704    @Override
705    public String toString() {
706        return getClass().getSimpleName(); // Shown on logcat
707    }
708}
709