com.google.android.marvin.talkback.TalkBackService.java Source code

Java tutorial

Introduction

Here is the source code for com.google.android.marvin.talkback.TalkBackService.java

Source

/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.google.android.marvin.talkback;

import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.PowerManager;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.support.v4.view.accessibility.AccessibilityManagerCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.view.accessibility.AccessibilityRecordCompat;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.CheckBox;
import android.widget.ScrollView;
import android.widget.TextView;

import com.google.android.marvin.talkback.CursorController.CursorControllerListener;
import com.google.android.marvin.talkback.KeyComboManager.KeyComboListener;
import com.google.android.marvin.talkback.TextToSpeechManager.TtsDiscoveryListener;
import com.google.android.marvin.talkback.speechrules.NodeHintRule;
import com.google.android.marvin.talkback.speechrules.NodeSpeechRuleProcessor;
import com.google.android.marvin.talkback.test.TalkBackListener;
import com.google.android.marvin.talkback.tutorial.AccessibilityTutorialActivity;
import com.google.android.marvin.talkback.tutorial.GestureActionMonitor;
import com.google.android.marvin.talkback.tutorial.GranularityMonitor;
import com.googlecode.eyesfree.compat.accessibilityservice.AccessibilityServiceCompatUtils;
import com.googlecode.eyesfree.compat.view.accessibility.AccessibilityEventCompatUtils;
import com.googlecode.eyesfree.compat.view.accessibility.AccessibilityServiceInfoCompatUtils;
import com.googlecode.eyesfree.labeling.CustomLabelManager;
import com.googlecode.eyesfree.labeling.PackageRemovalReceiver;
import com.googlecode.eyesfree.utils.AccessibilityEventListener;
import com.googlecode.eyesfree.utils.AccessibilityEventUtils;
import com.googlecode.eyesfree.utils.AccessibilityNodeInfoUtils;
import com.googlecode.eyesfree.utils.ClassLoadingManager;
import com.googlecode.eyesfree.utils.LogUtils;
import com.googlecode.eyesfree.utils.SharedPreferencesUtils;
import com.googlecode.eyesfree.utils.TtsEngineUtils.TtsEngineInfo;
import com.googlecode.eyesfree.utils.WebInterfaceUtils;

import java.lang.Thread.UncaughtExceptionHandler;
import java.util.LinkedList;
import java.util.List;

/**
 * An {@link AccessibilityService} that provides spoken, haptic, and audible
 * feedback.
 *
 * @author alanv@google.com (Alan Viverette)
 */
public class TalkBackService extends AccessibilityService implements Thread.UncaughtExceptionHandler {
    /** Whether the current SDK supports optional touch exploration. */
    /* package */ static final boolean SUPPORTS_TOUCH_PREF = (Build.VERSION.SDK_INT >= 16);

    /** Whether the user has seen the TalkBack tutorial. */
    /* package */ static final String PREF_FIRST_TIME_USER = "first_time_user";

    /** Permission required to perform gestures. */
    /* package */ static final String PERMISSION_TALKBACK = "com.google.android.marvin.feedback.permission.TALKBACK";

    /** The minimum delay between window state change and automatic events. */
    /* package */ static final long DELAY_AUTO_AFTER_STATE = 100;

    /** Event types to drop after receiving a window state change. */
    /* package */ static final int AUTOMATIC_AFTER_STATE_CHANGE = AccessibilityEvent.TYPE_VIEW_FOCUSED
            | AccessibilityEvent.TYPE_VIEW_SELECTED | AccessibilityEventCompat.TYPE_VIEW_SCROLLED;

    /** The intent action used to perform a custom gesture action. */
    /* package */ static final String ACTION_PERFORM_GESTURE_ACTION = "performCustomGestureAction";

    /**
     * The gesture action to pass with {@link #ACTION_PERFORM_GESTURE_ACTION} as
     * a string extra. Must match the name of a {@link ShortcutGestureAction}.
     */
    /* package */ static final String EXTRA_GESTURE_ACTION = "gestureAction";

    /** Whether the current SDK supports service-managed web scripts. */
    private static final boolean SUPPORTS_WEB_SCRIPT_TOGGLE = (Build.VERSION.SDK_INT >= 18);

    /** Whether to force debugging mode on. Turn off when releasing. */
    private static final boolean DEBUG = false;

    /** Event types that are allowed to interrupt radial menus. */
    // TODO: What's the rationale for HOVER_ENTER? Navigation bar?
    private static final int MASK_EVENT_TYPES_INTERRUPT_RADIAL_MENU = AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER
            | AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED
            | AccessibilityEventCompat.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY;

    /**
     * Event types that signal a change in touch interaction state and should be
     * dropped on {@link Configuration#TOUCHSCREEN_NOTOUCH} devices
     */
    private static final int MASK_EVENT_TYPES_TOUCH_STATE_CHANGES = AccessibilityEventCompat.TYPE_GESTURE_DETECTION_START
            | AccessibilityEventCompat.TYPE_GESTURE_DETECTION_END
            | AccessibilityEventCompat.TYPE_TOUCH_EXPLORATION_GESTURE_START
            | AccessibilityEventCompat.TYPE_TOUCH_EXPLORATION_GESTURE_END
            | AccessibilityEventCompat.TYPE_TOUCH_INTERACTION_START
            | AccessibilityEventCompat.TYPE_TOUCH_INTERACTION_END | AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER
            | AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT;

    /** Action used to resume feedback. */
    private static final String ACTION_RESUME_FEEDBACK = "com.google.android.marvin.talkback.RESUME_FEEDBACK";

    /** An active instance of TalkBack. */
    private static TalkBackService sInstance = null;

    /** The possible states of the service. */
    public enum ServiceState {
        /**
         * The state of the service before the system has bound to it or after
         * it is destroyed.
         */
        INACTIVE,

        /** The state of the service when it initialized and active. */
        ACTIVE,

        /** The state of the service when it has been suspended by the user. */
        SUSPENDED
    }

    /**
     * List of passive event processors. All processors in the list are sent the
     * event in the order they were added.
     */
    private final LinkedList<AccessibilityEventListener> mAccessibilityEventListeners = new LinkedList<AccessibilityEventListener>();

    /**
     * List of key event processors. Processors in the list are sent the event
     * in the order they were added until a processor consumes the event.
     */
    private final LinkedList<KeyEventListener> mKeyEventListeners = new LinkedList<KeyEventListener>();

    /** The current state of the service. */
    private ServiceState mServiceState;

    /** Components to receive callbacks on changes in the service's state. */
    private List<ServiceStateListener> mServiceStateListeners = new LinkedList<ServiceStateListener>();

    /** TalkBack-specific listener used for testing. */
    private TalkBackListener mTestingListener;

    /** Controller for cursor movement. */
    private CursorController mCursorController;

    /** Controller for speech feedback. */
    private SpeechController mSpeechController;

    /** Controller for audio and haptic feedback. */
    private MappedFeedbackController mFeedbackController;

    /** Controller for reading the entire hierarchy. */
    private FullScreenReadController mFullScreenReadController;

    /** Listener for device shake events. */
    private ShakeDetector mShakeDetector;

    /** Manager for tracking available TTS engines and languages. */
    private TextToSpeechManager mTextToSpeechManager;

    /** Manager for showing radial menus. */
    private RadialMenuManager mRadialMenuManager;

    /** Manager for handling custom labels. */
    private CustomLabelManager mLabelManager;

    /** Processor for moving access focus. Used in Jelly Bean and above. */
    private ProcessorFocusAndSingleTap mProcessorFollowFocus;

    /** Processor for generating and providing feedback for events. */
    private ProcessorEventQueue mProcessorEventQueue;

    /** Orientation monitor for watching orientation changes. */
    private OrientationMonitor mOrientationMonitor;

    /** {@link BroadcastReceiver} for tracking the ringer and screen states. */
    private RingerModeAndScreenMonitor mRingerModeAndScreenMonitor;

    /** {@link BroadcastReceiver} for tracking the call state. */
    private CallStateMonitor mCallStateMonitor;

    /** {@link BroadcastReceiver} for tracking volume changes. */
    private VolumeMonitor mVolumeMonitor;

    /**
     * {@link BroadcastReceiver} for tracking package removals for custom label
     * data consistency.
     */
    private PackageRemovalReceiver mPackageReceiver;

    /** Power manager, used for checking screen state. */
    private PowerManager mPowerManager;

    /** The accessibility manager, used for querying state. */
    private AccessibilityManager mAccessibilityManager;

    /** The last spoken accessibility event, used to detect duplicate events. */
    private AccessibilityEvent mLastSpokenEvent;

    /** Event time for the most recent window state changed event. */
    private long mLastWindowStateChanged = 0;

    /** Text-to-speech overlay for debugging speech output. */
    private TextToSpeechOverlay mTtsOverlay;

    /** Alert dialog shown when the user attempts to suspend feedback. */
    private AlertDialog mSuspendDialog;

    /** Shared preferences used within TalkBack. */
    private SharedPreferences mPrefs;

    /** The system's uncaught exception handler */
    private UncaughtExceptionHandler mSystemUeh;

    /**
     * Whether speech should be allowed when the screen is off. When set to
     * {@code false}, the phone will remain silent when the screen is off.
     */
    private boolean mSpeakWhenScreenOff;

    /**
     * Whether TalkBack should speak caller ID information.
     */
    private boolean mSpeakCallerId;

    /**
     * Whether TalkBack should interpret down-then-up and up-then-down gestures
     * as granularity cycles or moving of accessibility focus to the first or
     * last item on screen.
     * <p>
     * {@code true} to use granularity cycle, or {@code false} for moving focus.
     */
    private boolean mVerticalGestureCycleGranularity;

    /**
     * Keep track of whether the user is currently touch exploring so that we
     * don't interrupt with notifications.
     */
    private boolean mIsUserTouchExploring;

    /** Preference specifying when TalkBack should automatically resume. */
    private AutomaticResumePreference mAutomaticResume;

    @Override
    public void onCreate() {
        super.onCreate();

        sInstance = this;
        setServiceState(ServiceState.INACTIVE);

        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);

        mSystemUeh = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);

        initializeInfrastructure();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        if (isServiceActive()) {
            suspendInfrastructure();
        }

        sInstance = null;

        // Shutdown and unregister all components.
        shutdownInfrastructure();
        setServiceState(ServiceState.INACTIVE);
        mServiceStateListeners.clear();
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        if (isServiceActive() && (mOrientationMonitor != null)) {
            mOrientationMonitor.onConfigurationChanged(newConfig);
        }

        // Clear the radial menu cache to reload localized strings.
        mRadialMenuManager.clearCache();
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (mTestingListener != null) {
            mTestingListener.onAccessibilityEvent(event);
        }

        if (shouldDropEvent(event)) {
            return;
        }

        maintainExplorationState(event);

        // TODO(alanv): Figure out if this is actually needed for API < 14.
        if (Build.VERSION.SDK_INT < 14) {
            cacheEvent(event);
        }

        processEvent(event);
    }

    private void setServiceState(ServiceState newState) {
        if (mServiceState == newState) {
            return;
        }

        mServiceState = newState;
        for (ServiceStateListener listener : mServiceStateListeners) {
            listener.onServiceStateChanged(newState);
        }
    }

    public void addServiceStateListener(ServiceStateListener listener) {
        if (listener != null) {
            mServiceStateListeners.add(listener);
        }
    }

    public void removeServiceStateListener(ServiceStateListener listener) {
        if (listener != null) {
            mServiceStateListeners.remove(listener);
        }
    }

    public void setTestingListener(TalkBackListener testingListener) {
        mTestingListener = testingListener;

        if (mProcessorEventQueue != null) {
            mProcessorEventQueue.setTestingListener(testingListener);
        }
    }

    /**
     * Suspends TalkBack, showing a confirmation dialog if applicable.
     */
    public void requestSuspendTalkBack() {
        final boolean showConfirmation = SharedPreferencesUtils.getBooleanPref(mPrefs, getResources(),
                R.string.pref_show_suspension_confirmation_dialog,
                R.bool.pref_show_suspension_confirmation_dialog_default);
        if (showConfirmation) {
            confirmSuspendTalkBack();
        } else {
            suspendTalkBack();
        }
    }

    /**
     * Shows a dialog asking the user to confirm suspension of TalkBack.
     */
    private void confirmSuspendTalkBack() {
        // Ensure only one dialog is showing.
        if (mSuspendDialog != null) {
            if (mSuspendDialog.isShowing()) {
                return;
            } else {
                mSuspendDialog.dismiss();
                mSuspendDialog = null;
            }
        }

        final LayoutInflater inflater = LayoutInflater.from(this);
        final ScrollView root = (ScrollView) inflater.inflate(R.layout.suspend_talkback_dialog, null);
        final CheckBox confirmCheckBox = (CheckBox) root.findViewById(R.id.show_warning_checkbox);
        final TextView message = (TextView) root.findViewById(R.id.message_resume);

        final DialogInterface.OnClickListener okayClick = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                switch (which) {
                case DialogInterface.BUTTON_POSITIVE:
                    if (!confirmCheckBox.isChecked()) {
                        SharedPreferencesUtils.putBooleanPref(mPrefs, getResources(),
                                R.string.pref_show_suspension_confirmation_dialog, false);
                    }

                    suspendTalkBack();
                    break;
                }
            }
        };

        final OnDismissListener onDismissListener = new OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                mSuspendDialog = null;
            }
        };

        switch (mAutomaticResume) {
        case KEYGUARD:
            message.setText(getString(R.string.message_resume_keyguard));
            break;
        case SCREEN_ON:
            message.setText(getString(R.string.message_resume_screen_on));
            break;
        case MANUAL:
            message.setText(getString(R.string.message_resume_manual));
            break;
        }

        mSuspendDialog = new AlertDialog.Builder(this).setTitle(R.string.dialog_title_suspend_talkback)
                .setView(root).setNegativeButton(android.R.string.cancel, null)
                .setPositiveButton(android.R.string.ok, okayClick).create();

        // Ensure we can show the dialog from this service.
        mSuspendDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
        mSuspendDialog.setOnDismissListener(onDismissListener);
        mSuspendDialog.show();
    }

    /**
     * Suspends TalkBack and Explore by Touch.
     */
    private void suspendTalkBack() {
        if (!isServiceActive()) {
            LogUtils.log(this, Log.ERROR, "Attempted to suspend TalkBack while already suspended.");
            return;
        }

        mFeedbackController.playAuditory(R.id.sounds_paused_feedback);

        if (SUPPORTS_TOUCH_PREF) {
            requestTouchExploration(false);
        }

        if (mCursorController != null) {
            mCursorController.clearCursor();
        }

        final IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_RESUME_FEEDBACK);
        filter.addAction(Intent.ACTION_SCREEN_ON);
        registerReceiver(mSuspendedReceiver, filter, PERMISSION_TALKBACK, null);

        // Suspending infrastructure sets sIsTalkBackSuspended to true.
        suspendInfrastructure();

        final Intent resumeIntent = new Intent(ACTION_RESUME_FEEDBACK);
        final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, resumeIntent, 0);
        final Notification notification = new NotificationCompat.Builder(this)
                .setContentTitle(getString(R.string.notification_title_talkback_suspended))
                .setContentText(getString(R.string.notification_message_talkback_suspended))
                .setPriority(NotificationCompat.PRIORITY_MAX).setSmallIcon(R.drawable.ic_stat_info)
                .setContentIntent(pendingIntent).setOngoing(true).setWhen(0).build();

        startForeground(R.id.notification_suspended, notification);
    }

    /**
     * Resumes TalkBack and Explore by Touch.
     */
    private void resumeTalkBack() {
        if (isServiceActive()) {
            LogUtils.log(this, Log.ERROR, "Attempted to resume TalkBack when not suspended.");
            return;
        }

        unregisterReceiver(mSuspendedReceiver);
        resumeInfrastructure();
    }

    @Override
    protected boolean onKeyEvent(KeyEvent event) {
        // Don't intercept keys if TalkBack is suspended.
        if (!isServiceActive()) {
            return false;
        }

        for (KeyEventListener listener : mKeyEventListeners) {
            if (listener.onKeyEvent(event)) {
                return true;
            }
        }

        return false;
    }

    @Override
    protected boolean onGesture(int gestureId) {
        if (!isServiceActive()) {
            // Allow other services with touch exploration to handle gestures
            return false;
        }

        LogUtils.log(this, Log.VERBOSE, "Recognized gesture %s", gestureId);

        mFeedbackController.playAuditory(R.id.sounds_gesture_end);

        // Gestures always stop global speech on API 16. On API 17+ we silence
        // on TOUCH_INTERACTION_START.
        // TODO: Will this negatively affect something like Books?
        if (Build.VERSION.SDK_INT <= 16) {
            interruptAllFeedback();
        }

        mRadialMenuManager.dismissAll();

        boolean handled = true;
        boolean result = false;

        // Handle statically defined gestures.
        switch (gestureId) {
        case AccessibilityService.GESTURE_SWIPE_UP:
        case AccessibilityService.GESTURE_SWIPE_LEFT:
            result = mCursorController.previous(true /* shouldWrap */, true /* shouldScroll */);
            break;
        case AccessibilityService.GESTURE_SWIPE_DOWN:
        case AccessibilityService.GESTURE_SWIPE_RIGHT:
            result = mCursorController.next(true /* shouldWrap */, true /* shouldScroll */);
            break;
        case AccessibilityService.GESTURE_SWIPE_UP_AND_DOWN:
            // TODO(caseyburkhardt): Consider using existing custom gesture mechanism
            if (mVerticalGestureCycleGranularity) {
                result = mCursorController.previousGranularity();
            } else {
                result = mCursorController.jumpToTop();
            }
            break;
        case AccessibilityService.GESTURE_SWIPE_DOWN_AND_UP:
            if (mVerticalGestureCycleGranularity) {
                result = mCursorController.nextGranularity();
            } else {
                result = mCursorController.jumpToBottom();
            }
            break;
        case AccessibilityService.GESTURE_SWIPE_LEFT_AND_RIGHT:
            result = mCursorController.less();
            break;
        case AccessibilityService.GESTURE_SWIPE_RIGHT_AND_LEFT:
            result = mCursorController.more();
            break;
        default:
            handled = false;
        }

        if (handled) {
            if (!result) {
                mFeedbackController.playAuditory(R.id.sounds_complete);
            }
            return true;
        }

        // Handle user-definable gestures.
        switch (gestureId) {
        case AccessibilityService.GESTURE_SWIPE_DOWN_AND_LEFT:
            performCustomGesture(R.string.pref_shortcut_down_and_left_key,
                    R.string.pref_shortcut_down_and_left_default);
            return true;
        case AccessibilityService.GESTURE_SWIPE_DOWN_AND_RIGHT:
            performCustomGesture(R.string.pref_shortcut_down_and_right_key,
                    R.string.pref_shortcut_down_and_right_default);
            return true;
        case AccessibilityService.GESTURE_SWIPE_UP_AND_LEFT:
            performCustomGesture(R.string.pref_shortcut_up_and_left_key,
                    R.string.pref_shortcut_up_and_left_default);
            return true;
        case AccessibilityService.GESTURE_SWIPE_UP_AND_RIGHT:
            performCustomGesture(R.string.pref_shortcut_up_and_right_key,
                    R.string.pref_shortcut_up_and_right_default);
            return true;
        case AccessibilityService.GESTURE_SWIPE_RIGHT_AND_DOWN:
            performCustomGesture(R.string.pref_shortcut_right_and_down_key,
                    R.string.pref_shortcut_right_and_down_default);
            return true;
        case AccessibilityService.GESTURE_SWIPE_RIGHT_AND_UP:
            performCustomGesture(R.string.pref_shortcut_right_and_up_key,
                    R.string.pref_shortcut_right_and_up_default);
            return true;
        case AccessibilityService.GESTURE_SWIPE_LEFT_AND_DOWN:
            performCustomGesture(R.string.pref_shortcut_left_and_down_key,
                    R.string.pref_shortcut_left_and_down_default);
            return true;
        case AccessibilityService.GESTURE_SWIPE_LEFT_AND_UP:
            performCustomGesture(R.string.pref_shortcut_left_and_up_key,
                    R.string.pref_shortcut_left_and_up_default);
            return true;
        }

        // Never let the system handle gestures.
        return true;
    }

    public SpeechController getSpeechController() {
        if (mSpeechController == null) {
            throw new RuntimeException("mSpeechController has not been initialized");
        }

        return mSpeechController;
    }

    public MappedFeedbackController getFeedbackController() {
        if (mFeedbackController == null) {
            throw new RuntimeException("mFeedbackController has not been initialized");
        }

        return mFeedbackController;
    }

    public CursorController getCursorController() {
        if (mCursorController == null) {
            throw new RuntimeException("mCursorController has not been initialized");
        }

        return mCursorController;
    }

    public FullScreenReadController getFullScreenReadController() {
        if (mFullScreenReadController == null) {
            throw new RuntimeException("mFullScreenReadController has not been initialized");
        }

        return mFullScreenReadController;
    }

    public CustomLabelManager getLabelManager() {
        if (mLabelManager == null && Build.VERSION.SDK_INT >= CustomLabelManager.MIN_API_LEVEL) {
            throw new RuntimeException("mLabelManager has not been initialized");
        }

        return mLabelManager;
    }

    /**
     * Obtains the shared instance of TalkBack's {@link ShakeDetector}
     *
     * @return the shared {@link ShakeDetector} instance, or null if not initialized.
     */
    public ShakeDetector getShakeDetector() {
        return mShakeDetector;
    }

    /**
     * Performs a gesture associated with a preference key.
     *
     * @param keyResId The resource identifier for the preference key string.
     * @param defaultResId The resource identifier for the gesture to perform if
     *            the preference has not been set.
     */
    private boolean performCustomGesture(int keyResId, int defaultResId) {
        final String key = getString(keyResId);
        final String defaultValue = getString(defaultResId);
        final String value = mPrefs.getString(key, defaultValue);
        final ShortcutGestureAction gestureAction = ShortcutGestureAction.safeValueOf(value);

        return performGestureAction(gestureAction);
    }

    /**
     * Performs a {@link ShortcutGestureAction} as a result of a gesture.
     *
     * @param value The gesture action to perform.
     */
    private boolean performGestureAction(ShortcutGestureAction value) {
        // Broadcast a notification that a gesture action was performed.
        Intent intent = new Intent(GestureActionMonitor.ACTION_GESTURE_ACTION_PERFORMED);
        intent.putExtra(GestureActionMonitor.EXTRA_SHORTCUT_GESTURE_ACTION, value.toString());
        LocalBroadcastManager.getInstance(this).sendBroadcast(intent);

        switch (value) {
        case BACK:
            return AccessibilityServiceCompatUtils.performGlobalAction(this, GLOBAL_ACTION_BACK);
        case HOME:
            return AccessibilityServiceCompatUtils.performGlobalAction(this, GLOBAL_ACTION_HOME);
        case RECENTS:
            return AccessibilityServiceCompatUtils.performGlobalAction(this, GLOBAL_ACTION_RECENTS);
        case NOTIFICATIONS:
            return AccessibilityServiceCompatUtils.performGlobalAction(this, GLOBAL_ACTION_NOTIFICATIONS);
        case TALKBACK_BREAKOUT:
            return mRadialMenuManager.showRadialMenu(R.menu.global_context_menu);
        case LOCAL_BREAKOUT:
            return mRadialMenuManager.showRadialMenu(R.menu.local_context_menu);
        case READ_FROM_TOP:
            return mFullScreenReadController.startReading(true);
        case READ_FROM_CURRENT:
            return mFullScreenReadController.startReading(false);
        default:
            return false;
        }
    }

    @Override
    public void onInterrupt() {
        interruptAllFeedback();
    }

    public void interruptAllFeedback() {
        // Don't interrupt feedback if the tutorial is active.
        if (AccessibilityTutorialActivity.isTutorialActive()) {
            return;
        }

        // Instruct ChromeVox to stop speech and halt any automatic actions.
        if (mCursorController != null) {
            final AccessibilityNodeInfoCompat currentNode = mCursorController.getCursor();
            if (currentNode != null && WebInterfaceUtils.hasWebContent(currentNode)) {
                if (WebInterfaceUtils.isScriptInjectionEnabled(this)) {
                    WebInterfaceUtils.performSpecialAction(currentNode, WebInterfaceUtils.ACTION_STOP_SPEECH);
                }
            }
        }

        if (mFullScreenReadController != null) {
            mFullScreenReadController.interrupt();
        }

        if (mSpeechController != null) {
            mSpeechController.interrupt();
        }

        if (mFeedbackController != null) {
            mFeedbackController.interrupt();
        }
    }

    @Override
    protected void onServiceConnected() {
        LogUtils.log(this, Log.VERBOSE, "System bound to service.");
        resumeInfrastructure();

        // Handle any update actions.
        final TalkBackUpdateHelper helper = new TalkBackUpdateHelper(this);
        helper.showPendingNotifications();
        helper.checkUpdate();

        // Handle showing the tutorial if touch exploration is enabled.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            final ContentResolver resolver = getContentResolver();
            if (Settings.Secure.getInt(resolver, Settings.Secure.TOUCH_EXPLORATION_ENABLED, 0) == 1) {
                onTouchExplorationEnabled();
            } else {
                registerTouchSettingObserver(resolver);
            }
        }
    }

    /**
     * @return The current state of the TalkBack service, or
     *         {@code ServiceState.INACTIVE} if the service is not initialized.
     */
    public static ServiceState getServiceState() {
        final TalkBackService service = getInstance();
        if (service == null) {
            return ServiceState.INACTIVE;
        }

        return service.mServiceState;
    }

    /**
     * @return {@code true} if TalkBack is running and initialized,
     *         {@code false} otherwise.
     */
    public static boolean isServiceActive() {
        return (getServiceState() == ServiceState.ACTIVE);
    }

    /**
     * Returns the active TalkBack instance, or {@code null} if not available.
     */
    public static TalkBackService getInstance() {
        return sInstance;
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    private void registerTouchSettingObserver(ContentResolver resolver) {
        final Uri uri = Settings.Secure.getUriFor(Settings.Secure.TOUCH_EXPLORATION_ENABLED);
        resolver.registerContentObserver(uri, false, mTouchExploreObserver);
    }

    /**
     * Initializes the controllers, managers, and processors. This should only
     * be called once from {@link #onCreate}.
     */
    private void initializeInfrastructure() {
        // Initialize static instances that do not have dependencies.
        NodeSpeechRuleProcessor.initialize(this);
        ClassLoadingManager.getInstance().init(this);

        mAccessibilityManager = (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE);
        mPowerManager = (PowerManager) getSystemService(POWER_SERVICE);

        // Initialize the feedback controller and load the default theme.
        mFeedbackController = MappedFeedbackController.initialize(this);
        final MappedThemeLoader themeLoader = mFeedbackController.getThemeLoader();
        themeLoader.loadTheme(this, R.raw.feedbacktheme_default);

        mSpeechController = new SpeechController(this);

        if (Build.VERSION.SDK_INT >= CursorController.MIN_API_LEVEL) {
            mCursorController = new CursorController(this);
            mCursorController.setListener(mCursorControllerListener);
            mAccessibilityEventListeners.add(mCursorController);
        }

        if (Build.VERSION.SDK_INT >= FullScreenReadController.MIN_API_LEVEL) {
            mFullScreenReadController = new FullScreenReadController(this);
            mAccessibilityEventListeners.add(mFullScreenReadController);
        }

        if (Build.VERSION.SDK_INT >= ShakeDetector.MIN_API_LEVEL) {
            mShakeDetector = new ShakeDetector(this);
        }

        // Add event processors. These will process incoming AccessibilityEvents
        // in the order they are added.
        mProcessorEventQueue = new ProcessorEventQueue(this);
        mProcessorEventQueue.setTestingListener(mTestingListener);

        mAccessibilityEventListeners.add(mProcessorEventQueue);
        mAccessibilityEventListeners.add(new ProcessorScrollPosition(this));

        if (Build.VERSION.SDK_INT >= ProcessorLongHover.MIN_API_LEVEL) {
            mAccessibilityEventListeners.add(new ProcessorLongHover(this));
        }

        if (Build.VERSION.SDK_INT >= ProcessorFocusAndSingleTap.MIN_API_LEVEL) {
            mProcessorFollowFocus = new ProcessorFocusAndSingleTap(this);
            mAccessibilityEventListeners.add(mProcessorFollowFocus);
        }

        if (Build.VERSION.SDK_INT >= VolumeMonitor.MIN_API_LEVEL) {
            mVolumeMonitor = new VolumeMonitor(this);
        }

        if (Build.VERSION.SDK_INT >= PackageRemovalReceiver.MIN_API_LEVEL) {
            mPackageReceiver = new PackageRemovalReceiver();
        }

        if (Build.VERSION.SDK_INT >= ProcessorGestureVibrator.MIN_API_LEVEL) {
            mAccessibilityEventListeners.add(new ProcessorGestureVibrator());
        }

        if (Build.VERSION.SDK_INT >= ProcessorWebContent.MIN_API_LEVEL) {
            mAccessibilityEventListeners.add(new ProcessorWebContent(this));
        }

        if (Build.VERSION.SDK_INT >= ProcessorVolumeStream.MIN_API_LEVEL) {
            final ProcessorVolumeStream processorVolumeStream = new ProcessorVolumeStream(this);
            mAccessibilityEventListeners.add(processorVolumeStream);
            mKeyEventListeners.add(processorVolumeStream);
        }

        if (Build.VERSION.SDK_INT >= KeyComboManager.MIN_API_LEVEL) {
            final KeyComboManager keyComboManager = new KeyComboManager();
            keyComboManager.setListener(mKeyComboListener);
            keyComboManager.loadDefaultCombos();
            mKeyEventListeners.add(keyComboManager);
        }

        mOrientationMonitor = new OrientationMonitor(this);

        final PackageManager packageManager = getPackageManager();
        final boolean deviceIsPhone = packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY);

        // Only initialize telephony and call state for phones.
        if (deviceIsPhone) {
            mCallStateMonitor = new CallStateMonitor(this);
        }

        final boolean deviceHasTouchscreen = packageManager.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN);

        if (deviceIsPhone || deviceHasTouchscreen) {
            // Although this receiver includes code responding to phone-specific
            // intents, it should also be registered for touch screen devices
            // without telephony.
            mRingerModeAndScreenMonitor = new RingerModeAndScreenMonitor(this);
        }

        if (Build.VERSION.SDK_INT >= TextToSpeechManager.MIN_API_LEVEL) {
            mTextToSpeechManager = new TextToSpeechManager(this);
            mTextToSpeechManager.addListener(mTtsDiscoveryListener);
        }

        // Set up the radial menu manager and TalkBack-specific client.
        final TalkBackRadialMenuClient radialMenuClient = new TalkBackRadialMenuClient(this);
        mRadialMenuManager = new RadialMenuManager(this);
        mRadialMenuManager.setClient(radialMenuClient);

        if (Build.VERSION.SDK_INT >= CustomLabelManager.MIN_API_LEVEL) {
            mLabelManager = new CustomLabelManager(this);
            mAccessibilityEventListeners.add(mLabelManager);
        }
    }

    /**
     * Registers listeners, sets service info, loads preferences. This should be
     * called from {@link #onServiceConnected} and when TalkBack resumes from a
     * suspended state.
     */
    private void resumeInfrastructure() {
        if (isServiceActive()) {
            LogUtils.log(this, Log.ERROR, "Attempted to resume while not suspended");
            return;
        }

        setServiceState(ServiceState.ACTIVE);
        stopForeground(true);

        final AccessibilityServiceInfo info = new AccessibilityServiceInfo();
        info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
        info.feedbackType |= AccessibilityServiceInfo.FEEDBACK_SPOKEN;
        info.feedbackType |= AccessibilityServiceInfo.FEEDBACK_AUDIBLE;
        info.feedbackType |= AccessibilityServiceInfo.FEEDBACK_HAPTIC;
        info.flags |= AccessibilityServiceInfo.DEFAULT;
        info.flags |= AccessibilityServiceInfoCompatUtils.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY;
        info.flags |= AccessibilityServiceInfoCompatUtils.FLAG_REPORT_VIEW_IDS;
        info.flags |= AccessibilityServiceInfoCompatUtils.FLAG_REQUEST_FILTER_KEY_EVENTS;
        info.notificationTimeout = 0;

        // Ensure the initial touch exploration request mode is correct.
        if (SUPPORTS_TOUCH_PREF && SharedPreferencesUtils.getBooleanPref(mPrefs, getResources(),
                R.string.pref_explore_by_touch_key, R.bool.pref_explore_by_touch_default)) {
            info.flags |= AccessibilityServiceInfoCompatUtils.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
        }

        setServiceInfo(info);

        if (mCallStateMonitor != null) {
            registerReceiver(mCallStateMonitor, mCallStateMonitor.getFilter());
        }

        if (mRingerModeAndScreenMonitor != null) {
            registerReceiver(mRingerModeAndScreenMonitor, mRingerModeAndScreenMonitor.getFilter());
        }

        if (mTextToSpeechManager != null) {
            mTextToSpeechManager.startDiscovery();
        }

        if (mRadialMenuManager != null) {
            registerReceiver(mRadialMenuManager, mRadialMenuManager.getFilter());
        }

        if (mVolumeMonitor != null) {
            registerReceiver(mVolumeMonitor, mVolumeMonitor.getFilter());
        }

        if (mPackageReceiver != null) {
            registerReceiver(mPackageReceiver, mPackageReceiver.getFilter());
            if (mLabelManager != null) {
                mLabelManager.ensureDataConsistency();
            }
        }

        mPrefs.registerOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener);

        // Add the broadcast listener for gestures.
        final IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_PERFORM_GESTURE_ACTION);
        registerReceiver(mActiveReceiver, filter, PERMISSION_TALKBACK, null);

        // Enable the proxy activity for long-press search.
        final PackageManager packageManager = getPackageManager();
        final ComponentName shortcutProxy = new ComponentName(this, ShortcutProxyActivity.class);
        packageManager.setComponentEnabledSetting(shortcutProxy, PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                PackageManager.DONT_KILL_APP);

        reloadPreferences();
    }

    /**
     * Registers listeners, sets service info, loads preferences. This should be
     * called from {@link #onServiceConnected} and when TalkBack resumes from a
     * suspended state.
     */
    private void suspendInfrastructure() {
        if (!isServiceActive()) {
            LogUtils.log(this, Log.ERROR, "Attempted to suspend while already suspended");
            return;
        }

        interruptAllFeedback();
        setServiceState(ServiceState.SUSPENDED);

        setServiceInfo(new AccessibilityServiceInfo());

        mPrefs.unregisterOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener);

        unregisterReceiver(mActiveReceiver);

        if (mCallStateMonitor != null) {
            unregisterReceiver(mCallStateMonitor);
        }

        if (mRingerModeAndScreenMonitor != null) {
            unregisterReceiver(mRingerModeAndScreenMonitor);
        }

        if (mRadialMenuManager != null) {
            unregisterReceiver(mRadialMenuManager);
            mRadialMenuManager.clearCache();
        }

        if (mVolumeMonitor != null) {
            unregisterReceiver(mVolumeMonitor);
            mVolumeMonitor.releaseControl();
        }

        if (mPackageReceiver != null) {
            unregisterReceiver(mPackageReceiver);
        }

        if (mShakeDetector != null) {
            mShakeDetector.setEnabled(false);
        }

        if (SUPPORTS_TOUCH_PREF) {
            final ContentResolver resolver = getContentResolver();
            resolver.unregisterContentObserver(mTouchExploreObserver);
        }

        // Disable the proxy activity for long-press search.
        final PackageManager packageManager = getPackageManager();
        final ComponentName shortcutProxy = new ComponentName(this, ShortcutProxyActivity.class);
        packageManager.setComponentEnabledSetting(shortcutProxy, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                PackageManager.DONT_KILL_APP);

        // Remove any pending notifications that shouldn't persist.
        final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        nm.cancelAll();

        if (mTtsOverlay != null) {
            mTtsOverlay.hide();
        }
    }

    /**
     * Shuts down the infrastructure in case it has been initialized.
     */
    private void shutdownInfrastructure() {
        if (mCursorController != null) {
            mCursorController.shutdown();
        }

        if (mFullScreenReadController != null) {
            mFullScreenReadController.shutdown();
        }

        if (mTextToSpeechManager != null) {
            mTextToSpeechManager.shutdown();
        }

        if (mLabelManager != null) {
            mLabelManager.shutdown();
        }

        ClassLoadingManager.getInstance().shutdown();
        mFeedbackController.shutdown();
        mSpeechController.shutdown();
    }

    /**
     * Returns whether the device should drop this event. Caches notifications
     * if necessary.
     *
     * @param event The current event.
     * @return {@code true} if the event should be dropped.
     */
    private boolean shouldDropEvent(AccessibilityEvent event) {
        // Always drop null events.
        if (event == null) {
            return true;
        }

        // Always drop events if the service is suspended.
        if (!isServiceActive()) {
            return true;
        }

        // Always drop duplicate events (only applies to API < 14).
        if (AccessibilityEventUtils.eventEquals(mLastSpokenEvent, event)) {
            LogUtils.log(this, Log.VERBOSE, "Drop duplicate event");
            return true;
        }

        // If touch exploration is enabled, drop automatically generated events
        // that are sent immediately after a window state change... unless we
        // decide to keep the event.
        if (AccessibilityManagerCompat.isTouchExplorationEnabled(mAccessibilityManager)
                && ((event.getEventType() & AUTOMATIC_AFTER_STATE_CHANGE) != 0)
                && ((event.getEventTime() - mLastWindowStateChanged) < DELAY_AUTO_AFTER_STATE)
                && !shouldKeepAutomaticEvent(event)) {
            LogUtils.log(this, Log.VERBOSE, "Drop event after window state change");
            return true;
        }

        // Real notification events always have parcelable data.
        final boolean isNotification = (event.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED)
                && (event.getParcelableData() != null);

        final boolean isPhoneActive = (mCallStateMonitor != null)
                && (mCallStateMonitor.getCurrentCallState() != TelephonyManager.CALL_STATE_IDLE);
        final boolean shouldSpeakCallerId = (mSpeakCallerId && (mCallStateMonitor != null)
                && (mCallStateMonitor.getCurrentCallState() == TelephonyManager.CALL_STATE_RINGING));

        if (!mPowerManager.isScreenOn() && !shouldSpeakCallerId) {
            if (!mSpeakWhenScreenOff) {
                // If the user doesn't allow speech when the screen is
                // off, drop the event immediately.
                LogUtils.log(this, Log.VERBOSE, "Drop event due to screen state and user pref");
                return true;
            } else if (!isNotification) {
                // If the user allows speech when the screen is off, drop
                // all non-notification events.
                LogUtils.log(this, Log.VERBOSE, "Drop non-notification event due to screen state");
                return true;
            }
        }

        final boolean canInterruptRadialMenu = AccessibilityEventUtils.eventMatchesAnyType(event,
                MASK_EVENT_TYPES_INTERRUPT_RADIAL_MENU);
        final boolean silencedByRadialMenu = (mRadialMenuManager.isRadialMenuShowing() && !canInterruptRadialMenu);

        // Don't speak events that cannot interrupt the radial menu, if showing
        if (silencedByRadialMenu) {
            LogUtils.log(this, Log.VERBOSE, "Drop event due to radial menu state");
            return true;
        }

        // Don't speak notification events if the user is touch exploring or a phone call is active.
        if (isNotification && (mIsUserTouchExploring || isPhoneActive)) {
            LogUtils.log(this, Log.VERBOSE, "Drop notification due to touch or phone state");
            return true;
        }

        final int touchscreenState = getResources().getConfiguration().touchscreen;
        final boolean isTouchInteractionStateChange = AccessibilityEventUtils.eventMatchesAnyType(event,
                MASK_EVENT_TYPES_TOUCH_STATE_CHANGES);

        // Drop all events related to touch interaction state on devices that don't support touch.
        final int touchscreenConfig = getResources().getConfiguration().touchscreen;
        if ((touchscreenState == Configuration.TOUCHSCREEN_NOTOUCH) && isTouchInteractionStateChange) {
            return true;
        }

        return false;
    }

    /**
     * Helper method for {@link #shouldDropEvent} that handles events that
     * automatically occur immediately after a window state change.
     *
     * @param event The automatically generated event to consider retaining.
     * @return Whether to retain the event.
     */
    private boolean shouldKeepAutomaticEvent(AccessibilityEvent event) {
        final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event);

        // Don't drop focus events from EditTexts.
        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
            AccessibilityNodeInfoCompat node = null;

            try {
                node = record.getSource();
                if (AccessibilityNodeInfoUtils.nodeMatchesClassByType(this, node, android.widget.EditText.class)) {
                    return true;
                }
            } finally {
                AccessibilityNodeInfoUtils.recycleNodes(node);
            }
        }

        return false;
    }

    /**
     * Manages touch exploration state.
     *
     * @param event The current event.
     */
    private void maintainExplorationState(AccessibilityEvent event) {
        final int eventType = event.getEventType();

        if (eventType == AccessibilityEventCompat.TYPE_TOUCH_EXPLORATION_GESTURE_START) {
            mIsUserTouchExploring = true;
        } else if (eventType == AccessibilityEventCompat.TYPE_TOUCH_EXPLORATION_GESTURE_END) {
            mIsUserTouchExploring = false;
        } else if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
            mLastWindowStateChanged = SystemClock.uptimeMillis();
        }
    }

    /**
     * Caches the last spoken event. Used for duplicate detection on API < 14.
     *
     * @param event The current event.
     */
    private void cacheEvent(AccessibilityEvent event) {
        if (mLastSpokenEvent != null) {
            mLastSpokenEvent.recycle();
        }

        mLastSpokenEvent = AccessibilityEventCompatUtils.obtain(event);
    }

    /**
     * Passes the event to all registered {@link AccessibilityEventListener}s in the order
     * they were added.
     *
     * @param event The current event.
     */
    private void processEvent(AccessibilityEvent event) {
        for (AccessibilityEventListener eventProcessor : mAccessibilityEventListeners) {
            eventProcessor.onAccessibilityEvent(event);
        }
    }

    /**
     * Adds an event listener.
     *
     * @param listener The listener to add.
     */
    public void addEventListener(AccessibilityEventListener listener) {
        mAccessibilityEventListeners.add(listener);
    }

    /**
     * Posts a {@link Runnable} to removes an event listener. This is safe to
     * call from inside {@link AccessibilityEventListener#onAccessibilityEvent(AccessibilityEvent)}.
     *
     * @param listener The listener to remove.
     */
    /* protected */void postRemoveEventListener(final AccessibilityEventListener listener) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mAccessibilityEventListeners.remove(listener);
            }
        });
    }

    /**
     * Reloads service preferences.
     */
    private void reloadPreferences() {
        final Resources res = getResources();

        mSpeakWhenScreenOff = SharedPreferencesUtils.getBooleanPref(mPrefs, res, R.string.pref_screenoff_key,
                R.bool.pref_screenoff_default);
        mSpeakCallerId = SharedPreferencesUtils.getBooleanPref(mPrefs, res, R.string.pref_caller_id_key,
                R.bool.pref_caller_id_default);

        final String automaticResume = SharedPreferencesUtils.getStringPref(mPrefs, res,
                R.string.pref_resume_talkback_key, R.string.pref_resume_talkback_default);
        mAutomaticResume = AutomaticResumePreference.safeValueOf(automaticResume);

        final boolean silenceOnProximity = SharedPreferencesUtils.getBooleanPref(mPrefs, res,
                R.string.pref_proximity_key, R.bool.pref_proximity_default);
        mSpeechController.setSilenceOnProximity(silenceOnProximity);

        final int logLevel = (DEBUG ? Log.VERBOSE
                : SharedPreferencesUtils.getIntFromStringPref(mPrefs, res, R.string.pref_log_level_key,
                        R.string.pref_log_level_default));
        LogUtils.setLogLevel(logLevel);

        if (mProcessorFollowFocus != null) {
            final boolean useSingleTap = SharedPreferencesUtils.getBooleanPref(mPrefs, res,
                    R.string.pref_single_tap_key, R.bool.pref_single_tap_default);

            mProcessorFollowFocus.setSingleTapEnabled(useSingleTap);

            // Update the "X to select" long-hover hint.
            NodeHintRule.NodeHintHelper.updateActionResId(useSingleTap);
        }

        if (mShakeDetector != null) {
            final int shakeThreshold = SharedPreferencesUtils.getIntFromStringPref(mPrefs, res,
                    R.string.pref_shake_to_read_threshold_key, R.string.pref_shake_to_read_threshold_default);
            final boolean useShake = (shakeThreshold > 0) && ((mCallStateMonitor == null)
                    || (mCallStateMonitor.getCurrentCallState() == TelephonyManager.CALL_STATE_IDLE));

            mShakeDetector.setEnabled(useShake);
        }

        if (SUPPORTS_TOUCH_PREF) {
            final boolean touchExploration = SharedPreferencesUtils.getBooleanPref(mPrefs, res,
                    R.string.pref_explore_by_touch_key, R.bool.pref_explore_by_touch_default);
            requestTouchExploration(touchExploration);

            final String verticalGesturesPref = SharedPreferencesUtils.getStringPref(mPrefs, res,
                    R.string.pref_two_part_vertical_gestures_key, R.string.pref_two_part_vertical_gestures_default);
            mVerticalGestureCycleGranularity = verticalGesturesPref
                    .equals(getString(R.string.value_two_part_vertical_gestures_cycle));
        }

        if (SUPPORTS_WEB_SCRIPT_TOGGLE) {
            final boolean requestWebScripts = SharedPreferencesUtils.getBooleanPref(mPrefs, res,
                    R.string.pref_web_scripts_key, R.bool.pref_web_scripts_default);
            requestWebScripts(requestWebScripts);
        }
    }

    /**
     * Attempts to change the state of touch exploration.
     * <p>
     * Should only be called if {@link #SUPPORTS_TOUCH_PREF} is true.
     *
     * @param requestedState {@code true} to request exploration.
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void requestTouchExploration(boolean requestedState) {
        final AccessibilityServiceInfo info = getServiceInfo();
        if (info == null) {
            LogUtils.log(this, Log.ERROR,
                    "Failed to change touch exploration request state, service info was null");
            return;
        }

        final boolean currentState = ((info.flags
                & AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0);
        if (currentState == requestedState) {
            return;
        }

        if (requestedState) {
            info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
        } else {
            info.flags &= ~AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
        }

        setServiceInfo(info);
    }

    /**
     * Launches the touch exploration tutorial if necessary.
     */
    private void onTouchExplorationEnabled() {
        if (!mPrefs.getBoolean(PREF_FIRST_TIME_USER, true)) {
            return;
        }

        final Editor editor = mPrefs.edit();
        editor.putBoolean(PREF_FIRST_TIME_USER, false);
        editor.commit();

        final int touchscreenState = getResources().getConfiguration().touchscreen;

        if (Build.VERSION.SDK_INT >= AccessibilityTutorialActivity.MIN_API_LEVEL
                && (touchscreenState != Configuration.TOUCHSCREEN_NOTOUCH)) {
            final Intent tutorial = new Intent(this, AccessibilityTutorialActivity.class);
            tutorial.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            tutorial.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            startActivity(tutorial);
        }
    }

    /**
     * Attempts to change the state of web script injection.
     * <p>
     * Should only be called if {@link #SUPPORTS_WEB_SCRIPT_TOGGLE} is true.
     *
     * @param requestedState {@code true} to request script injection,
     *            {@code false} otherwise.
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private void requestWebScripts(boolean requestedState) {
        final AccessibilityServiceInfo info = getServiceInfo();
        if (info == null) {
            LogUtils.log(this, Log.ERROR,
                    "Failed to change web script injection request state, service info was null");
            return;
        }

        final boolean currentState = ((info.flags
                & AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY) != 0);
        if (currentState == requestedState) {
            return;
        }

        if (requestedState) {
            info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY;
        } else {
            info.flags &= ~AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY;
        }

        setServiceInfo(info);
    }

    /**
     * Handler used for receiving URI change updates.
     */
    private final Handler mHandler = new Handler();

    /**
     * Handles granularity change events by speaking the current mode.
     */
    private final CursorControllerListener mCursorControllerListener = new CursorControllerListener() {
        @Override
        public void onGranularityChanged(CursorGranularity granularity, boolean fromUser) {
            // Only announce the granularity change if it was requested
            // by the user.
            if (fromUser) {
                final int resId = granularity.resId;
                final String name = getString(resId);

                mSpeechController.speak(name, SpeechController.QUEUE_MODE_INTERRUPT, 0, null);
            }

            // Broadcast a notification that the granularity was changed.
            final Intent intent = new Intent(GranularityMonitor.ACTION_GRANULARITY_CHANGED);
            intent.putExtra(GranularityMonitor.EXTRA_GRANULARITY_KEY, granularity.keyId);
            LocalBroadcastManager.getInstance(TalkBackService.getInstance()).sendBroadcast(intent);
        }

        @Override
        public void onActionPerformed(int action) {
            // The follow focus processor needs to know if we perform a
            // scroll action.
            if (mProcessorFollowFocus != null) {
                mProcessorFollowFocus.onActionPerformed(action);
            }
        }
    };

    private final KeyComboListener mKeyComboListener = new KeyComboListener() {
        @Override
        public boolean onComboPerformed(int id) {
            if (id == R.id.key_combo_suspend_resume) {
                if (isServiceActive()) {
                    confirmSuspendTalkBack();
                } else {
                    resumeTalkBack();
                }
                return true;
            }

            return false;
        }
    };

    /**
     * Reloads preferences whenever their values change.
     */
    private final OnSharedPreferenceChangeListener mSharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
        @Override
        public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
            LogUtils.log(this, Log.DEBUG, "A shared preference changed: %s", key);
            reloadPreferences();
        }
    };

    /**
     * Shows the accessibility tutorial after touch exploration is turned on for
     * the first time.
     */
    private final ContentObserver mTouchExploreObserver = new ContentObserver(mHandler) {
        @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
        @Override
        public void onChange(boolean selfChange, Uri uri) {
            final ContentResolver resolver = getContentResolver();
            final boolean touchExplorationEnabled = (Settings.Secure.getInt(resolver,
                    Settings.Secure.TOUCH_EXPLORATION_ENABLED, 0) == 1);
            if (!touchExplorationEnabled) {
                return;
            }

            resolver.unregisterContentObserver(this);

            onTouchExplorationEnabled();
        }
    };

    /**
     * Broadcast receiver for actions that happen while the service is active.
     */
    private final BroadcastReceiver mActiveReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();

            if (ACTION_PERFORM_GESTURE_ACTION.equals(action)) {
                final String gestureActionString = intent.getStringExtra(EXTRA_GESTURE_ACTION);
                final ShortcutGestureAction gestureAction = ShortcutGestureAction.safeValueOf(gestureActionString);
                performGestureAction(gestureAction);
            }
        }
    };

    /**
     * Broadcast receiver for actions that happen while the service is inactive.
     */
    private final BroadcastReceiver mSuspendedReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();

            if (ACTION_RESUME_FEEDBACK.equals(action)) {
                resumeTalkBack();
            } else if (Intent.ACTION_SCREEN_ON.equals(action)) {
                switch (mAutomaticResume) {
                case KEYGUARD:
                    final KeyguardManager keyguard = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
                    if (keyguard.inKeyguardRestrictedInputMode()) {
                        resumeTalkBack();
                    }
                    break;
                case SCREEN_ON:
                    resumeTalkBack();
                    break;
                default:
                    // Do nothing.
                }
            }
        }
    };

    private final TtsDiscoveryListener mTtsDiscoveryListener = new TtsDiscoveryListener() {
        @Override
        public void onTtsDiscovery(TtsEngineInfo engine, List<String> availableLanguages) {
            LogUtils.log(this, Log.INFO, "Discovered engine %s with %d languages", engine.name,
                    availableLanguages.size());
        }

        @Override
        public void onTtsRemoval(TtsEngineInfo engine) {
            LogUtils.log(this, Log.INFO, "Removed engine %s", engine.name);
        }
    };

    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        try {
            if (mRadialMenuManager != null && mRadialMenuManager.isRadialMenuShowing()) {
                mRadialMenuManager.dismissAll();
            }

            if (mSuspendDialog != null) {
                mSuspendDialog.dismiss();
            }
        } catch (Exception e) {
            // Do nothing.
        } finally {
            if (mSystemUeh != null) {
                mSystemUeh.uncaughtException(thread, ex);
            }
        }
    }

    /**
     * Interface for receiving callbacks when the state of the TalkBack service
     * changes.
     * <p>
     * Implementing controllers should note that this may be invoked even after
     * the controller was explicitly shut down by TalkBack.
     * <p>
     * {@link ServiceState}
     * {@link TalkBackService#addServiceStateListener(ServiceStateListener)}
     * {@link TalkBackService#removeServiceStateListener(ServiceStateListener)}
     */
    public interface ServiceStateListener {
        public void onServiceStateChanged(ServiceState newState);
    }

    /**
     * Interface for key event listeners.
     */
    public interface KeyEventListener {
        public boolean onKeyEvent(KeyEvent event);
    }
}