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