/* * 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.annotation.SuppressLint; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.view.accessibility.AccessibilityNodeInfo; 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.os.Build; import android.os.Handler; import android.support.v4.app.NotificationCompat; import android.support.v4.os.BuildCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 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.AccessibilityWindowInfo; import android.widget.CheckBox; import android.widget.ScrollView; import android.widget.TextView; import com.android.switchaccess.SwitchAccessService; import com.android.talkback.Analytics; import com.android.talkback.BatteryMonitor; import com.android.talkback.CallStateMonitor; import com.android.talkback.controller.TelevisionNavigationController; import com.android.talkback.InputModeManager; import com.android.talkback.KeyComboManager; import com.android.talkback.KeyboardLockMonitor; import com.android.talkback.KeyboardSearchManager; import com.android.talkback.OrientationMonitor; import com.android.talkback.R; import com.android.talkback.RingerModeAndScreenMonitor; import com.android.talkback.SavedNode; import com.android.talkback.ShakeDetector; import com.android.talkback.ShortcutProxyActivity; import com.android.talkback.SideTapManager; import com.android.talkback.SpeechController; import com.android.talkback.TalkBackAnalytics; import com.android.talkback.TalkBackKeyboardShortcutPreferencesActivity; import com.android.talkback.TalkBackPreferencesActivity; import com.android.talkback.TalkBackUpdateHelper; import com.android.talkback.VolumeMonitor; import com.android.talkback.contextmenu.ListMenuManager; import com.android.talkback.contextmenu.MenuManager; import com.android.talkback.contextmenu.MenuManagerWrapper; import com.android.talkback.contextmenu.RadialMenuManager; import com.android.talkback.controller.CursorController; import com.android.talkback.controller.CursorControllerApp; import com.android.talkback.controller.DimScreenController; import com.android.talkback.controller.DimScreenControllerApp; import com.android.talkback.controller.FeedbackController; import com.android.talkback.controller.FeedbackControllerApp; import com.android.talkback.controller.FullScreenReadController; import com.android.talkback.controller.FullScreenReadControllerApp; import com.android.talkback.controller.GestureController; import com.android.talkback.controller.GestureControllerApp; import com.android.talkback.eventprocessor.AccessibilityEventProcessor; import com.android.talkback.eventprocessor.AccessibilityEventProcessor.TalkBackListener; import com.android.talkback.eventprocessor.ProcessorEventQueue; import com.android.talkback.eventprocessor.ProcessorFocusAndSingleTap; import com.android.talkback.eventprocessor.ProcessorGestureVibrator; import com.android.talkback.eventprocessor.ProcessorAccessibilityHints; import com.android.talkback.eventprocessor.ProcessorPhoneticLetters; import com.android.talkback.eventprocessor.ProcessorScreen; import com.android.talkback.eventprocessor.ProcessorScrollPosition; import com.android.talkback.eventprocessor.ProcessorVolumeStream; import com.android.talkback.eventprocessor.ProcessorWebContent; import com.android.talkback.controller.TextCursorController; import com.android.talkback.controller.TextCursorControllerApp; import com.android.talkback.speechrules.NodeHintRule; import com.android.talkback.speechrules.NodeSpeechRuleProcessor; import com.android.talkback.tutorial.AccessibilityTutorialActivity; import com.android.utils.AccessibilityEventListener; import com.android.utils.AccessibilityNodeInfoUtils; import com.android.utils.LogUtils; import com.android.utils.PerformActionUtils; import com.android.utils.SharedPreferencesUtils; import com.android.utils.WebInterfaceUtils; import com.android.utils.labeling.CustomLabelManager; import com.android.utils.labeling.PackageRemovalReceiver; import java.lang.Thread.UncaughtExceptionHandler; import java.util.LinkedList; import java.util.List; /** * An {@link AccessibilityService} that provides spoken, haptic, and audible * feedback. */ public class TalkBackService extends AccessibilityService implements Thread.UncaughtExceptionHandler { /** Whether the user has seen the TalkBack tutorial. */ public static final String PREF_FIRST_TIME_USER = "first_time_user"; /** Permission required to perform gestures. */ public static final String PERMISSION_TALKBACK = "com.google.android.marvin.feedback.permission.TALKBACK"; /** The intent action used to perform a custom gesture action. */ public static final String ACTION_PERFORM_GESTURE_ACTION = "performCustomGestureAction"; /** * The gesture action to pass with {@link #ACTION_PERFORM_GESTURE_ACTION} as a string extra. * Expected to be the name of the shortcut pref value, like R.strings.shortcut_value_previous */ public static final String EXTRA_GESTURE_ACTION = "gestureAction"; /** * Intent to open text-to-speech settings. */ public static final String INTENT_TTS_SETTINGS = "com.android.settings.TTS_SETTINGS"; /** Whether the current SDK supports service-managed web scripts. */ private static final boolean SUPPORTS_WEB_SCRIPT_TOGGLE = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2); /** 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. */ /** The state of the service before the system has bound to it or after it is destroyed. */ public static final int SERVICE_STATE_INACTIVE = 0; /** The state of the service when it initialized and active. */ public static final int SERVICE_STATE_ACTIVE = 1; /** The state of the service when it has been suspended by the user. */ public static final int SERVICE_STATE_SUSPENDED = 2; private final static String LOGTAG = "TalkBackService"; private final static String ARC_DEVICE_PATTERN = ".+_cheets"; /** * 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<>(); /** The current state of the service. */ private int mServiceState; /** Components to receive callbacks on changes in the service's state. */ private List<ServiceStateListener> mServiceStateListeners = new LinkedList<>(); /** Controller for cursor movement. */ private CursorControllerApp mCursorController; /** Controller for speech feedback. */ private SpeechController mSpeechController; /** Controller for audio and haptic feedback. */ private FeedbackController mFeedbackController; /** Controller for reading the entire hierarchy. */ private FullScreenReadControllerApp mFullScreenReadController; /** Controller for monitoring current and previous cursor position in editable node */ private TextCursorController mTextCursorController; /** Controller for manage keyboard commands */ private KeyComboManager mKeyComboManager; /** Listener for device shake events. */ private ShakeDetector mShakeDetector; /** Manager for side tap events */ private SideTapManager mSideTapManager; /** Manager for showing radial menus. */ private MenuManagerWrapper mMenuManager; /** Manager for handling custom labels. */ private CustomLabelManager mLabelManager; /** Manager for keyboard search. */ private KeyboardSearchManager mKeyboardSearchManager; /** Processor for moving access focus. Used in Jelly Bean and above. */ private ProcessorFocusAndSingleTap mProcessorFollowFocus; /** 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 android.content.BroadcastReceiver} for tracking battery status changes. */ private BatteryMonitor mBatteryMonitor; /** Manages screen dimming */ private DimScreenController mDimScreenController; /** Whether the current device is a television (Android TV). */ private boolean mIsDeviceTelevision; /** The television controller; non-null if the device is a television (Android TV). */ private TelevisionNavigationController mTelevisionNavigationController; /** * {@link BroadcastReceiver} for tracking package removals for custom label * data consistency. */ private PackageRemovalReceiver mPackageReceiver; /** The analytics instance, used for sending data to Google Analytics. */ private Analytics mAnalytics; private GestureController mGestureController; /** 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; /** The node that was focused during the last call to {@link #saveFocusedNode()} */ private SavedNode mSavedNode = new SavedNode(); /** The system feature if the device supports touch screen */ private boolean mSupportsTouchScreen = true; /** Preference specifying when TalkBack should automatically resume. */ private String mAutomaticResume; /** * Whether the current root node is dirty or not. **/ private boolean mIsRootNodeDirty = true; /** * Keep Track of current root node. */ private AccessibilityNodeInfo mRootNode; private AccessibilityEventProcessor mAccessibilityEventProcessor; /** Keeps track of whether we need to run the locked-boot-completed callback when connected. */ private boolean mLockedBootCompletedPending; private final InputModeManager mInputModeManager = new InputModeManager(); private ProcessorScreen mProcessorScreen; @Override public void onCreate() { super.onCreate(); sInstance = this; setServiceState(SERVICE_STATE_INACTIVE); SharedPreferencesUtils.migrateSharedPreferences(this); mPrefs = SharedPreferencesUtils.getSharedPreferences(this); mSystemUeh = Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler(this); mAccessibilityEventProcessor = new AccessibilityEventProcessor(this); initializeInfrastructure(); } @Override public void onDestroy() { super.onDestroy(); if (isServiceActive()) { suspendInfrastructure(); } sInstance = null; // Shutdown and unregister all components. shutdownInfrastructure(); setServiceState(SERVICE_STATE_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. mMenuManager.clearCache(); } @Override public void onAccessibilityEvent(AccessibilityEvent event) { mAccessibilityEventProcessor.onAccessibilityEvent(event); } public boolean supportsTouchScreen() { return mSupportsTouchScreen; } @Override public AccessibilityNodeInfo getRootInActiveWindow() { if(mIsRootNodeDirty || mRootNode == null) { mRootNode = super.getRootInActiveWindow(); mIsRootNodeDirty = false; } return mRootNode == null ? null : AccessibilityNodeInfo.obtain(mRootNode); } public void setRootDirty(boolean rootIsDirty) { mIsRootNodeDirty = rootIsDirty; } private void setServiceState(int newState) { if (mServiceState == newState) { return; } mServiceState = newState; for (ServiceStateListener listener : mServiceStateListeners) { listener.onServiceStateChanged(newState); } } @Override public AccessibilityNodeInfo findFocus(int focus) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { return super.findFocus(focus); } else { AccessibilityNodeInfo root = getRootInActiveWindow(); return root == null ? null : root.findFocus(focus); } } public void addServiceStateListener(ServiceStateListener listener) { if (listener != null) { mServiceStateListeners.add(listener); } } public void removeServiceStateListener(ServiceStateListener listener) { if (listener != null) { mServiceStateListeners.remove(listener); } } /** * 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); @SuppressLint("InflateParams") 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) { if (which == DialogInterface.BUTTON_POSITIVE) { if (!confirmCheckBox.isChecked()) { SharedPreferencesUtils.putBooleanPref(mPrefs, getResources(), R.string.pref_show_suspension_confirmation_dialog, false); } suspendTalkBack(); } } }; final OnDismissListener onDismissListener = new OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { mSuspendDialog = null; } }; if (mAutomaticResume.equals(getString(R.string.resume_screen_keyguard))) { message.setText(getString(R.string.message_resume_keyguard)); } else if (mAutomaticResume.equals(getString(R.string.resume_screen_manual))) { message.setText(getString(R.string.message_resume_manual)); } else { // screen on is the default value message.setText(getString(R.string.message_resume_screen_on)); } 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(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { mSuspendDialog.getWindow().setType( WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY); } else { mSuspendDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR); } mSuspendDialog.setOnDismissListener(onDismissListener); mSuspendDialog.show(); } /** * Suspends TalkBack and Explore by Touch. */ public void suspendTalkBack() { if (!isServiceActive()) { if (LogUtils.LOG_LEVEL <= Log.ERROR) { Log.e(LOGTAG, "Attempted to suspend TalkBack while already suspended."); } return; } SharedPreferencesUtils.storeBooleanAsync(mPrefs, getString(R.string.pref_suspended), true); mFeedbackController.playAuditory(R.raw.paused_feedback); if (mSupportsTouchScreen) { requestTouchExploration(false); } if (mCursorController != null) { try { mCursorController.clearCursor(); } catch (SecurityException e) { if (LogUtils.LOG_LEVEL >= Log.ERROR) { Log.e(LOGTAG, "Unable to clear cursor"); } } } mInputModeManager.clear(); AccessibilityTutorialActivity.stopActiveTutorial(); 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); mSpeechController.speak(getString(R.string.talkback_suspended), SpeechController.QUEUE_MODE_UNINTERRUPTIBLE, 0, null); } public void disableTalkBack() { if (isServiceActive()) { if (mSupportsTouchScreen) { requestTouchExploration(false); } AccessibilityTutorialActivity.stopActiveTutorial(); suspendInfrastructure(); // OK to clear completely since TalkBack is about to go away. mServiceStateListeners.clear(); } TalkBackService.getInstance().getSpeechController().speak( getString(R.string.talkback_disabled), null, null, SpeechController.QUEUE_MODE_UNINTERRUPTIBLE, 0, SpeechController.UTTERANCE_GROUP_DEFAULT, null, null, mDisableTalkBackHandler); } /** * Resumes TalkBack and Explore by Touch. */ public void resumeTalkBack() { if (isServiceActive()) { if (LogUtils.LOG_LEVEL <= Log.ERROR) { Log.e(LOGTAG, "Attempted to resume TalkBack when not suspended."); } return; } SharedPreferencesUtils.storeBooleanAsync(mPrefs, getString(R.string.pref_suspended), false); unregisterReceiver(mSuspendedReceiver); resumeInfrastructure(); mSpeechController.speak(getString(R.string.talkback_resumed), SpeechController.QUEUE_MODE_UNINTERRUPTIBLE, 0, null); } /** * Intended to mimic the behavior of onKeyEvent if this were the only service running. * It will be called from onKeyEvent, both from this service and from others in this apk * (TalkBack). This method must not block, since it will block onKeyEvent as well. * @param keyEvent A key event * @return {@code true} if the event is handled, {@code false} otherwise. */ public boolean onKeyEventShared(KeyEvent keyEvent) { for (KeyEventListener listener : mKeyEventListeners) { if (!isServiceActive() && !listener.processWhenServiceSuspended()) { continue; } if (listener.onKeyEvent(keyEvent)) { return true; } } return false; } @Override protected boolean onKeyEvent(KeyEvent keyEvent) { boolean keyHandled = onKeyEventShared(keyEvent); if (!BuildCompat.isAtLeastN()) { SwitchAccessService switchAccessService = SwitchAccessService.getInstance(); if (switchAccessService != null) { keyHandled = switchAccessService.onKeyEventShared(keyEvent) || keyHandled; } } return keyHandled; } @Override protected boolean onGesture(int gestureId) { if (!isServiceActive()) return false; if (LogUtils.LOG_LEVEL <= Log.VERBOSE) { Log.v(LOGTAG, String.format("Recognized gesture %s", gestureId)); } if (mKeyboardSearchManager != null && mKeyboardSearchManager.onGesture()) return true; mAnalytics.onGesture(gestureId); mFeedbackController.playAuditory(R.raw.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 == Build.VERSION_CODES.JELLY_BEAN) { interruptAllFeedback(); } mMenuManager.onGesture(gestureId); mGestureController.onGesture(gestureId); return true; } public GestureController getGestureController() { if (mGestureController == null) { throw new RuntimeException("mGestureController has not been initialized"); } return mGestureController; } public SpeechController getSpeechController() { if (mSpeechController == null) { throw new RuntimeException("mSpeechController has not been initialized"); } return mSpeechController; } public FeedbackController 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 TextCursorController getTextCursorController() { if (mTextCursorController == null) { throw new RuntimeException("mTextCursorController has not been initialized"); } return mTextCursorController; } public KeyComboManager getKeyComboManager() { return mKeyComboManager; } public FullScreenReadController getFullScreenReadController() { if (mFullScreenReadController == null) { throw new RuntimeException("mFullScreenReadController has not been initialized"); } return mFullScreenReadController; } public DimScreenController getDimScreenController() { if (mDimScreenController == null) { throw new RuntimeException("mDimScreenController has not been initialized"); } return mDimScreenController; } public CustomLabelManager getLabelManager() { if (mLabelManager == null && Build.VERSION.SDK_INT >= CustomLabelManager.MIN_API_LEVEL) { throw new RuntimeException("mLabelManager has not been initialized"); } return mLabelManager; } public Analytics getAnalytics() { if (mAnalytics == null) { throw new RuntimeException("mAnalytics has not been initialized"); } return mAnalytics; } public RingerModeAndScreenMonitor getRingerModeAndScreenMonitor() { if (mRingerModeAndScreenMonitor == null) { throw new RuntimeException("mRingerModeAndScreenMonitor has not been initialized"); } return mRingerModeAndScreenMonitor; } public CallStateMonitor getCallStateMonitor() { if (mCallStateMonitor == null) { throw new RuntimeException("mCallStateMonitor has not been initialized"); } return mCallStateMonitor; } /** * 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; } /** * Obtains the shared instance of TalkBack's {@link TelevisionNavigationController} if the * current device is a television. Otherwise returns {@code null}. */ public TelevisionNavigationController getTelevisionNavigationController() { return mTelevisionNavigationController; } /** Save the currently focused node so that focus can be returned to it later. */ public void saveFocusedNode() { mSavedNode.recycle(); AccessibilityNodeInfoCompat node = mCursorController.getCursorOrInputCursor(); if (node != null) { mSavedNode.saveNodeState(node, mCursorController.getGranularityAt(node), this); node.recycle(); } } public boolean hasSavedNode() { return mSavedNode.getNode() != null; } /** * Reset the accessibility focus to the node that was focused during the last call to * {@link #saveFocusedNode()} */ public void resetFocusedNode() { resetFocusedNode(0); } public void resetFocusedNode(long delay) { final Handler handler = new Handler(); handler.postDelayed(new Runnable() { @SuppressLint("InlinedApi") @Override public void run() { AccessibilityNodeInfoCompat node = mSavedNode.getNode(); if (node == null) { return; } AccessibilityNodeInfoCompat refreshed = AccessibilityNodeInfoUtils.refreshNode(node); if (refreshed != null) { if (!refreshed.isAccessibilityFocused()) { // Restore accessibility focus. mCursorController.setGranularity(mSavedNode.getGranularity(), refreshed, false); mSavedNode.restoreTextAndSelection(); PerformActionUtils.performAction(refreshed, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); } else if (BuildCompat.isAtLeastN() && mSavedNode.getAnchor() != null && refreshed.getWindow() == null) { // The node is anchored, but its window has disappeared. // We need to wait for the node's window to appear, and then do a fuzzy-find // to place accessibility focus on the node's replacement. mCursorController.setGranularity(mSavedNode.getGranularity(), refreshed, false); mSavedNode.restoreTextAndSelection(); resetFocusedNodeInAnchoredWindow( mSavedNode.getNode(), mSavedNode.getAnchor(), 250 /* ms */); } refreshed.recycle(); } mSavedNode.recycle(); } }, delay); } /** * Resets the accessibility focus to an item inside an anchored window. * We must delay slightly in order for the anchored window to reappear in the windows list * before attempting to place the accessibility focus. Furthermore, we cannot find the exact * previously-focused item again; we must do a fuzzy search for it instead. */ private void resetFocusedNodeInAnchoredWindow(AccessibilityNodeInfoCompat node, AccessibilityNodeInfoCompat anchor, long delay) { if (node == null || anchor == null) { return; } final AccessibilityNodeInfoCompat obtainedNode = AccessibilityNodeInfoCompat.obtain(node); final AccessibilityNodeInfoCompat obtainedAnchor = AccessibilityNodeInfoCompat.obtain(anchor); final Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { com.android.utils.WindowManager windowManager = new com.android.utils.WindowManager(isScreenLayoutRTL()); windowManager.setWindows(getWindows()); AccessibilityWindowInfo anchoredWindow = windowManager.getAnchoredWindow(obtainedAnchor); AccessibilityNodeInfoCompat refreshed = AccessibilityNodeInfoUtils.refreshNodeFuzzy(obtainedNode, anchoredWindow); if (refreshed != null) { PerformActionUtils.performAction(refreshed, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); refreshed.recycle(); } obtainedNode.recycle(); obtainedAnchor.recycle(); } }, delay); } private void showGlobalContextMenu() { if (mSupportsTouchScreen) { mMenuManager.showMenu(R.menu.global_context_menu); } } private void showLocalContextMenu() { if (mSupportsTouchScreen) { mMenuManager.showMenu(R.menu.local_context_menu); } } private void openManageKeyboardShortcuts() { Intent intent = new Intent(this, TalkBackKeyboardShortcutPreferencesActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } private void openTalkBackSettings() { Intent intent = new Intent(this, TalkBackPreferencesActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } @Override public void onInterrupt() { if (mProcessorScreen != null && isInArc()) { // In Arc, we consider that focus goes out from Arc when onInterrupt is called. mProcessorScreen.clearScreenState(); } interruptAllFeedback(); } public void interruptAllFeedback() { interruptAllFeedback(false); } public void interruptAllFeedback(boolean isSuspending) { // Don't interrupt feedback if the tutorial is active unless it's triggered by // suspendInfrastructure() if (!isSuspending && AccessibilityTutorialActivity.isTutorialActive()) { return; } // If this method is not called from suspendInfrastructure(), instruct ChromeVox to stop // speech and halt any automatic actions. if (!isSuspending && mCursorController != null) { final AccessibilityNodeInfoCompat currentNode = mCursorController.getCursor(); if (currentNode != null && WebInterfaceUtils.hasLegacyWebContent(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() { if (LogUtils.LOG_LEVEL <= Log.VERBOSE) { Log.v(LOGTAG, "System bound to service."); } resumeInfrastructure(); // Handle any update actions. final TalkBackUpdateHelper helper = new TalkBackUpdateHelper(this); helper.showPendingNotifications(); helper.checkUpdate(); final ContentResolver resolver = getContentResolver(); if (!TalkBackPreferencesActivity.TalkBackPreferenceFragment.isTouchExplorationEnabled( resolver) || !showTutorial()) { startCallStateMonitor(); } if (mPrefs.getBoolean(getString(R.string.pref_suspended), false)) { suspendTalkBack(); } else { if (!isInArc()) { mSpeechController.speak(getString(R.string.talkback_on), SpeechController.QUEUE_MODE_UNINTERRUPTIBLE, 0, null); } } // If the locked-boot-completed intent was fired before onServiceConnected, we queued it, // so now we need to run it. if (mLockedBootCompletedPending) { onLockedBootCompletedInternal(); mLockedBootCompletedPending = false; } } /** * @return The current state of the TalkBack service, or * {@code INACTIVE} if the service is not initialized. */ public static int getServiceState() { final TalkBackService service = getInstance(); if (service == null) { return SERVICE_STATE_INACTIVE; } return service.mServiceState; } /** * Whether the current TalkBackService instance is running and initialized. * This method is useful for testing because it can be overridden by mocks. */ public boolean isInstanceActive() { return mServiceState == SERVICE_STATE_ACTIVE; } /** * @return {@code true} if TalkBack is running and initialized, * {@code false} otherwise. */ public static boolean isServiceActive() { return (getServiceState() == SERVICE_STATE_ACTIVE); } /** * Returns the active TalkBack instance, or {@code null} if not available. */ public static TalkBackService getInstance() { return sInstance; } /** * 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); // Assume that a device cannot become a TV or stop being a TV while running. mIsDeviceTelevision = TelevisionNavigationController.isContextTelevision(this); final PackageManager packageManager = getPackageManager(); final boolean deviceIsPhone = packageManager.hasSystemFeature( PackageManager.FEATURE_TELEPHONY); //TODO we still need it keep true for TV until TouchExplore and Accessibility focus is not //unpaired //mSupportsTouchScreen = packageManager.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); // Only initialize telephony and call state for phones. if (deviceIsPhone) { mCallStateMonitor = new CallStateMonitor(this); mAccessibilityEventProcessor.setCallStateMonitor(mCallStateMonitor); } mCursorController = new CursorControllerApp(this); addEventListener(mCursorController); mFeedbackController = new FeedbackControllerApp(this); mFullScreenReadController = new FullScreenReadControllerApp(mFeedbackController, mCursorController, this); addEventListener(mFullScreenReadController); mSpeechController = new SpeechController(this, mFeedbackController); mKeyEventListeners.add(mSpeechController); mShakeDetector = new ShakeDetector(mFullScreenReadController, this); mMenuManager = new MenuManagerWrapper(); updateMenuManagerFromPreferences(); // Sets mMenuManager mRingerModeAndScreenMonitor = new RingerModeAndScreenMonitor(mFeedbackController, mMenuManager, mShakeDetector, mSpeechController, this); mAccessibilityEventProcessor.setRingerModeAndScreenMonitor(mRingerModeAndScreenMonitor); mGestureController = new GestureControllerApp(this, mCursorController, mFeedbackController, mFullScreenReadController, mMenuManager); mSideTapManager = new SideTapManager(this, mGestureController); addEventListener(mSideTapManager); mFeedbackController.addHapticFeedbackListener(mSideTapManager); mTextCursorController = new TextCursorControllerApp(); addEventListener(mTextCursorController); // Add event processors. These will process incoming AccessibilityEvents // in the order they are added. ProcessorEventQueue processorEventQueue = new ProcessorEventQueue(mSpeechController, this); processorEventQueue.setTestingListener(mAccessibilityEventProcessor.getTestingListener()); mAccessibilityEventProcessor.setProcessorEventQueue(processorEventQueue); addEventListener(processorEventQueue); addEventListener(new ProcessorScrollPosition(mFullScreenReadController, mSpeechController, mCursorController, this)); addEventListener(new ProcessorAccessibilityHints(this, mSpeechController)); addEventListener(new ProcessorPhoneticLetters(this, mSpeechController)); mProcessorScreen = new ProcessorScreen(this); addEventListener(mProcessorScreen); mProcessorFollowFocus = new ProcessorFocusAndSingleTap( mCursorController, mFeedbackController, mSpeechController, this); mAccessibilityEventProcessor.setProcessorFocusAndSingleTap(mProcessorFollowFocus); addEventListener(mProcessorFollowFocus); if (mCursorController != null) { mCursorController.addScrollListener(mProcessorFollowFocus); } mVolumeMonitor = new VolumeMonitor(mSpeechController, this); mBatteryMonitor = new BatteryMonitor(this, mSpeechController, (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE)); if (Build.VERSION.SDK_INT >= PackageRemovalReceiver.MIN_API_LEVEL) { // TODO: Move this into the custom label manager code mPackageReceiver = new PackageRemovalReceiver(); } if (Build.VERSION.SDK_INT >= ProcessorGestureVibrator.MIN_API_LEVEL) { addEventListener(new ProcessorGestureVibrator(mFeedbackController)); } addEventListener(new ProcessorWebContent(this)); DimScreenControllerApp dimScreenController = new DimScreenControllerApp(this); mDimScreenController = dimScreenController; if (Build.VERSION.SDK_INT >= ProcessorVolumeStream.MIN_API_LEVEL) { ProcessorVolumeStream processorVolumeStream = new ProcessorVolumeStream(mFeedbackController, mCursorController, mDimScreenController, this); addEventListener(processorVolumeStream); mKeyEventListeners.add(processorVolumeStream); } if (Build.VERSION.SDK_INT >= CustomLabelManager.MIN_API_LEVEL) { mLabelManager = new CustomLabelManager(this); } mKeyComboManager = KeyComboManager.create(this); if (mKeyComboManager != null) { mKeyComboManager.addListener(mKeyComboListener); // Search mode should receive key combos immediately after the TalkBackService. if (Build.VERSION.SDK_INT >= KeyboardSearchManager.MIN_API_LEVEL) { mKeyboardSearchManager = new KeyboardSearchManager(this, mLabelManager); mKeyEventListeners.add(mKeyboardSearchManager); addEventListener(mKeyboardSearchManager); mKeyComboManager.addListener(mKeyboardSearchManager); } mKeyComboManager.addListener(mCursorController); mKeyEventListeners.add(mKeyComboManager); mServiceStateListeners.add(mKeyComboManager); } addEventListener(mSavedNode); mOrientationMonitor = new OrientationMonitor(mSpeechController, this); mOrientationMonitor.addOnOrientationChangedListener(dimScreenController); KeyboardLockMonitor keyboardLockMonitor = new KeyboardLockMonitor(mSpeechController, this); mKeyEventListeners.add(keyboardLockMonitor); if (Build.VERSION.SDK_INT >= TelevisionNavigationController.MIN_API_LEVEL && isDeviceTelevision()) { mTelevisionNavigationController = new TelevisionNavigationController(this); mKeyEventListeners.add(mTelevisionNavigationController); } mAnalytics = new TalkBackAnalytics(this); } public void updateMenuManagerFromPreferences() { mMenuManager.dismissAll(); MenuManager menuManager; if (SharedPreferencesUtils.getBooleanPref(mPrefs, getResources(), R.string.pref_show_context_menu_as_list_key, R.bool.pref_show_menu_as_list)) { menuManager = new ListMenuManager(this); } else { menuManager = new RadialMenuManager(mSupportsTouchScreen, this); } setMenuManager(menuManager); } public void setMenuManager(MenuManager menuManager) { mMenuManager.setMenuManager(menuManager); } public MenuManager getMenuManager() { return mMenuManager; } /** * 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()) { if (LogUtils.LOG_LEVEL <= Log.ERROR) { Log.e(LOGTAG, "Attempted to resume while not suspended"); } return; } setServiceState(SERVICE_STATE_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; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY; info.flags |= AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS; info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; } info.notificationTimeout = 0; // Ensure the initial touch exploration request mode is correct. if (mSupportsTouchScreen && SharedPreferencesUtils.getBooleanPref( mPrefs, getResources(), R.string.pref_explore_by_touch_key, R.bool.pref_explore_by_touch_default)) { info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE; } setServiceInfo(info); if (mRingerModeAndScreenMonitor != null) { registerReceiver(mRingerModeAndScreenMonitor, mRingerModeAndScreenMonitor.getFilter()); // It could now be confused with the current screen state mRingerModeAndScreenMonitor.updateScreenState(); } if (mVolumeMonitor != null) { registerReceiver(mVolumeMonitor, mVolumeMonitor.getFilter()); } if (mBatteryMonitor != null) { registerReceiver(mBatteryMonitor, mBatteryMonitor.getFilter()); } if (mPackageReceiver != null) { registerReceiver(mPackageReceiver, mPackageReceiver.getFilter()); if (mLabelManager != null) { mLabelManager.ensureDataConsistency(); } } if (mSideTapManager != null) { registerReceiver(mSideTapManager, SideTapManager.getFilter()); } 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(); mDimScreenController.resume(); } /** * 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()) { if (LogUtils.LOG_LEVEL <= Log.ERROR) { Log.e(LOGTAG, "Attempted to suspend while already suspended"); } return; } mDimScreenController.suspend(); interruptAllFeedback(true); setServiceState(SERVICE_STATE_SUSPENDED); // Some apps depend on these being set to false when TalkBack is disabled. if (mSupportsTouchScreen) { requestTouchExploration(false); } if (SUPPORTS_WEB_SCRIPT_TOGGLE) { requestWebScripts(false); } mPrefs.unregisterOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener); unregisterReceiver(mActiveReceiver); if (mCallStateMonitor != null) { mCallStateMonitor.stopMonitor(); } if (mRingerModeAndScreenMonitor != null) { unregisterReceiver(mRingerModeAndScreenMonitor); } if (mMenuManager != null) { mMenuManager.clearCache(); } if (mVolumeMonitor != null) { unregisterReceiver(mVolumeMonitor); mVolumeMonitor.releaseControl(); } if (mBatteryMonitor != null) { unregisterReceiver(mBatteryMonitor); } if (mPackageReceiver != null) { unregisterReceiver(mPackageReceiver); } if (mShakeDetector != null) { mShakeDetector.setEnabled(false); } // The tap detector is enabled through reloadPreferences if (mSideTapManager != null) { unregisterReceiver(mSideTapManager); mSideTapManager.onSuspendInfrastructure(); } // 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(); } /** * Shuts down the infrastructure in case it has been initialized. */ private void shutdownInfrastructure() { // we put it first to be sure that screen dimming would be removed even if code bellow // will crash by any reason. Because leaving user with dimmed screen is super bad mDimScreenController.shutdown(); if (mCursorController != null) { mCursorController.shutdown(); } if (mFullScreenReadController != null) { mFullScreenReadController.shutdown(); } if (mLabelManager != null) { mLabelManager.shutdown(); } mFeedbackController.shutdown(); mSpeechController.shutdown(); } /** * Adds an event listener. * * @param listener The listener to add. */ public void addEventListener(AccessibilityEventListener listener) { mAccessibilityEventProcessor.addAccessibilityEventListener(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. */ public void postRemoveEventListener(final AccessibilityEventListener listener) { mAccessibilityEventProcessor.postRemoveAccessibilityEventListener(listener); } /** * Reloads service preferences. */ private void reloadPreferences() { final Resources res = getResources(); mAccessibilityEventProcessor.setSpeakWhenScreenOff(SharedPreferencesUtils.getBooleanPref( mPrefs, res, R.string.pref_screenoff_key, R.bool.pref_screenoff_default)); mAutomaticResume = mPrefs.getString(res.getString(R.string.pref_resume_talkback_key), getString(R.string.resume_screen_on)); final boolean silenceOnProximity = SharedPreferencesUtils.getBooleanPref( mPrefs, res, R.string.pref_proximity_key, R.bool.pref_proximity_default); mSpeechController.setSilenceOnProximity(silenceOnProximity); LogUtils.setLogLevel( SharedPreferencesUtils.getIntFromStringPref(mPrefs, res, R.string.pref_log_level_key, R.string.pref_log_level_default)); 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 activate" long-hover hint. NodeHintRule.NodeHintHelper.updateHints(useSingleTap, isDeviceTelevision()); } 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 (mSideTapManager != null) { mSideTapManager.onReloadPreferences(); } if (mSupportsTouchScreen) { // Touch exploration *must* be enabled on TVs for TalkBack to function. final boolean touchExploration = isDeviceTelevision() || SharedPreferencesUtils.getBooleanPref(mPrefs, res, R.string.pref_explore_by_touch_key, R.bool.pref_explore_by_touch_default); requestTouchExploration(touchExploration); } 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); } updateMenuManagerFromPreferences(); } /** * Attempts to change the state of touch exploration. * <p> * Should only be called if {@link #mSupportsTouchScreen} is true. * * @param requestedState {@code true} to request exploration. */ private void requestTouchExploration(boolean requestedState) { final AccessibilityServiceInfo info = getServiceInfo(); if (info == null) { if (LogUtils.LOG_LEVEL <= Log.ERROR) { Log.e(LOGTAG, "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. */ public boolean showTutorial() { if (isInArc()) { return false; } boolean isDeviceProvisioned = true; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { isDeviceProvisioned = Settings.Secure .getInt(getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 1) != 0; } if (isDeviceProvisioned && !isFirstTimeUser()) { return false; } final int touchscreenState = getResources().getConfiguration().touchscreen; if (touchscreenState != Configuration.TOUCHSCREEN_NOTOUCH && mSupportsTouchScreen) { final Intent tutorial = new Intent(this, AccessibilityTutorialActivity.class); tutorial.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); tutorial.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(tutorial); return true; } return false; } private boolean isFirstTimeUser() { return mPrefs.getBoolean(PREF_FIRST_TIME_USER, true); } public void startCallStateMonitor() { if (mCallStateMonitor == null || mCallStateMonitor.isStarted()) { return; } mCallStateMonitor.startMonitor(); } /** * 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) { if (LogUtils.LOG_LEVEL <= Log.ERROR) { Log.e(LOGTAG, "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); } private final KeyComboManager.KeyComboListener mKeyComboListener = new KeyComboManager.KeyComboListener() { @Override public boolean onComboPerformed(int id) { switch (id) { case KeyComboManager.ACTION_SUSPEND_OR_RESUME: if (mServiceState == SERVICE_STATE_SUSPENDED) { resumeTalkBack(); } else if (mServiceState == SERVICE_STATE_ACTIVE) { requestSuspendTalkBack(); } return true; case KeyComboManager.ACTION_BACK: TalkBackService.this.performGlobalAction(GLOBAL_ACTION_BACK); return true; case KeyComboManager.ACTION_HOME: TalkBackService.this.performGlobalAction(GLOBAL_ACTION_HOME); return true; case KeyComboManager.ACTION_NOTIFICATION: TalkBackService.this.performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS); return true; case KeyComboManager.ACTION_RECENTS: TalkBackService.this.performGlobalAction(GLOBAL_ACTION_RECENTS); return true; case KeyComboManager.ACTION_GRANULARITY_INCREASE: mCursorController.nextGranularity(); return true; case KeyComboManager.ACTION_GRANULARITY_DECREASE: mCursorController.previousGranularity(); return true; case KeyComboManager.ACTION_READ_FROM_TOP: mFullScreenReadController.startReadingFromBeginning(); return true; case KeyComboManager.ACTION_READ_FROM_NEXT_ITEM: mFullScreenReadController.startReadingFromNextNode(); return true; case KeyComboManager.ACTION_GLOBAL_CONTEXT_MENU: showGlobalContextMenu(); return true; case KeyComboManager.ACTION_LOCAL_CONTEXT_MENU: showLocalContextMenu(); return true; case KeyComboManager.ACTION_OPEN_MANAGE_KEYBOARD_SHORTCUTS: openManageKeyboardShortcuts(); return true; case KeyComboManager.ACTION_OPEN_TALKBACK_SETTINGS: openTalkBackSettings(); return true; } return false; } }; /** * Reloads preferences whenever their values change. */ private final OnSharedPreferenceChangeListener mSharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { if (LogUtils.LOG_LEVEL <= Log.DEBUG) { Log.d(LOGTAG, "A shared preference changed: " + key); } reloadPreferences(); } }; /** * 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)) { mGestureController.onGesture( intent.getIntExtra(EXTRA_GESTURE_ACTION, R.string.shortcut_value_unassigned)); } } }; /** * 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)) { if (mAutomaticResume.equals(getString(R.string.resume_screen_keyguard))) { final KeyguardManager keyguard = (KeyguardManager) getSystemService( Context.KEYGUARD_SERVICE); if (keyguard.inKeyguardRestrictedInputMode()) { resumeTalkBack(); } } else if (mAutomaticResume.equals(getString(R.string.resume_screen_on))) { resumeTalkBack(); } } } }; private final SpeechController.UtteranceCompleteRunnable mDisableTalkBackHandler = new SpeechController.UtteranceCompleteRunnable() { @Override public void run(int status) { // Double-check that TalkBack is still available before we try to disable it. TalkBackService service = TalkBackService.getInstance(); if (service != null) { service.disableSelf(); } } }; public void onLockedBootCompleted() { if (mServiceState == SERVICE_STATE_INACTIVE) { // onServiceConnected has not completed yet. We need to defer the boot completion // callback until after onServiceConnected has run. mLockedBootCompletedPending = true; } else { // onServiceConnected has already completed, so we should run the callback now. onLockedBootCompletedInternal(); } } private void onLockedBootCompletedInternal() { // Update TTS quietly. // If the TTS changes here, it is probably a non-FBE TTS that didn't appear in the TTS // engine list when TalkBack initialized during system boot, so we want the change to be // invisible to the user. mSpeechController.updateTtsEngine(true /* quiet */); if (!isServiceActive() && mAutomaticResume != null && !mAutomaticResume.equals(getString(R.string.resume_screen_manual))) { resumeTalkBack(); } } public void onUnlockedBootCompleted() { // Update TTS and allow announcement. // If the TTS changes here, it is probably a non-FBE TTS that is available after the user // unlocks their phone. In this case, the user heard Google TTS at the lock screen, so we // should let them know that we're using their preferred TTS now. mSpeechController.updateTtsEngine(false /* quiet */); if (mLabelManager != null) { mLabelManager.ensureLabelsLoaded(); } } @Override public void uncaughtException(Thread thread, Throwable ex) { try { if (mDimScreenController != null) { mDimScreenController.shutdown(); } if (mMenuManager != null && mMenuManager.isMenuShowing()) { mMenuManager.dismissAll(); } if (mSuspendDialog != null) { mSuspendDialog.dismiss(); } } catch (Exception e) { // Do nothing. } finally { if (mSystemUeh != null) { mSystemUeh.uncaughtException(thread, ex); } } } public void setTestingListener(TalkBackListener testingListener) { mAccessibilityEventProcessor.setTestingListener(testingListener); } /** * 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 TalkBackService#addServiceStateListener(ServiceStateListener)} * {@link TalkBackService#removeServiceStateListener(ServiceStateListener)} * {@link TalkBackService#SERVICE_STATE_INACTIVE} * {@link TalkBackService#SERVICE_STATE_ACTIVE} * {@link TalkBackService#SERVICE_STATE_SUSPENDED} */ public interface ServiceStateListener { void onServiceStateChanged(int newState); } /** * Interface for key event listeners. */ public interface KeyEventListener { boolean onKeyEvent(KeyEvent event); boolean processWhenServiceSuspended(); } public boolean isScreenLayoutRTL() { Configuration config = getResources().getConfiguration(); if (config == null) { return false; } return (config.screenLayout & Configuration.SCREENLAYOUT_LAYOUTDIR_MASK) == Configuration.SCREENLAYOUT_LAYOUTDIR_RTL; } public boolean isScreenOrientationLandscape() { Configuration config = getResources().getConfiguration(); if (config == null) { return false; } return config.orientation == Configuration.ORIENTATION_LANDSCAPE; } public boolean isDeviceTelevision() { return mIsDeviceTelevision; } public InputModeManager getInputModeManager() { return mInputModeManager; } public CharSequence getApplicationLabel(CharSequence packageName) { PackageManager packageManager = (PackageManager) getPackageManager(); if (packageManager == null) { return null; } ApplicationInfo applicationInfo; try { applicationInfo = packageManager.getApplicationInfo(packageName.toString(), 0 /* no flag */); } catch (PackageManager.NameNotFoundException exception) { return null; } return packageManager.getApplicationLabel(applicationInfo); } public static boolean isInArc() { return Build.DEVICE != null && Build.DEVICE.matches(ARC_DEVICE_PATTERN); } public void notifyDumpEventPreferenceChanged(int eventType, boolean shouldDump) { mAccessibilityEventProcessor.onDumpEventPreferenceChanged(eventType, shouldDump); } }