Java tutorial
/* * 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.screenspeak; import android.annotation.SuppressLint; import android.os.Bundle; 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.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; 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.widget.CheckBox; import android.widget.ScrollView; import android.widget.TextView; import com.android.switchaccess.SwitchAccessService; import com.android.screenspeak.Analytics; import com.android.screenspeak.BatteryMonitor; import com.android.screenspeak.CallStateMonitor; import com.android.screenspeak.KeyComboManager; import com.android.screenspeak.KeyboardSearchManager; import com.android.screenspeak.OrientationMonitor; import com.android.screenspeak.R; import com.android.screenspeak.RingerModeAndScreenMonitor; import com.android.screenspeak.SavedNode; import com.android.screenspeak.ShakeDetector; import com.android.screenspeak.ShortcutProxyActivity; import com.android.screenspeak.SideTapManager; import com.android.screenspeak.SpeechController; import com.android.screenspeak.ScreenSpeakAnalytics; import com.android.screenspeak.ScreenSpeakPreferencesActivity; import com.android.screenspeak.ScreenSpeakUpdateHelper; import com.android.screenspeak.VolumeMonitor; import com.android.screenspeak.contextmenu.ListMenuManager; import com.android.screenspeak.contextmenu.MenuManager; import com.android.screenspeak.contextmenu.MenuManagerWrapper; import com.android.screenspeak.contextmenu.RadialMenuManager; import com.android.screenspeak.contextmenu.ScreenSpeakRadialMenuClient; import com.android.screenspeak.controller.CursorController; import com.android.screenspeak.controller.CursorControllerApp; import com.android.screenspeak.controller.DimScreenController; import com.android.screenspeak.controller.DimScreenControllerApp; import com.android.screenspeak.controller.FeedbackController; import com.android.screenspeak.controller.FeedbackControllerApp; import com.android.screenspeak.controller.FullScreenReadController; import com.android.screenspeak.controller.FullScreenReadControllerApp; import com.android.screenspeak.controller.GestureController; import com.android.screenspeak.controller.GestureControllerApp; import com.android.screenspeak.eventprocessor.AccessibilityEventProcessor; import com.android.screenspeak.eventprocessor.AccessibilityEventProcessor.ScreenSpeakListener; import com.android.screenspeak.eventprocessor.ProcessorEventQueue; import com.android.screenspeak.eventprocessor.ProcessorFocusAndSingleTap; import com.android.screenspeak.eventprocessor.ProcessorGestureVibrator; import com.android.screenspeak.eventprocessor.ProcessorAccessibilityHints; import com.android.screenspeak.eventprocessor.ProcessorPhoneticLetters; import com.android.screenspeak.eventprocessor.ProcessorScrollPosition; import com.android.screenspeak.eventprocessor.ProcessorVolumeStream; import com.android.screenspeak.eventprocessor.ProcessorWebContent; import com.android.screenspeak.controller.TextCursorController; import com.android.screenspeak.controller.TextCursorControllerApp; import com.android.screenspeak.speechrules.NodeHintRule; import com.android.screenspeak.speechrules.NodeSpeechRuleProcessor; import com.android.screenspeak.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 ScreenSpeakService extends AccessibilityService implements Thread.UncaughtExceptionHandler { /** Whether the user has seen the ScreenSpeak tutorial. */ public static final String PREF_FIRST_TIME_USER = "first_time_user"; /** Permission required to perform gestures. */ public static final String PERMISSION_SCREENSPEAK = "com.google.android.marvin.feedback.permission.SCREENSPEAK"; /** 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"; /** Whether the current SDK supports service-managed web scripts. */ private static final boolean SUPPORTS_WEB_SCRIPT_TOGGLE = (Build.VERSION.SDK_INT >= 18); /** Action used to resume feedback. */ private static final String ACTION_RESUME_FEEDBACK = "com.google.android.marvin.screenspeak.RESUME_FEEDBACK"; /** An active instance of ScreenSpeak. */ private static ScreenSpeakService 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 = "ScreenSpeakService"; /** * 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; /** * {@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 ScreenSpeak. */ 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 ScreenSpeak 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; @Override public void onCreate() { super.onCreate(); sInstance = this; setServiceState(SERVICE_STATE_INACTIVE); mPrefs = PreferenceManager.getDefaultSharedPreferences(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 >= 21) { 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 ScreenSpeak, showing a confirmation dialog if applicable. */ public void requestSuspendScreenSpeak() { final boolean showConfirmation = SharedPreferencesUtils.getBooleanPref(mPrefs, getResources(), R.string.pref_show_suspension_confirmation_dialog, R.bool.pref_show_suspension_confirmation_dialog_default); if (showConfirmation) { confirmSuspendScreenSpeak(); } else { suspendScreenSpeak(); } } /** * Shows a dialog asking the user to confirm suspension of ScreenSpeak. */ private void confirmSuspendScreenSpeak() { // 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_screenspeak_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); } suspendScreenSpeak(); } } }; 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_screenspeak) .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 ScreenSpeak and Explore by Touch. */ public void suspendScreenSpeak() { if (!isServiceActive()) { if (LogUtils.LOG_LEVEL <= Log.ERROR) { Log.e(LOGTAG, "Attempted to suspend ScreenSpeak 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"); } } } final IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_RESUME_FEEDBACK); filter.addAction(Intent.ACTION_SCREEN_ON); registerReceiver(mSuspendedReceiver, filter, PERMISSION_SCREENSPEAK, null); // Suspending infrastructure sets sIsScreenSpeakSuspended 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_screenspeak_suspended)) .setContentText(getString(R.string.notification_message_screenspeak_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.screenspeak_suspended), SpeechController.QUEUE_MODE_UNINTERRUPTIBLE, 0, null); } /** * Resumes ScreenSpeak and Explore by Touch. */ public void resumeScreenSpeak() { if (isServiceActive()) { if (LogUtils.LOG_LEVEL <= Log.ERROR) { Log.e(LOGTAG, "Attempted to resume ScreenSpeak when not suspended."); } return; } SharedPreferencesUtils.storeBooleanAsync(mPrefs, getString(R.string.pref_suspended), false); unregisterReceiver(mSuspendedReceiver); resumeInfrastructure(); mSpeechController.speak(getString(R.string.screenspeak_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 * (ScreenSpeak). 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); 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(KM): Will this negatively affect something like Books? if (Build.VERSION.SDK_INT <= 16) { 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() { if (mKeyComboManager == null) { throw new RuntimeException("mKeyComboManager has not been initialized"); } return mKeyComboManager; } 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; } public Analytics getAnalytics() { if (mAnalytics == null) { throw new RuntimeException("mAnalytics has not been initialized"); } return mAnalytics; } /** * Obtains the shared instance of ScreenSpeak's {@link ShakeDetector} * * @return the shared {@link ShakeDetector} instance, or null if not initialized. */ public ShakeDetector getShakeDetector() { return mShakeDetector; } /** Save the currently focused node so that focus can be returned to it later. */ public void saveFocusedNode() { mSavedNode.recycle(); AccessibilityNodeInfoCompat node = mCursorController.getCursor(); if (node != null) { mSavedNode.saveNodeState(node, mCursorController.getGranularityAt(node)); node.recycle(); } } /** * 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()) { mCursorController.setGranularity(mSavedNode.getGranularity(), refreshed, false); SavedNode.Selection selection = mSavedNode.getSelection(); if (selection != null) { Bundle args = new Bundle(); args.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, selection.start); args.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, selection.end); PerformActionUtils.performAction(refreshed, AccessibilityNodeInfoCompat.ACTION_SET_SELECTION, args); } PerformActionUtils.performAction(refreshed, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); } refreshed.recycle(); } mSavedNode.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); } } @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.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 ScreenSpeakUpdateHelper helper = new ScreenSpeakUpdateHelper(this); helper.showPendingNotifications(); helper.checkUpdate(); final ContentResolver resolver = getContentResolver(); if (!ScreenSpeakPreferencesActivity.isTouchExplorationEnabled(resolver) || !showTutorial()) { startCallStateMonitor(); } if (mPrefs.getBoolean(getString(R.string.pref_suspended), false)) { suspendScreenSpeak(); } else { mSpeechController.speak(getString(R.string.screenspeak_on), SpeechController.QUEUE_MODE_UNINTERRUPTIBLE, 0, null); } } /** * @return The current state of the ScreenSpeak service, or * {@code INACTIVE} if the service is not initialized. */ public static int getServiceState() { final ScreenSpeakService service = getInstance(); if (service == null) { return SERVICE_STATE_INACTIVE; } return service.mServiceState; } /** * @return {@code true} if ScreenSpeak is running and initialized, * {@code false} otherwise. */ public static boolean isServiceActive() { return (getServiceState() == SERVICE_STATE_ACTIVE); } /** * Returns the active ScreenSpeak instance, or {@code null} if not available. */ public static ScreenSpeakService 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); 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); mShakeDetector = new ShakeDetector(mFullScreenReadController, this); mMenuManager = new MenuManagerWrapper(); updateMenuManager(mSpeechController, mFeedbackController); // 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, this)); addEventListener(new ProcessorAccessibilityHints(this, mSpeechController, mCursorController)); addEventListener(new ProcessorPhoneticLetters(this, mSpeechController)); mProcessorFollowFocus = new ProcessorFocusAndSingleTap(mCursorController, mFeedbackController, mSpeechController, this); 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(KM): 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); } if (Build.VERSION.SDK_INT >= KeyComboManager.MIN_API_LEVEL) { mKeyComboManager = new KeyComboManager(this); mKeyComboManager.addListener(mKeyComboListener); // Search mode should receive key combos immediately after the ScreenSpeakService. 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); } addEventListener(mSavedNode); mOrientationMonitor = new OrientationMonitor(mSpeechController, this); mOrientationMonitor.addOnOrientationChangedListener(dimScreenController); mAnalytics = new ScreenSpeakAnalytics(this); } private void updateMenuManager(SpeechController speechController, FeedbackController cachedFeedbackController) { if (speechController == null) throw new IllegalStateException(); if (cachedFeedbackController == null) throw new IllegalStateException(); 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 { // Set up the radial menu manager and ScreenSpeak-specific client. final ScreenSpeakRadialMenuClient radialMenuClient = new ScreenSpeakRadialMenuClient(this); RadialMenuManager radialMenuManager = new RadialMenuManager(mSupportsTouchScreen, this, speechController, cachedFeedbackController); radialMenuManager.setClient(radialMenuClient); menuManager = radialMenuManager; } mMenuManager.setMenuManager(menuManager); } public MenuManager getMenuManager() { return mMenuManager; } /** * Registers listeners, sets service info, loads preferences. This should be * called from {@link #onServiceConnected} and when ScreenSpeak 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_SCREENSPEAK, 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(); if (mDimScreenController.isDimmingEnabled()) { mDimScreenController.makeScreenDim(); } } /** * Registers listeners, sets service info, loads preferences. This should be called from * {@link #onServiceConnected} and when ScreenSpeak 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.makeScreenBright(); interruptAllFeedback(); setServiceState(SERVICE_STATE_SUSPENDED); // Some apps depend on these being set to false when ScreenSpeak 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.setSpeakCallerId(SharedPreferencesUtils.getBooleanPref(mPrefs, res, R.string.pref_caller_id_key, R.bool.pref_caller_id_default)); 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_screenspeak_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 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 (mSideTapManager != null) { mSideTapManager.onReloadPreferences(); } if (mSupportsTouchScreen) { final boolean touchExploration = 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); } updateMenuManager(mSpeechController, mFeedbackController); } /** * 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 (!mPrefs.getBoolean(PREF_FIRST_TIME_USER, true)) { return false; } final Editor editor = mPrefs.edit(); editor.putBoolean(PREF_FIRST_TIME_USER, false); editor.apply(); 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; } 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: requestSuspendScreenSpeak(); return true; case KeyComboManager.ACTION_BACK: ScreenSpeakService.this.performGlobalAction(GLOBAL_ACTION_BACK); return true; case KeyComboManager.ACTION_HOME: ScreenSpeakService.this.performGlobalAction(GLOBAL_ACTION_HOME); return true; case KeyComboManager.ACTION_NOTIFICATION: ScreenSpeakService.this.performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS); return true; case KeyComboManager.ACTION_RECENTS: ScreenSpeakService.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; } 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)) { resumeScreenSpeak(); } 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()) { resumeScreenSpeak(); } } else if (mAutomaticResume.equals(getString(R.string.resume_screen_on))) { resumeScreenSpeak(); } } } }; public void onBootCompleted() { if (!isServiceActive() && !mAutomaticResume.equals(getString(R.string.resume_screen_manual))) { resumeScreenSpeak(); } } @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(ScreenSpeakListener testingListener) { mAccessibilityEventProcessor.setTestingListener(testingListener); } /** * Interface for receiving callbacks when the state of the ScreenSpeak service * changes. * <p> * Implementing controllers should note that this may be invoked even after * the controller was explicitly shut down by ScreenSpeak. * <p> * {@link ScreenSpeakService#addServiceStateListener(ServiceStateListener)} * {@link ScreenSpeakService#removeServiceStateListener(ServiceStateListener)} * {@link ScreenSpeakService#SERVICE_STATE_INACTIVE} * {@link ScreenSpeakService#SERVICE_STATE_ACTIVE} * {@link ScreenSpeakService#SERVICE_STATE_SUSPENDED} */ public interface ServiceStateListener { void onServiceStateChanged(int newState); } /** * Interface for key event listeners. */ public interface KeyEventListener { boolean onKeyEvent(KeyEvent event); boolean processWhenServiceSuspended(); } }