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