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