diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..e0430541d --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.gradle/ +.idea/ +build/ +.cxx/ +braille/translate/src/phone/res/ +gradle/ +gradlew +gradlew.bat +local.properties diff --git a/README.md b/README.md index 3c9273062..6ac398f63 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,19 @@ # Introduction -This repository contains source code for Google's TalkBack, which is a screen +![TalkBack For Developers Logo][100] + +This repository contains forked source code for Google's TalkBack, which is a screen reader for blind and visually-impaired users of Android. For usage instructions, -see +read [TalkBack User Guide](https://support.google.com/accessibility/android/answer/6283677?hl=en). ### How to Build -To build TalkBack, run ./build.sh, which will produce an apk file. +To build TalkBack, run `./build.sh`, which will produce an apk file. You can also specify a serial number by running `./build.sh -s [SERIAL]` to automatically install to your device. + +Ensure that: +- jenv local 1.8 +- NDK 24.0.8215888 is installed ### How to Install @@ -18,3 +24,59 @@ Install the apk onto your Android device in the usual manner using adb. With the apk now installed on the device, the TalkBack service should now be present under Settings -> Accessibility, and will be off by default. To turn it on, toggle the switch preference to the on position. + +### How to use with ADB + +```shell +# Activate +adb shell settings put secure enabled_accessibility_services com.android.talkback4d/com.developer.talkback.TalkBackDevService + +# Deactivate +adb shell settings put secure enabled_accessibility_services null + +# General format +# All commands take the format of a broadcast +adb shell am broadcast -a com.a11y.adb.[ACTION] [OPTIONS] + +BROADCAST () { adb shell am broadcast "$@"; } + +# Perform actions +BROADCAST -a com.a11y.adb.previous # default granularity +BROADCAST -a com.a11y.adb.next # default granularity + +BROADCAST -a com.a11y.adb.previous -e mode headings # move tp previous heading +BROADCAST -a com.a11y.adb.next -e mode headings # move to next heading + +# Toggle settings +BROADCAST -a com.a11y.adb.toggle_speech_output # show special toasts for spoken text +BROADCAST -a com.a11y.adb.perform_click_action +BROADCAST -a com.a11y.adb.volume_toggle # special case that toggles between 5% and 50% +``` + +## All parameters +- [Action list][0] +- [Action parameter list in the SelectorController enum][1] +- [Developer settings][2] +- [Volume specific controls][3] + +## TODO +- Add curtain + - Activate via ADB +- Dev tools: + - Colour contrast check + - Touch target size check + - Developer-friendly details on curtain (add to announcements) + - [NAF control checker][4] + - Hide all screen except highlighted node + - Show labels + +## FIXED +- Menus lacking dark mode / styling +- Back button in menus + +[0]: https://github.com/qbalsdon/talkback/blob/main/talkback/src/main/java/com/google/android/accessibility/talkback/adb/A11yAction.java +[1]: https://github.com/qbalsdon/talkback/blob/main/talkback/src/main/java/com/google/android/accessibility/talkback/selector/SelectorController.java#L116 +[2]: https://github.com/qbalsdon/talkback/blob/main/talkback/src/main/java/com/google/android/accessibility/talkback/adb/ToggleDeveloperSetting.java +[3]: https://github.com/qbalsdon/talkback/blob/main/talkback/src/main/java/com/google/android/accessibility/talkback/adb/VolumeSetting.java +[4]: https://android.googlesource.com/platform/frameworks/uiautomator/+/android-support-test/src/main/java/android/support/test/uiautomator/AccessibilityNodeInfoDumper.java#125 +[100]: ./talkback/src/main/res/drawable-xxxhdpi/icon_tb4d_round.png "TalkBack for developers" {: height="200px"} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 53105a71f..5af8388b1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,6 @@ // For building the open-source release of TalkBack. - apply plugin: 'com.android.application' - apply from: 'shared.gradle' - apply from: 'version.gradle' final BUILD_TIMESTAMP = new Date().format('yyyy_MM_dd_HHmm') @@ -19,7 +16,7 @@ buildscript { } dependencies { classpath 'org.aspectj:aspectjtools:1.8.1' - classpath 'com.android.tools.build:gradle:3.5.4' + classpath 'com.android.tools.build:gradle:4.2.2' } } @@ -31,10 +28,10 @@ allprojects { } android { - buildToolsVersion '29.0.0' + buildToolsVersion '30.0.2' defaultConfig { applicationId talkbackApplicationId - versionName talkbackVersionName + "-" + BUILD_TIMESTAMP + versionName talkbackVersionName + "-" + BUILD_TIMESTAMP + "-" + talkback4DevVersionName minSdkVersion 26 targetSdkVersion 30 testInstrumentationRunner 'android.test.InstrumentationTestRunner' diff --git a/build.sh b/build.sh index d23437a76..71209590d 100755 --- a/build.sh +++ b/build.sh @@ -6,8 +6,22 @@ ### JAVA_HOME # path to local copy of Java SDK. Should be Java 8. # On gLinux, use 'export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64' - -GRADLE_DOWNLOAD_VERSION=5.4.1 +#----------------------------------------------------------------------------- +DEVICE="" +PIPELINE=false +USAGE="./build.sh [[-s | --device] SERIAL_NUMBER]" +while [[ "$#" -gt 0 ]]; do + case $1 in + -s|--device) DEVICE="$2"; shift ;; + -p) PIPELINE=true; shift ;; + -h|--help) echo $USAGE; exit 0 ;; + *) echo "Unknown parameter passed: $1"; exit 1 ;; + esac + shift +done +#----------------------------------------------------------------------------- + +GRADLE_DOWNLOAD_VERSION=6.7.1 GRADLE_TRACE=false # change to true to enable verbose logging of gradlew @@ -60,6 +74,14 @@ log "cat local.properties"; cat local.properties log +#----------------------------------------------------------------------------- +if [[ "$PIPELINE" = false ]]; then + unset JAVA_HOME; + export JAVA_HOME=$(/usr/libexec/java_home -v"1.8"); +fi +#----------------------------------------------------------------------------- + + if [[ -z "${JAVA_HOME}" ]]; then fail_with_message "JAVA_HOME environment variable is unset. It should be set to a Java 8 SDK (in order for the license acceptance to work)" fi @@ -79,9 +101,10 @@ if [[ $ACCEPT_SDK_LICENSES_EXIT_CODE -ne 0 ]]; then fi -# Having compileSdkVersion=31 leads to javac error "unrecognized Attribute name MODULE (class com.sun.tools.javac.util.UnsharedNameTable$NameImpl)"; switching to Java 11 fixes this problem. -sudo update-java-alternatives --set java-1.11.0-openjdk-amd64 -export JAVA_HOME=/usr/lib/jvm/java-1.11.0-openjdk-amd64 +if [[ "$PIPELINE" = false ]]; then + unset JAVA_HOME; + export JAVA_HOME=$(/usr/libexec/java_home -v"11"); +fi log "\${JAVA_HOME}: ${JAVA_HOME}" log "ls \${JAVA_HOME}:"; ls "${JAVA_HOME}" log "java -version:"; java -version @@ -91,14 +114,29 @@ log GRADLE_ZIP_REMOTE_FILE=gradle-${GRADLE_DOWNLOAD_VERSION}-bin.zip GRADLE_ZIP_DEST_PATH=~/Desktop/${GRADLE_DOWNLOAD_VERSION}.zip -log "Download gradle binary from the web ${GRADLE_ZIP_REMOTE_FILE} to ${GRADLE_ZIP_DEST_PATH} using wget" -wget -O ${GRADLE_ZIP_DEST_PATH} https://services.gradle.org/distributions/${GRADLE_ZIP_REMOTE_FILE} -log +GRADLE_UNZIP_HOSTING_FOLDER=/opt/gradle-${GRADLE_DOWNLOAD_VERSION} + + +if [[ ! -f "$GRADLE_ZIP_DEST_PATH" ]]; then + log "--> Downloading GRADLE" + if [[ "$PIPELINE" = true ]]; then + mkdir ~/tmp + mkdir ~/tmp/opt + GRADLE_ZIP_DEST_PATH=~/tmp/${GRADLE_DOWNLOAD_VERSION}.zip + GRADLE_UNZIP_HOSTING_FOLDER=~/tmp/opt/gradle-${GRADLE_DOWNLOAD_VERSION} + log "Download gradle binary from the web ${GRADLE_ZIP_REMOTE_FILE} to ${GRADLE_ZIP_DEST_PATH} using wget" + wget -O ${GRADLE_ZIP_DEST_PATH} https://services.gradle.org/distributions/${GRADLE_ZIP_REMOTE_FILE} + log + else + log "Download gradle binary from the web ${GRADLE_ZIP_REMOTE_FILE} to ${GRADLE_ZIP_DEST_PATH} using curl" + COMMAND="curl -L -o ${GRADLE_ZIP_DEST_PATH} https://services.gradle.org/distributions/${GRADLE_ZIP_REMOTE_FILE}" + sudo curl -L -o ${GRADLE_ZIP_DEST_PATH} https://services.gradle.org/distributions/${GRADLE_ZIP_REMOTE_FILE} + fi +fi -GRADLE_UNZIP_HOSTING_FOLDER=/opt/gradle-${GRADLE_DOWNLOAD_VERSION} log "Unzip gradle zipfile ${GRADLE_ZIP_DEST_PATH} to ${GRADLE_UNZIP_HOSTING_FOLDER}" -sudo unzip -n -d ${GRADLE_UNZIP_HOSTING_FOLDER} ${GRADLE_ZIP_DEST_PATH} +unzip -n -d ${GRADLE_UNZIP_HOSTING_FOLDER} ${GRADLE_ZIP_DEST_PATH} log @@ -137,16 +175,17 @@ log "./gradlew assembleDebug" BUILD_EXIT_CODE=$? log - -print_sdk_info -log - - if [[ $BUILD_EXIT_CODE -eq 0 ]]; then + if [[ ! -z $DEVICE ]]; then + log "installing on $DEVICE" + adb -s $DEVICE install ./build/outputs/apk/phone/debug/talkback-phone-debug.apk + fi + print_sdk_info + log + log "find . -name *.apk" find . -name "*.apk" log fi - exit $BUILD_EXIT_CODE ### This should be the last line in this file diff --git a/download_resources.sh b/download_resources.sh new file mode 100755 index 000000000..f4445e666 --- /dev/null +++ b/download_resources.sh @@ -0,0 +1,45 @@ +echo "talkback/src/main/res/drawable-anydpi-v26/icon_tb4d.xml" > tempfile.txt +echo "talkback/src/main/res/drawable-anydpi-v26/icon_tb4d_round.xml" >> tempfile.txt +echo "talkback/src/main/res/drawable-hdpi/icon_tb4d.png" >> tempfile.txt +echo "talkback/src/main/res/drawable-hdpi/icon_tb4d_round.png" >> tempfile.txt +echo "talkback/src/main/res/drawable-ldpi/icon_tb4d.png" >> tempfile.txt +echo "talkback/src/main/res/drawable-ldpi/icon_tb4d_round.png" >> tempfile.txt +echo "talkback/src/main/res/drawable-mdpi/icon_tb4d.png" >> tempfile.txt +echo "talkback/src/main/res/drawable-mdpi/icon_tb4d_round.png" >> tempfile.txt +echo "talkback/src/main/res/drawable-xhdpi/icon_tb4d.png" >> tempfile.txt +echo "talkback/src/main/res/drawable-xhdpi/icon_tb4d_round.png" >> tempfile.txt +echo "talkback/src/main/res/drawable-xxhdpi/icon_tb4d.png" >> tempfile.txt +echo "talkback/src/main/res/drawable-xxhdpi/icon_tb4d_round.png" >> tempfile.txt +echo "talkback/src/main/res/drawable-xxxhdpi/icon_tb4d.png" >> tempfile.txt +echo "talkback/src/main/res/drawable-xxxhdpi/icon_tb4d_round.png" >> tempfile.txt +echo "talkback/src/main/res/drawable/ic_tb4d_launcher_background.xml" >> tempfile.txt +echo "talkback/src/main/res/drawable/ic_tb4d_launcher_foreground.xml" >> tempfile.txt +echo "talkback/src/main/res/drawable/talkback_intro.png" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_1.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_2.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_3.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_4.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_5.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_6.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_7.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_8.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_9.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_10.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_11.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_12.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_13.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_14.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_15.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/rounded_corner_16.xml" >> tempfile.txt +echo "utils/src/main/res/values/integers.xml" >> tempfile.txt +echo "utils/src/main/res/drawable/toast_transition.xml" >> tempfile.txt + +URL=https://raw.githubusercontent.com/qbalsdon/talkback/feature/tb4d_modifications/ +mkdir ./utils/src/main/res/drawable/ +cat tempfile.txt | while read ITEM +do + curl -L $URL$ITEM -o ./$ITEM +done + +rm tempfile.txt diff --git a/shared.gradle b/shared.gradle index 1cdba1a15..62817949d 100644 --- a/shared.gradle +++ b/shared.gradle @@ -1,11 +1,11 @@ // For building the open-source release of TalkBack. ext { - talkbackApplicationId = "com.android.talkback" + talkbackApplicationId = "com.android.talkback4d" } android { - compileSdkVersion 'android-Tiramisu' + compileSdkVersion 'android-33' compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -59,3 +59,9 @@ dependencies { // UI understanding implementation 'joda-time:joda-time:2.9.9' } + +android { + defaultConfig { + buildConfigField("String", "APPLICATION_ID", "\"" + talkbackApplicationId + "\"") + } +} diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index f323f5328..0c763a47f 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ keyEventListeners = new ArrayList<>(); - /** - * 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 List keyEventListeners = new ArrayList<>(); + /** + * The current state of the service. + */ + private int serviceState; - /** The current state of the service. */ - private int serviceState; + /** + * Components to receive callbacks on changes in the service's state. + */ + private List serviceStateListeners = new ArrayList<>(); - /** Components to receive callbacks on changes in the service's state. */ - private List serviceStateListeners = new ArrayList<>(); + /** + * Controller for speech feedback. + */ + private SpeechControllerImpl speechController; - /** Controller for speech feedback. */ - private SpeechControllerImpl speechController; + /** + * Controller for diagnostic overlay (developer mode). + */ + private DiagnosticOverlayControllerImpl diagnosticOverlayController; - /** Controller for diagnostic overlay (developer mode). */ - private DiagnosticOverlayControllerImpl diagnosticOverlayController; + /** + * Staged pipeline for separating interpreters, feedback-mappers, and actors. + */ + private Pipeline pipeline; - /** Staged pipeline for separating interpreters, feedback-mappers, and actors. */ - private Pipeline pipeline; + /** + * Controller for audio and haptic feedback. + */ + private FeedbackController feedbackController; - /** Controller for audio and haptic feedback. */ - private FeedbackController feedbackController; + /** + * Watches the proximity sensor, and silences feedback when triggered. + */ + private ProximitySensorListener proximitySensorListener; - /** Watches the proximity sensor, and silences feedback when triggered. */ - private ProximitySensorListener proximitySensorListener; + private PassThroughModeActor passThroughModeActor; + private GlobalVariables globalVariables; + private EventFilter eventFilter; + private TextEventInterpreter textEventInterpreter; + private Compositor compositor; + private DirectionNavigationActor.StateReader directionNavigationActorStateReader; + private FullScreenReadActor fullScreenReadActor; + private EditTextActionHistory editTextActionHistory; - private PassThroughModeActor passThroughModeActor; - private GlobalVariables globalVariables; - private EventFilter eventFilter; - private TextEventInterpreter textEventInterpreter; - private Compositor compositor; - private DirectionNavigationActor.StateReader directionNavigationActorStateReader; - private FullScreenReadActor fullScreenReadActor; - private EditTextActionHistory editTextActionHistory; + /** + * Interface for monitoring current and previous cursor position in editable node + */ + private TextCursorTracker textCursorTracker; - /** Interface for monitoring current and previous cursor position in editable node */ - private TextCursorTracker textCursorTracker; + /** + * Monitors the call state for the phone device. + */ + private CallStateMonitor callStateMonitor; - /** Monitors the call state for the phone device. */ - private CallStateMonitor callStateMonitor; + /** + * Monitors voice actions from other applications + */ + private VoiceActionMonitor voiceActionMonitor; - /** Monitors voice actions from other applications */ - private VoiceActionMonitor voiceActionMonitor; + /** + * Monitors speech actions from other applications + */ + private SpeechStateMonitor speechStateMonitor; - /** Monitors speech actions from other applications */ - private SpeechStateMonitor speechStateMonitor; + /** + * Maintains cursor state during explore-by-touch by working around EBT problems. + */ + private ProcessorCursorState processorCursorState; - /** Maintains cursor state during explore-by-touch by working around EBT problems. */ - private ProcessorCursorState processorCursorState; + /** + * Processor for allowing clicking on buttons in permissions dialogs. + */ + private ProcessorPermissionDialogs processorPermissionsDialogs; - /** Processor for allowing clicking on buttons in permissions dialogs. */ - private ProcessorPermissionDialogs processorPermissionsDialogs; + /** + * Controller for manage keyboard commands + */ + private KeyComboManager keyComboManager; - /** Controller for manage keyboard commands */ - private KeyComboManager keyComboManager; + /** + * Manager for showing radial menus. + */ + private ListMenuManager menuManager; - /** Manager for showing radial menus. */ - private ListMenuManager menuManager; + /** + * Manager for handling custom labels. + */ + private CustomLabelManager labelManager; - /** Manager for handling custom labels. */ - private CustomLabelManager labelManager; + /** + * Manager for the screen search feature. + */ + private UniversalSearchManager universalSearchManager; - /** Manager for the screen search feature. */ - private UniversalSearchManager universalSearchManager; + /** + * Orientation monitor for watching orientation changes. + */ + private OrientationMonitor orientationMonitor; - /** Orientation monitor for watching orientation changes. */ - private OrientationMonitor orientationMonitor; + /** + * {@link BroadcastReceiver} for tracking the ringer and screen states. + */ + private RingerModeAndScreenMonitor ringerModeAndScreenMonitor; - /** {@link BroadcastReceiver} for tracking the ringer and screen states. */ - private RingerModeAndScreenMonitor ringerModeAndScreenMonitor; + /** + * {@link BroadcastReceiver} for tracking volume changes. + */ + private VolumeMonitor volumeMonitor; - /** {@link BroadcastReceiver} for tracking volume changes. */ - private VolumeMonitor volumeMonitor; + /** + * {@link android.content.BroadcastReceiver} for tracking battery status changes. + */ + private BatteryMonitor batteryMonitor; - /** {@link android.content.BroadcastReceiver} for tracking battery status changes. */ - private BatteryMonitor batteryMonitor; + /** + * {@link BroadcastReceiver} for tracking headphone connected status changes. + */ + private HeadphoneStateMonitor headphoneStateMonitor; - /** {@link BroadcastReceiver} for tracking headphone connected status changes. */ - private HeadphoneStateMonitor headphoneStateMonitor; + /** + * Tracks changes to audio output and provides information on what types of audio are playing. + */ + private AudioPlaybackMonitor audioPlaybackMonitor; - /** Tracks changes to audio output and provides information on what types of audio are playing. */ - private AudioPlaybackMonitor audioPlaybackMonitor; + /** + * Manages screen dimming + */ + private DimScreenActor dimScreenController; - /** Manages screen dimming */ - private DimScreenActor dimScreenController; + /** + * The television controller; non-null if the device is a television (Android TV). + */ + private TelevisionNavigationController televisionNavigationController; - /** The television controller; non-null if the device is a television (Android TV). */ - private TelevisionNavigationController televisionNavigationController; + private TelevisionDPadManager televisionDPadManager; - private TelevisionDPadManager televisionDPadManager; + /** + * {@link BroadcastReceiver} for tracking package removals for custom label data consistency. + */ + private PackageRemovalReceiver packageReceiver; - /** {@link BroadcastReceiver} for tracking package removals for custom label data consistency. */ - private PackageRemovalReceiver packageReceiver; + /** + * The analytics instance, used for sending data to Google Analytics. + */ + private TalkBackAnalyticsImpl analytics; - /** The analytics instance, used for sending data to Google Analytics. */ - private TalkBackAnalyticsImpl analytics; + /** + * Callback to be invoked when fingerprint gestures are being used for accessibility. + */ + private FingerprintGestureCallback fingerprintGestureCallback; - /** Callback to be invoked when fingerprint gestures are being used for accessibility. */ - private FingerprintGestureCallback fingerprintGestureCallback; + /** + * Controller for the selector + */ + private SelectorController selectorController; - /** Controller for the selector */ - private SelectorController selectorController; + /** + * Controller for handling gestures + */ + private GestureController gestureController; - /** Controller for handling gestures */ - private GestureController gestureController; + /** + * Speech recognition wrapper for voice commands + */ + private SpeechRecognizerActor speechRecognizer; - /** Speech recognition wrapper for voice commands */ - private SpeechRecognizerActor speechRecognizer; + /** + * Processor for voice commands + */ + private VoiceCommandProcessor voiceCommandProcessor; - /** Processor for voice commands */ - private VoiceCommandProcessor voiceCommandProcessor; + /** + * Shared preferences used within TalkBack. + */ + private SharedPreferences prefs; - /** Shared preferences used within TalkBack. */ - private SharedPreferences prefs; + /** + * The system's uncaught exception handler + */ + private UncaughtExceptionHandler systemUeh; - /** The system's uncaught exception handler */ - private UncaughtExceptionHandler systemUeh; + /** + * The system feature if the device supports touch screen + */ + private boolean supportsTouchScreen = true; - /** The system feature if the device supports touch screen */ - private boolean supportsTouchScreen = true; + /** + * Whether the current root node is dirty or not. + */ + private boolean isRootNodeDirty = true; + /** + * Keep Track of current root node. + */ + private AccessibilityNodeInfo rootNode; - /** Whether the current root node is dirty or not. */ - private boolean isRootNodeDirty = true; - /** Keep Track of current root node. */ - private AccessibilityNodeInfo rootNode; + private AccessibilityEventProcessor accessibilityEventProcessor; - private AccessibilityEventProcessor accessibilityEventProcessor; + /** + * Keeps track of whether we need to run the locked-boot-completed callback when connected. + */ + private boolean lockedBootCompletedPending; + + private final InputModeManager inputModeManager = new InputModeManager(); + private ProcessorAccessibilityHints processorHints; + private ProcessorScreen processorScreen; + @Nullable + private ProcessorMagnification processorMagnification; + private final DisableTalkBackCompleteAction disableTalkBackCompleteAction = + new DisableTalkBackCompleteAction(); + private SpeakPasswordsManager speakPasswordsManager; + + // Focus logic + private AccessibilityFocusMonitor accessibilityFocusMonitor; + private AccessibilityFocusInterpreter accessibilityFocusInterpreter; + private FocusActor focuser; + private InputFocusInterpreter inputFocusInterpreter; + private ScrollPositionInterpreter scrollPositionInterpreter; + private ScreenStateMonitor screenStateMonitor; + private ProcessorEventQueue processorEventQueue; + private ProcessorPhoneticLetters processorPhoneticLetters; - /** Keeps track of whether we need to run the locked-boot-completed callback when connected. */ - private boolean lockedBootCompletedPending; + /** + * A reference to the active Braille IME if any. + */ + private @Nullable BrailleImeForTalkBack brailleImeForTalkBack; - private final InputModeManager inputModeManager = new InputModeManager(); - private ProcessorAccessibilityHints processorHints; - private ProcessorScreen processorScreen; - @Nullable private ProcessorMagnification processorMagnification; - private final DisableTalkBackCompleteAction disableTalkBackCompleteAction = - new DisableTalkBackCompleteAction(); - private SpeakPasswordsManager speakPasswordsManager; - - // Focus logic - private AccessibilityFocusMonitor accessibilityFocusMonitor; - private AccessibilityFocusInterpreter accessibilityFocusInterpreter; - private FocusActor focuser; - private InputFocusInterpreter inputFocusInterpreter; - private ScrollPositionInterpreter scrollPositionInterpreter; - private ScreenStateMonitor screenStateMonitor; - private ProcessorEventQueue processorEventQueue; - private ProcessorPhoneticLetters processorPhoneticLetters; - - /** A reference to the active Braille IME if any. */ - private @Nullable BrailleImeForTalkBack brailleImeForTalkBack; - - private BrailleDisplayForTalkBack brailleDisplay; - - private GestureShortcutMapping gestureShortcutMapping; - private NodeMenuRuleProcessor nodeMenuRuleProcessor; - private PrimesController primesController; - private SpeechLanguage speechLanguage; - private boolean isBrailleKeyboardActivated; - private ImageCaptioner imageCaptioner; - private ImageContents imageContents; - private @Nullable Boolean useServiceGestureDetection; - - private final @NonNull Map displayIdToTouchInteractionMonitor = - new HashMap<>(); - - @Override - public void onCreate() { - super.onCreate(); - - this.setTheme(R.style.TalkbackBaseTheme); - - instance = this; - setServiceState(ServiceStateListener.SERVICE_STATE_INACTIVE); - - systemUeh = Thread.getDefaultUncaughtExceptionHandler(); - Thread.setDefaultUncaughtExceptionHandler(this); - } - - /** - * Calculates the volume for {@link SpeechControllerImpl#setSpeechVolume(float)} when announcing - * "TalkBack off". - * - *

TalkBack switches to use {@link AudioManager#STREAM_ACCESSIBILITY} from Android O. However, - * when announcing "TalkBack off" before turning TalkBack off, the audio goes through {@link - * AudioManager#STREAM_MUSIC}. It's because accessibility stream has already been shut down before - * {@link #onUnbind(Intent)} is called. - * - *

To work around this issue, it's not recommended to directly override media stream volume. - * Instead, we can adjust the relative TTS volume to match the original accessibility stream - * volume. - * - * @return TTS volume in [0.0f, 1.0f]. - */ - private float calculateFinalAnnouncementVolume() { - if (!FeatureSupport.hasAccessibilityAudioStream(this)) { - return 1.0f; - } - AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - - int musicStreamVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); - int musicStreamMaxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); - int accessibilityStreamVolume = - (volumeMonitor == null) ? -1 : volumeMonitor.getCachedAccessibilityStreamVolume(); - int accessibilityStreamMaxVolume = - (volumeMonitor == null) ? -1 : volumeMonitor.getCachedAccessibilityMaxVolume(); - if (musicStreamVolume <= 0 - || musicStreamMaxVolume <= 0 - || accessibilityStreamVolume < 0 - || accessibilityStreamMaxVolume <= 0) { - // Do not adjust volume if music stream is muted, or when any volume is invalid. - return 1.0f; - } - if (accessibilityStreamVolume == 0) { - return 0.0f; - } - - // Depending on devices/API level, a stream might have 7 steps or 15 steps adjustment. - // We need to normalize the values to eliminate this difference. - float musicVolumeFraction = (float) musicStreamVolume / musicStreamMaxVolume; - float accessibilityVolumeFraction = - (float) accessibilityStreamVolume / accessibilityStreamMaxVolume; - if (musicVolumeFraction <= accessibilityVolumeFraction) { - // Do not adjust volume when a11y stream volume is louder than music stream volume. - return 1.0f; - } - - // AudioManager measures the volume in dB scale, while TTS measures it in linear scale. We need - // to apply exponential operation to map dB/logarithmic-scaled diff value into linear-scaled - // multiplier value. - // The dB scaling could be different based on devices/OEMs/streams, which is not under our - // control. - // What we can do is to try our best to adjust the volume and avoid sudden volume increase. - // TODO: The parameters in Math.pow() are results from experiments. Feel free to change - // them. - return (float) Math.pow(10.0f, (accessibilityVolumeFraction - musicVolumeFraction) / 0.4f); - } - - @Override - public boolean onUnbind(Intent intent) { - final long turningOffTime = System.currentTimeMillis(); - interruptAllFeedback(false /* stopTtsSpeechCompletely */); - if (pipeline != null) { - pipeline.onUnbind(calculateFinalAnnouncementVolume(), disableTalkBackCompleteAction); - } - if (gestureShortcutMapping != null) { - gestureShortcutMapping.onUnbind(); - } - while (true) { - synchronized (disableTalkBackCompleteAction) { - try { - disableTalkBackCompleteAction.wait(TURN_OFF_WAIT_PERIOD_MS); - } catch (InterruptedException e) { - // Do nothing + private BrailleDisplayForTalkBack brailleDisplay; + + private GestureShortcutMapping gestureShortcutMapping; + private NodeMenuRuleProcessor nodeMenuRuleProcessor; + private PrimesController primesController; + private SpeechLanguage speechLanguage; + private boolean isBrailleKeyboardActivated; + private ImageCaptioner imageCaptioner; + private ImageContents imageContents; + private @Nullable Boolean useServiceGestureDetection; + + private final @NonNull Map displayIdToTouchInteractionMonitor = + new HashMap<>(); + + @Override + public void onCreate() { + super.onCreate(); + + this.setTheme(R.style.TalkbackBaseTheme); + + instance = this; + setServiceState(ServiceStateListener.SERVICE_STATE_INACTIVE); + + systemUeh = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(this); + } + + /** + * Calculates the volume for {@link SpeechControllerImpl#setSpeechVolume(float)} when announcing + * "TalkBack off". + * + *

TalkBack switches to use {@link AudioManager#STREAM_ACCESSIBILITY} from Android O. However, + * when announcing "TalkBack off" before turning TalkBack off, the audio goes through {@link + * AudioManager#STREAM_MUSIC}. It's because accessibility stream has already been shut down before + * {@link #onUnbind(Intent)} is called. + * + *

To work around this issue, it's not recommended to directly override media stream volume. + * Instead, we can adjust the relative TTS volume to match the original accessibility stream + * volume. + * + * @return TTS volume in [0.0f, 1.0f]. + */ + private float calculateFinalAnnouncementVolume() { + if (!FeatureSupport.hasAccessibilityAudioStream(this)) { + return 1.0f; } - if (System.currentTimeMillis() - turningOffTime > TURN_OFF_TIMEOUT_MS - || disableTalkBackCompleteAction.isDone) { - break; + AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + + int musicStreamVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); + int musicStreamMaxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + int accessibilityStreamVolume = + (volumeMonitor == null) ? -1 : volumeMonitor.getCachedAccessibilityStreamVolume(); + int accessibilityStreamMaxVolume = + (volumeMonitor == null) ? -1 : volumeMonitor.getCachedAccessibilityMaxVolume(); + if (musicStreamVolume <= 0 + || musicStreamMaxVolume <= 0 + || accessibilityStreamVolume < 0 + || accessibilityStreamMaxVolume <= 0) { + // Do not adjust volume if music stream is muted, or when any volume is invalid. + return 1.0f; } - } + if (accessibilityStreamVolume == 0) { + return 0.0f; + } + + // Depending on devices/API level, a stream might have 7 steps or 15 steps adjustment. + // We need to normalize the values to eliminate this difference. + float musicVolumeFraction = (float) musicStreamVolume / musicStreamMaxVolume; + float accessibilityVolumeFraction = + (float) accessibilityStreamVolume / accessibilityStreamMaxVolume; + if (musicVolumeFraction <= accessibilityVolumeFraction) { + // Do not adjust volume when a11y stream volume is louder than music stream volume. + return 1.0f; + } + + // AudioManager measures the volume in dB scale, while TTS measures it in linear scale. We need + // to apply exponential operation to map dB/logarithmic-scaled diff value into linear-scaled + // multiplier value. + // The dB scaling could be different based on devices/OEMs/streams, which is not under our + // control. + // What we can do is to try our best to adjust the volume and avoid sudden volume increase. + // TODO: The parameters in Math.pow() are results from experiments. Feel free to change + // them. + return (float) Math.pow(10.0f, (accessibilityVolumeFraction - musicVolumeFraction) / 0.4f); } - // Resume animation if necessary. - enableAnimation(/* enable= */ true); - return false; - } - @Override - public void onDestroy() { - if (shouldUseTalkbackGestureDetection()) { - unregisterGestureDetection(); + @Override + public boolean onUnbind(Intent intent) { + final long turningOffTime = System.currentTimeMillis(); + interruptAllFeedback(false /* stopTtsSpeechCompletely */); + if (pipeline != null) { + pipeline.onUnbind(calculateFinalAnnouncementVolume(), disableTalkBackCompleteAction); + } + if (gestureShortcutMapping != null) { + gestureShortcutMapping.onUnbind(); + } + while (true) { + synchronized (disableTalkBackCompleteAction) { + try { + disableTalkBackCompleteAction.wait(TURN_OFF_WAIT_PERIOD_MS); + } catch (InterruptedException e) { + // Do nothing + } + if (System.currentTimeMillis() - turningOffTime > TURN_OFF_TIMEOUT_MS + || disableTalkBackCompleteAction.isDone) { + break; + } + } + } + // Resume animation if necessary. + enableAnimation(/* enable= */ true); + return false; } - if (passThroughModeActor != null) { - passThroughModeActor.onDestroy(); + @Override + public void onDestroy() { + // INFO: TalkBack For Developers modification + AdbReceiver.unregisterAdbReceiver(this); + // ------------------------------------------ + if (shouldUseTalkbackGestureDetection()) { + unregisterGestureDetection(); + } + + if (passThroughModeActor != null) { + passThroughModeActor.onDestroy(); + } + super.onDestroy(); + + SharedKeyEvent.unregister(this); + + if (isServiceActive()) { + suspendInfrastructure(); + } + + instance = null; + + // Shutdown and unregister all components. + shutdownInfrastructure(); + setServiceState(ServiceStateListener.SERVICE_STATE_INACTIVE); + serviceStateListeners.clear(); + if (televisionNavigationController != null) { + televisionNavigationController.onDestroy(); + } } - super.onDestroy(); - SharedKeyEvent.unregister(this); + @Override + public void onConfigurationChanged(Configuration newConfig) { + this.getTheme().applyStyle(R.style.TalkbackBaseTheme, /* force= */ true); + + // onConfigurationChanged may be called before TalkBack initialization. To avoid crash, each + // listener should checks the instance is null or not. + if (universalSearchManager != null) { + pipeline + .getFeedbackReturner() + .returnFeedback(EVENT_ID_UNTRACKED, Feedback.renewOverlay(newConfig)); + } + + if (isServiceActive() && (orientationMonitor != null)) { + orientationMonitor.onConfigurationChanged(newConfig); + } + + if (gestureShortcutMapping != null) { + gestureShortcutMapping.onConfigurationChanged(newConfig); + } - if (isServiceActive()) { - suspendInfrastructure(); + if (pipeline != null) { + resetTouchExplorePassThrough(); + pipeline + .getFeedbackReturner() + .returnFeedback( + EVENT_ID_UNTRACKED, Feedback.deviceInfo(Action.CONFIG_CHANGED, newConfig)); + } } - instance = null; + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + Performance perf = Performance.getInstance(); + EventId eventId = perf.onEventReceived(event); + accessibilityEventProcessor.onAccessibilityEvent(event, eventId); + perf.onHandlerDone(eventId); + + if (brailleDisplay != null) { + brailleDisplay.onAccessibilityEvent(event); + } + + // Re-apply diagnosis-mode logging, in case other accessibility-services changed the shared + // log-level preference. + enforceDiagnosisModeLogging(); - // Shutdown and unregister all components. - shutdownInfrastructure(); - setServiceState(ServiceStateListener.SERVICE_STATE_INACTIVE); - serviceStateListeners.clear(); - if (televisionNavigationController != null) { - televisionNavigationController.onDestroy(); + if (diagnosticOverlayController != null) { + diagnosticOverlayController.displayEvent(event); + } } - } - @Override - public void onConfigurationChanged(Configuration newConfig) { - this.getTheme().applyStyle(R.style.TalkbackBaseTheme, /* force= */ true); + public boolean supportsTouchScreen() { + return supportsTouchScreen; + } - // onConfigurationChanged may be called before TalkBack initialization. To avoid crash, each - // listener should checks the instance is null or not. - if (universalSearchManager != null) { - pipeline - .getFeedbackReturner() - .returnFeedback(EVENT_ID_UNTRACKED, Feedback.renewOverlay(newConfig)); + @Override + public @Nullable AccessibilityNodeInfo getRootInActiveWindow() { + if (isRootNodeDirty || rootNode == null) { + rootNode = super.getRootInActiveWindow(); + isRootNodeDirty = false; + } + return rootNode == null ? null : AccessibilityNodeInfo.obtain(rootNode); } - if (isServiceActive() && (orientationMonitor != null)) { - orientationMonitor.onConfigurationChanged(newConfig); + public void setRootDirty(boolean rootIsDirty) { + isRootNodeDirty = rootIsDirty; } - if (gestureShortcutMapping != null) { - gestureShortcutMapping.onConfigurationChanged(newConfig); + private void setServiceState(int newState) { + if (serviceState == newState) { + return; + } + + serviceState = newState; + for (ServiceStateListener listener : serviceStateListeners) { + listener.onServiceStateChanged(newState); + } + } + + public void addServiceStateListener(ServiceStateListener listener) { + if (listener != null) { + serviceStateListeners.add(listener); + } } - if (pipeline != null) { - resetTouchExplorePassThrough(); - pipeline - .getFeedbackReturner() - .returnFeedback( - EVENT_ID_UNTRACKED, Feedback.deviceInfo(Action.CONFIG_CHANGED, newConfig)); + public void removeServiceStateListener(ServiceStateListener listener) { + if (listener != null) { + serviceStateListeners.remove(listener); + } } - } - @Override - public void onAccessibilityEvent(AccessibilityEvent event) { - Performance perf = Performance.getInstance(); - EventId eventId = perf.onEventReceived(event); - accessibilityEventProcessor.onAccessibilityEvent(event, eventId); - perf.onHandlerDone(eventId); + /** + * Stops all delayed events in the service. + */ + public void clearQueues() { + interruptAllFeedback(/* stopTtsSpeechCompletely= */ false); + processorEventQueue.clearQueue(); + if (processorScreen != null && processorScreen.getWindowEventInterpreter() != null) { + processorScreen.getWindowEventInterpreter().clearQueue(); + } + // TODO: Clear queues wherever there are message handlers that delay event processing. + } - if (brailleDisplay != null) { - brailleDisplay.onAccessibilityEvent(event); + private boolean shouldInterruptByAnyKeyEvent() { + return !fullScreenReadActor.isActive(); } - // Re-apply diagnosis-mode logging, in case other accessibility-services changed the shared - // log-level preference. - enforceDiagnosisModeLogging(); + /** + * 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. + */ + @Override + public boolean onKeyEventShared(KeyEvent keyEvent) { + if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) { + // Tapping on fingerprint sensor somehow files KeyEvent with KEYCODE_UNKNOWN, which will + // change input mode to keyboard, and cancel pending accessibility hints. It is OK to just + // ignore these KeyEvents since they're unused in TalkBack. + return false; + } + if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { + textEventInterpreter.setLastKeyEventTime(keyEvent.getEventTime()); + } + Performance perf = Performance.getInstance(); + EventId eventId = perf.onEventReceived(keyEvent); + + if (isServiceActive()) { + // Stop the TTS engine when any key (except for volume up/down key) is pressed on physical + // keyboard. + if (shouldInterruptByAnyKeyEvent() + && keyEvent.getDeviceId() != 0 + && keyEvent.getAction() == KeyEvent.ACTION_DOWN + && keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_DOWN + && keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_UP) { + interruptAllFeedback(false /* stopTtsSpeechCompletely */); + } + } + + for (ServiceKeyEventListener listener : keyEventListeners) { + if (!isServiceActive() && !listener.processWhenServiceSuspended()) { + continue; + } + + if (listener.onKeyEvent(keyEvent, eventId)) { + perf.onHandlerDone(eventId); + return true; + } + } + + return false; + } + + @Override + protected boolean onKeyEvent(KeyEvent keyEvent) { + return SharedKeyEvent.onKeyEvent(this, keyEvent); + } + + @Override + protected boolean onGesture(int gestureId) { + return handleOnGestureById(gestureId); + } - if (diagnosticOverlayController != null) { - diagnosticOverlayController.displayEvent(event); + @Override + public boolean onGesture(AccessibilityGestureEvent accessibilityGestureEvent) { + if (handleOnGestureById(accessibilityGestureEvent.getGestureId())) { + pipeline + .getFeedbackReturner() + .returnFeedback( + Performance.EVENT_ID_UNTRACKED, Feedback.saveGesture(accessibilityGestureEvent)); + return true; + } + return false; } - } - public boolean supportsTouchScreen() { - return supportsTouchScreen; - } + private boolean handleOnGestureById(int gestureId) { + if (!isServiceActive()) { + return false; + } + Performance perf = Performance.getInstance(); + EventId eventId = perf.onGestureEventReceived(gestureId); + primesController.startTimer(Timer.GESTURE_EVENT); + + analytics.onGesture(gestureId); + feedbackController.playAuditory(R.raw.gesture_end, eventId); + + gestureController.onGesture(gestureId, eventId); - @Override - public @Nullable AccessibilityNodeInfo getRootInActiveWindow() { - if (isRootNodeDirty || rootNode == null) { - rootNode = super.getRootInActiveWindow(); - isRootNodeDirty = false; + // Measure latency. + // Preceding event handling frequently initiates a framework action, which in turn + // cascades a focus event, which in turn generates feedback. + perf.onHandlerDone(eventId); + primesController.stopTimer(Timer.GESTURE_EVENT); + return true; } - return rootNode == null ? null : AccessibilityNodeInfo.obtain(rootNode); - } - public void setRootDirty(boolean rootIsDirty) { - isRootNodeDirty = rootIsDirty; - } + public GestureController getGestureController() { + if (gestureController == null) { + throw new RuntimeException("mGestureController has not been initialized"); + } - private void setServiceState(int newState) { - if (serviceState == newState) { - return; + return gestureController; } - serviceState = newState; - for (ServiceStateListener listener : serviceStateListeners) { - listener.onServiceStateChanged(newState); + // TODO: As controller logic moves to pipeline, delete this function. + public SpeechControllerImpl getSpeechController() { + if (speechController == null) { + throw new RuntimeException("mSpeechController has not been initialized"); + } + + return speechController; } - } - public void addServiceStateListener(ServiceStateListener listener) { - if (listener != null) { - serviceStateListeners.add(listener); + public FeedbackController getFeedbackController() { + if (feedbackController == null) { + throw new RuntimeException("mFeedbackController has not been initialized"); + } + + return feedbackController; } - } - public void removeServiceStateListener(ServiceStateListener listener) { - if (listener != null) { - serviceStateListeners.remove(listener); + public VoiceActionMonitor getVoiceActionMonitor() { + if (voiceActionMonitor == null) { + throw new RuntimeException("mVoiceActionMonitor has not been initialized"); + } + + return voiceActionMonitor; } - } - /** Stops all delayed events in the service. */ - public void clearQueues() { - interruptAllFeedback(/* stopTtsSpeechCompletely= */ false); - processorEventQueue.clearQueue(); - if (processorScreen != null && processorScreen.getWindowEventInterpreter() != null) { - processorScreen.getWindowEventInterpreter().clearQueue(); + public KeyComboManager getKeyComboManager() { + return keyComboManager; } - // TODO: Clear queues wherever there are message handlers that delay event processing. - } - private boolean shouldInterruptByAnyKeyEvent() { - return !fullScreenReadActor.isActive(); - } + public CustomLabelManager getLabelManager() { + if (labelManager == null) { + throw new RuntimeException("mLabelManager has not been initialized"); + } - /** - * 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. - */ - @Override - public boolean onKeyEventShared(KeyEvent keyEvent) { - if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) { - // Tapping on fingerprint sensor somehow files KeyEvent with KEYCODE_UNKNOWN, which will - // change input mode to keyboard, and cancel pending accessibility hints. It is OK to just - // ignore these KeyEvents since they're unused in TalkBack. - return false; + return labelManager; } - if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { - textEventInterpreter.setLastKeyEventTime(keyEvent.getEventTime()); - } - Performance perf = Performance.getInstance(); - EventId eventId = perf.onEventReceived(keyEvent); - - if (isServiceActive()) { - // Stop the TTS engine when any key (except for volume up/down key) is pressed on physical - // keyboard. - if (shouldInterruptByAnyKeyEvent() - && keyEvent.getDeviceId() != 0 - && keyEvent.getAction() == KeyEvent.ACTION_DOWN - && keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_DOWN - && keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_UP) { - interruptAllFeedback(false /* stopTtsSpeechCompletely */); - } + + public TalkBackAnalyticsImpl getAnalytics() { + if (analytics == null) { + throw new RuntimeException("mAnalytics has not been initialized"); + } + + return analytics; } - for (ServiceKeyEventListener listener : keyEventListeners) { - if (!isServiceActive() && !listener.processWhenServiceSuspended()) { - continue; - } + /** + * Obtains the shared instance of TalkBack's {@link TelevisionNavigationController} if the current + * device is a television. Otherwise returns {@code null}. + */ + public TelevisionNavigationController getTelevisionNavigationController() { + return televisionNavigationController; + } - if (listener.onKeyEvent(keyEvent, eventId)) { - perf.onHandlerDone(eventId); - return true; - } + @VisibleForTesting + public TextCursorTracker getTextCursorTracker() { + return textCursorTracker; } - return false; - } + @VisibleForTesting + public RingerModeAndScreenMonitor getRingerModeAndScreenMonitor() { + return ringerModeAndScreenMonitor; + } - @Override - protected boolean onKeyEvent(KeyEvent keyEvent) { - return SharedKeyEvent.onKeyEvent(this, keyEvent); - } + @VisibleForTesting + public GlobalVariables getGlobalVariables() { + return globalVariables; + } - @Override - protected boolean onGesture(int gestureId) { - return handleOnGestureById(gestureId); - } + @VisibleForTesting + public ProcessorScreen getProcessorScreen() { + return processorScreen; + } - @Override - public boolean onGesture(AccessibilityGestureEvent accessibilityGestureEvent) { - if (handleOnGestureById(accessibilityGestureEvent.getGestureId())) { - pipeline - .getFeedbackReturner() - .returnFeedback( - Performance.EVENT_ID_UNTRACKED, Feedback.saveGesture(accessibilityGestureEvent)); - return true; + /** + * Registers the dialog to {@link RingerModeAndScreenMonitor} for screen monitor. + */ + public void registerDialog(DialogInterface dialog) { + if (ringerModeAndScreenMonitor != null) { + ringerModeAndScreenMonitor.registerDialog(dialog); + } } - return false; - } - private boolean handleOnGestureById(int gestureId) { - if (!isServiceActive()) { - return false; + /** + * Unregisters the dialog from {@link RingerModeAndScreenMonitor} for screen monitor. + */ + public void unregisterDialog(DialogInterface dialog) { + if (ringerModeAndScreenMonitor != null) { + ringerModeAndScreenMonitor.unregisterDialog(dialog); + } } - Performance perf = Performance.getInstance(); - EventId eventId = perf.onGestureEventReceived(gestureId); - primesController.startTimer(Timer.GESTURE_EVENT); - analytics.onGesture(gestureId); - feedbackController.playAuditory(R.raw.gesture_end, eventId); + @Override + public void onInterrupt() { + if (processorScreen != null && FeatureSupport.isArc()) { + // In Arc, we consider that focus goes out from Arc when onInterrupt is called. + processorScreen.clearScreenState(); + } + interruptAllFeedback(false /* stopTtsSpeechCompletely */); + } - gestureController.onGesture(gestureId, eventId); + @Override + public boolean isAudioPlaybackActive() { + return voiceActionMonitor.isAudioPlaybackActive(); + } - // Measure latency. - // Preceding event handling frequently initiates a framework action, which in turn - // cascades a focus event, which in turn generates feedback. - perf.onHandlerDone(eventId); - primesController.stopTimer(Timer.GESTURE_EVENT); - return true; - } + @Override + public boolean isMicrophoneActiveAndHeadphoneOff() { + return voiceActionMonitor.isMicrophoneActiveAndHeadphoneOff(); + } - public GestureController getGestureController() { - if (gestureController == null) { - throw new RuntimeException("mGestureController has not been initialized"); + @Override + public boolean isSsbActiveAndHeadphoneOff() { + return voiceActionMonitor.isSsbActiveAndHeadphoneOff(); } - return gestureController; - } + @Override + public boolean isPhoneCallActive() { + return voiceActionMonitor.isPhoneCallActive(); + } - // TODO: As controller logic moves to pipeline, delete this function. - public SpeechControllerImpl getSpeechController() { - if (speechController == null) { - throw new RuntimeException("mSpeechController has not been initialized"); + @Override + public void onSpeakingForcedFeedback() { + voiceActionMonitor.onSpeakingForcedFeedback(); } - return speechController; - } + // Interrupts all Talkback feedback. Stops speech from other apps if stopTtsSpeechCompletely + // is true. + public void interruptAllFeedback(boolean stopTtsSpeechCompletely) { + + if (fullScreenReadActor != null) { + fullScreenReadActor.interrupt(); + } - public FeedbackController getFeedbackController() { - if (feedbackController == null) { - throw new RuntimeException("mFeedbackController has not been initialized"); + if (pipeline != null) { + pipeline.interruptAllFeedback(stopTtsSpeechCompletely); + } } - return feedbackController; - } + @Override + protected void onServiceConnected() { + LogUtils.v(TAG, "System bound to service."); + + // INFO: TalkBack For Developers modification + AdbReceiver.registerAdbReceiver(this); + // ------------------------------------------ + + primesController = new PrimesController(); + primesController.initialize(getApplication()); + primesController.startTimer(Timer.START_UP); + + SharedPreferencesUtils.migrateSharedPreferences(this); + prefs = SharedPreferencesUtils.getSharedPreferences(this); + + initializeInfrastructure(); + SharedKeyEvent.register(this); + + // Configure logs. + LogUtils.setTagPrefix("talkback: "); + LogUtils.setParameterCustomizer( + (object) -> { + if (object instanceof AccessibilityNodeInfoCompat) { + return AccessibilityNodeInfoUtils.toStringShort((AccessibilityNodeInfoCompat) object) + + " "; + } else if (object instanceof AccessibilityNodeInfo) { + return AccessibilityNodeInfoUtils.toStringShort((AccessibilityNodeInfo) object) + + " "; + } else if (object instanceof AccessibilityEvent) { + return AccessibilityEventUtils.toStringShort((AccessibilityEvent) object) + " "; + } else { + return object; + } + }); + + // The service must be connected before getFingerprintGestureController() is called, thus we + // cannot initialize fingerprint gesture detection in onCreate(). + initializeFingerprintGestureCallback(); + + resumeInfrastructure(); + + // Handle any update actions. + final TalkBackUpdateHelper helper = new TalkBackUpdateHelper(this); + helper.checkUpdate(); + + compositor.handleEvent(Compositor.EVENT_SPOKEN_FEEDBACK_ON, EVENT_ID_UNTRACKED); + + // If the locked-boot-completed intent was fired before onServiceConnected, we queued it, + // so now we need to run it. + if (lockedBootCompletedPending) { + onLockedBootCompletedInternal(); + lockedBootCompletedPending = false; + } + + // Shows tutorial or onboarding. + if (showTutorialIfNecessary()) { + // Avoids showing onboarding when user turns on TalkBack for the second time. + OnboardingInitiator.ignoreOnboarding(this); + return; + } + if (!FeatureSupport.isTv(getApplicationContext()) + && !FeatureSupport.isWatch(getApplicationContext())) { + OnboardingInitiator.showOnboardingIfNecessary(this); + } - public VoiceActionMonitor getVoiceActionMonitor() { - if (voiceActionMonitor == null) { - throw new RuntimeException("mVoiceActionMonitor has not been initialized"); + // Service gesture detection. + if (shouldUseTalkbackGestureDetection()) { + registerGestureDetection(); + } + + primesController.stopTimer(Timer.START_UP); } - return voiceActionMonitor; - } + /** + * @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 ServiceStateListener.SERVICE_STATE_INACTIVE; + } - public KeyComboManager getKeyComboManager() { - return keyComboManager; - } + return service.serviceState; + } - public CustomLabelManager getLabelManager() { - if (labelManager == null) { - throw new RuntimeException("mLabelManager has not been initialized"); + /** + * 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 serviceState == ServiceStateListener.SERVICE_STATE_ACTIVE; } - return labelManager; - } + /** + * @return {@code true} if TalkBack is running and initialized, {@code false} otherwise. + */ + public static boolean isServiceActive() { + return (getServiceState() == ServiceStateListener.SERVICE_STATE_ACTIVE); + } + + /** + * Returns the active TalkBack instance, or {@code null} if not available. + */ + public static @Nullable TalkBackService getInstance() { + return instance; + } - public TalkBackAnalyticsImpl getAnalytics() { - if (analytics == null) { - throw new RuntimeException("mAnalytics has not been initialized"); + /** + * Initialize {@link FingerprintGestureCallback} for detecting fingerprint gestures. + */ + private void initializeFingerprintGestureCallback() { + if (fingerprintGestureCallback != null || !FeatureSupport.isFingerprintGestureSupported(this)) { + return; + } + fingerprintGestureCallback = + new FingerprintGestureCallback() { + @Override + public void onGestureDetected(int gesture) { + if (isServiceActive() && gestureController != null) { + Performance perf = Performance.getInstance(); + EventId eventId = perf.onFingerprintGestureEventReceived(gesture); + + LogUtils.v(TAG, "Recognized fingerprint gesture %s", gesture); + + // TODO: Update analytics data. + // TODO: Check if we should dismiss radial menu. + feedbackController.playAuditory(R.raw.gesture_end, eventId); + + gestureController.onFingerprintGesture(gesture, eventId); + + // Measure latency. + // Preceding event handling frequently initiates a framework action, which in turn + // cascades a focus event, which in turn generates feedback. + perf.onHandlerDone(eventId); + } + } + + @Override + public void onGestureDetectionAvailabilityChanged(boolean available) { + LogUtils.v( + TAG, + "Fingerprint gesture detection is now " + + (available ? "available" : "unavailable") + + "."); + } + }; } - return analytics; - } + /** + * Initializes the controllers, managers, and processors. This should only be called once from + * {@link #onServiceConnected()}. + */ + private void initializeInfrastructure() { + // TODO: we still need it keep true for TV until TouchExplore and Accessibility focus is + // not unpaired + // supportsTouchScreen = packageManager.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); + + accessibilityEventProcessor = new AccessibilityEventProcessor(this); + feedbackController = new FeedbackController(this); + speechController = new SpeechControllerImpl(this, this, feedbackController); + speechStateMonitor = new SpeechStateMonitor(); + diagnosticOverlayController = new DiagnosticOverlayControllerImpl(this); + + gestureShortcutMapping = new GestureShortcutMapping(this); + + globalVariables = + new GlobalVariables(this, inputModeManager, keyComboManager, gestureShortcutMapping); + + labelManager = new CustomLabelManager(this); + addEventListener(labelManager); + + ImageCaptionStorage imageCaptionStorage = new ImageCaptionStorage(); + imageContents = + ImageCaptioner.supportsImageCaption(this) + ? new ImageContents(labelManager, imageCaptionStorage) + : new ImageContents(labelManager, /* imageCaptionStorage= */ null); + + compositor = + new Compositor( + this, + /* speechController= */ null, + imageContents, + globalVariables, + getCompositorFlavor()); + // TODO: Make pipeline run Compositor, which returns speech feedback, no callback. + + analytics = new TalkBackAnalyticsImpl(this); + + processorPhoneticLetters = new ProcessorPhoneticLetters(this); + + FocusFinder focusFinder = new FocusFinder(this); + + // Construct system-monitors. + batteryMonitor = new BatteryMonitor(this); + callStateMonitor = new CallStateMonitor(this); + audioPlaybackMonitor = new AudioPlaybackMonitor(this); + @NonNull TouchMonitor touchMonitor = new TouchMonitor(); + + // Construct event-interpreters. + AutoScrollInterpreter autoScrollInterpreter = new AutoScrollInterpreter(); + screenStateMonitor = new ScreenStateMonitor(/* service= */ this); + FullScreenReadInterpreter fullScreenReadInterpreter = new FullScreenReadInterpreter(); + scrollPositionInterpreter = new ScrollPositionInterpreter(); + ScrollEventInterpreter scrollEventInterpreter = + new ScrollEventInterpreter(audioPlaybackMonitor, touchMonitor); + ManualScrollInterpreter manualScrollInterpreter = new ManualScrollInterpreter(); + + // Constructor output-actor-state. + textCursorTracker = new TextCursorTracker(); + editTextActionHistory = new EditTextActionHistory(); + AccessibilityFocusActionHistory focusHistory = new AccessibilityFocusActionHistory(this); + + // Construct output-actors. + AutoScrollActor scroller = new AutoScrollActor(); + accessibilityFocusMonitor = + new AccessibilityFocusMonitor(this, focusFinder, focusHistory.reader); + + imageCaptioner = new ImageCaptioner(this, imageCaptionStorage, accessibilityFocusMonitor); + + // TODO: ScreenState should be passed through pipeline. + focuser = + new FocusActor( + this, focusFinder, screenStateMonitor.state, focusHistory, accessibilityFocusMonitor); + DirectionNavigationActor directionNavigationActor = + new DirectionNavigationActor( + inputModeManager, + globalVariables, + analytics, + compositor, + this, + focusFinder, + processorPhoneticLetters, + accessibilityFocusMonitor, + screenStateMonitor.state); + directionNavigationActorStateReader = directionNavigationActor.state; + TextEditActor editor = + new TextEditActor( + this, + editTextActionHistory, + textCursorTracker, + getSystemService(ClipboardManager.class)); + fullScreenReadActor = + new FullScreenReadActor(accessibilityFocusMonitor, this, speechController); + dimScreenController = new DimScreenActor(this, gestureShortcutMapping); + + accessibilityFocusInterpreter = + new AccessibilityFocusInterpreter( + this, accessibilityFocusMonitor, screenStateMonitor.state); + + inputFocusInterpreter = + new InputFocusInterpreter(accessibilityFocusInterpreter, focusFinder, globalVariables); + + proximitySensorListener = new ProximitySensorListener(/* service= */ this); + speechLanguage = new SpeechLanguage(); + + DirectionNavigationInterpreter directionNavigationInterpreter = + new DirectionNavigationInterpreter(this); + + processorHints = new ProcessorAccessibilityHints(); + addEventListener(processorHints); + keyEventListeners.add(0, processorHints); // Needs to be first; will not catch any events. + + passThroughModeActor = new PassThroughModeActor(this); + + selectorController = + new SelectorController( + this, accessibilityFocusMonitor, analytics, gestureShortcutMapping, processorHints); + + UniversalSearchActor universalSearchActor = + new UniversalSearchActor(this, screenStateMonitor.state, focusFinder, labelManager); + + autoScrollInterpreter.setUniversalSearchActor(universalSearchActor); + + voiceCommandProcessor = + new VoiceCommandProcessor(this, accessibilityFocusMonitor, selectorController, analytics); + speechRecognizer = new SpeechRecognizerActor(this, voiceCommandProcessor, analytics); + UiChangeEventInterpreter uiChangeEventInterpreter = new UiChangeEventInterpreter(); + addEventListener(uiChangeEventInterpreter); + + UserInterface userInterface = new UserInterface(selectorController); + + // Construct pipeline. + pipeline = + new Pipeline( + this, + new Monitors(batteryMonitor, callStateMonitor, touchMonitor, speechStateMonitor), + new Interpreters( + inputFocusInterpreter, + scrollEventInterpreter, + manualScrollInterpreter, + autoScrollInterpreter, + scrollPositionInterpreter, + accessibilityFocusInterpreter, + fullScreenReadInterpreter, + new StateChangeEventInterpreter(), + directionNavigationInterpreter, + processorHints, + voiceCommandProcessor, + new PassThroughModeInterpreter(), + new SubtreeChangeEventInterpreter(screenStateMonitor.state), + new AccessibilityEventIdleInterpreter(), + uiChangeEventInterpreter), + new Mappers(this, compositor, focusFinder), + new Actors( + this, + accessibilityFocusMonitor, + dimScreenController, + speechController, + fullScreenReadActor, + feedbackController, + scroller, + focuser, + new FocusActorForScreenStateChange(focusFinder, primesController), + new FocusActorForTapAndTouchExploration(), + directionNavigationActor, + new SearchScreenNodeStrategy(/* observer= */ null, labelManager), + editor, + labelManager, + new NodeActionPerformer(), + new SystemActionPerformer(this), + new LanguageActor(this, speechLanguage), + passThroughModeActor, + new TalkBackUIActor(this), + new SpeechRateActor(this), + new NumberAdjustor(this, accessibilityFocusMonitor), + new VolumeAdjustor(this), + speechRecognizer, + new GestureReporter(this, new GestureHistory()), + imageCaptioner, + universalSearchActor, + this::requestServiceFlag), + proximitySensorListener, + speechController, + diagnosticOverlayController, + compositor, + userInterface); + + processorHints.setActorState(pipeline.getActorState()); + processorHints.setPipeline(pipeline.getFeedbackReturner()); + + voiceCommandProcessor.setActorState(pipeline.getActorState()); + voiceCommandProcessor.setPipeline(pipeline.getFeedbackReturner()); + + accessibilityEventProcessor.setActorState(pipeline.getActorState()); + accessibilityEventProcessor.setAccessibilityEventIdleListener(pipeline); + + autoScrollInterpreter.setDirectionNavigationActor(directionNavigationActor); + + nodeMenuRuleProcessor = + new NodeMenuRuleProcessor( + this, pipeline.getFeedbackReturner(), pipeline.getActorState(), analytics); + compositor.setNodeMenuProvider(nodeMenuRuleProcessor); + + compositor.setSpeaker(pipeline.getSpeaker()); + + TouchExplorationInterpreter touchExplorationInterpreter = + new TouchExplorationInterpreter(inputModeManager); + + if (FeatureSupport.supportMagnificationController()) { + processorMagnification = + new ProcessorMagnification( + getMagnificationController(), + globalVariables, + compositor, + FeatureSupport.supportWindowMagnification(this)); + } + + // Register AccessibilityEventListeners + addEventListener(touchExplorationInterpreter); + addEventListener(directionNavigationInterpreter); + if (processorMagnification != null) { + addEventListener(processorMagnification); + } + addEventListener(pipeline); + + touchExplorationInterpreter.addTouchExplorationActionListener(accessibilityFocusInterpreter); + screenStateMonitor.addScreenStateChangeListener(accessibilityFocusInterpreter); + + screenStateMonitor.addScreenStateChangeListener(inputFocusInterpreter); + + voiceActionMonitor = new VoiceActionMonitor(this, callStateMonitor, speechStateMonitor); + accessibilityEventProcessor.setVoiceActionMonitor(voiceActionMonitor); + + keyEventListeners.add(inputModeManager); + + menuManager = + new ListMenuManager( + this, + pipeline.getFeedbackReturner(), + pipeline.getActorState(), + accessibilityFocusMonitor, + nodeMenuRuleProcessor, + analytics); + voiceCommandProcessor.setListMenuManager(menuManager); + + keyComboManager = + new KeyComboManager( + this, + pipeline.getFeedbackReturner(), + selectorController, + menuManager, + fullScreenReadActor); + + ringerModeAndScreenMonitor = + new RingerModeAndScreenMonitor( + menuManager, + pipeline.getFeedbackReturner(), + proximitySensorListener, + callStateMonitor, + this); + accessibilityEventProcessor.setRingerModeAndScreenMonitor(ringerModeAndScreenMonitor); + + // Only use speak-pass talkback-preference on android O+. + if (FeatureSupport.useSpeakPasswordsServicePref()) { + headphoneStateMonitor = new HeadphoneStateMonitor(this); + speakPasswordsManager = + new SpeakPasswordsManager(this, headphoneStateMonitor, globalVariables); + } + + ProcessorVolumeStream processorVolumeStream = + new ProcessorVolumeStream(pipeline.getActorState(), this); + addEventListener(processorVolumeStream); + keyEventListeners.add(processorVolumeStream); + + gestureController = + new GestureController( + this, + pipeline.getFeedbackReturner(), + pipeline.getActorState(), + menuManager, + selectorController, + accessibilityFocusMonitor, + gestureShortcutMapping); + + audioPlaybackMonitor = new AudioPlaybackMonitor(this); + + // Add event processors. These will process incoming AccessibilityEvents + // in the order they are added. + eventFilter = new EventFilter(compositor, this, touchMonitor, globalVariables); + eventFilter.setVoiceActionDelegate(voiceActionMonitor); + eventFilter.setAccessibilityFocusEventInterpreter(accessibilityFocusInterpreter); + ActorStateProvider actorStateProvider = + new ActorStateProvider() { + @Override + public boolean resettingNodeCursor() { + return globalVariables.resettingNodeCursor(); + } + }; + PreferenceProvider preferenceProvider = + new PreferenceProvider() { + @Override + public boolean shouldSpeakPasswords() { + return globalVariables.shouldSpeakPasswords(); + } + }; + textEventInterpreter = + new TextEventInterpreter( + this, + textCursorTracker, + directionNavigationActor.state, + inputModeManager, + new TextEventHistory(), + editTextActionHistory.provider, + actorStateProvider, + preferenceProvider, + voiceActionMonitor); + processorEventQueue = new ProcessorEventQueue(eventFilter, textEventInterpreter); + + addEventListener(processorEventQueue); + addEventListener(processorPhoneticLetters); + + // Create window event interpreter and announcer. + processorScreen = + new ProcessorScreen( + this, + processorHints, + keyComboManager, + focusFinder, + gestureShortcutMapping, + pipeline.getFeedbackReturner()); + globalVariables.setWindowsDelegate(processorScreen.getWindowEventInterpreter()); + screenStateMonitor.setWindowsDelegate(processorScreen.getWindowEventInterpreter()); + addEventListener(processorScreen); + + // Monitor window transition status by registering listeners. + if (processorScreen != null && processorScreen.getWindowEventInterpreter() != null) { + processorScreen.getWindowEventInterpreter().addListener(menuManager); + processorScreen.getWindowEventInterpreter().addListener(screenStateMonitor); + processorScreen.getWindowEventInterpreter().addListener(uiChangeEventInterpreter); + processorScreen.getWindowEventInterpreter().addListener(imageCaptioner); + } + + processorCursorState = + new ProcessorCursorState(this, pipeline.getFeedbackReturner(), globalVariables); + processorPermissionsDialogs = + new ProcessorPermissionDialogs( + this, pipeline.getActorState(), pipeline.getFeedbackReturner()); - /** - * Obtains the shared instance of TalkBack's {@link TelevisionNavigationController} if the current - * device is a television. Otherwise returns {@code null}. - */ - public TelevisionNavigationController getTelevisionNavigationController() { - return televisionNavigationController; - } + volumeMonitor = new VolumeMonitor(pipeline.getFeedbackReturner(), this, callStateMonitor); - @VisibleForTesting - public TextCursorTracker getTextCursorTracker() { - return textCursorTracker; - } + // TODO: Move this into the custom label manager code + packageReceiver = new PackageRemovalReceiver(); - @VisibleForTesting - public RingerModeAndScreenMonitor getRingerModeAndScreenMonitor() { - return ringerModeAndScreenMonitor; - } + addEventListener(new ProcessorGestureVibrator(pipeline.getFeedbackReturner())); - @VisibleForTesting - public GlobalVariables getGlobalVariables() { - return globalVariables; - } + universalSearchManager = + new UniversalSearchManager( + pipeline.getFeedbackReturner(), + ringerModeAndScreenMonitor, + processorScreen.getWindowEventInterpreter()); - @VisibleForTesting - public ProcessorScreen getProcessorScreen() { - return processorScreen; - } + keyEventListeners.add(keyComboManager); + serviceStateListeners.add(keyComboManager); + + orientationMonitor = new OrientationMonitor(compositor, this); + orientationMonitor.addOnOrientationChangedListener(dimScreenController); + + KeyboardLockMonitor keyboardLockMonitor = new KeyboardLockMonitor(compositor); + keyEventListeners.add(keyboardLockMonitor); + + if (Build.VERSION.SDK_INT >= TelevisionNavigationController.MIN_API_LEVEL + && FeatureSupport.isTv(this)) { + televisionNavigationController = + new TelevisionNavigationController( + this, accessibilityFocusMonitor, pipeline.getFeedbackReturner()); + keyEventListeners.add(televisionNavigationController); + televisionDPadManager = new TelevisionDPadManager(televisionNavigationController, this); + addEventListener(televisionDPadManager); + } - /** Registers the dialog to {@link RingerModeAndScreenMonitor} for screen monitor. */ - public void registerDialog(DialogInterface dialog) { - if (ringerModeAndScreenMonitor != null) { - ringerModeAndScreenMonitor.registerDialog(dialog); + brailleDisplay = new BrailleDisplay(this, talkBackForBrailleDisplay); + + BrailleIme.initialize( + this, talkBackForBrailleIme, brailleDisplay.getBrailleDisplayForBrailleIme()); + analytics.onTalkBackServiceStarted(); + } + + private final TalkBackForBrailleDisplay talkBackForBrailleDisplay = + new TalkBackForBrailleDisplay() { + @Override + public boolean performAction(ScreenReaderAction action, Object... args) { + // TODO: implement the screen reader actions. + if (action == ScreenReaderAction.OPEN_TALKBACK_MENU) { + return menuManager.showMenu(R.menu.context_menu, EVENT_ID_UNTRACKED); + } + return BrailleDisplayHelper.performAction( + getInstance(), pipeline.getFeedbackReturner(), action, args); + } + + @Override + public AccessibilityNodeInfoCompat getAccessibilityFocusNode(boolean fallbackOnRoot) { + return FocusFinder.getAccessibilityFocusNode(getInstance(), fallbackOnRoot); + } + + @Override + public FocusFinder createFocusFinder() { + return new FocusFinder(getInstance()); + } + + @Override + public boolean showLabelDialog(CustomLabelAction action, AccessibilityNodeInfoCompat node) { + if (action == CustomLabelAction.ADD_LABEL) { + return LabelDialogManager.addLabel( + getInstance(), + node.getViewIdResourceName(), + /* needToRestoreFocus= */ true, + pipeline.getFeedbackReturner()); + } else if (action == CustomLabelAction.EDIT_LABEL) { + return LabelDialogManager.editLabel( + getInstance(), + labelManager.getLabelForViewIdFromCache(node.getViewIdResourceName()).getId(), + /* needToRestoreFocus= */ true, + pipeline.getFeedbackReturner()); + } + return false; + } + + @Override + public CharSequence getCustomLabelText(AccessibilityNodeInfoCompat node) { + Label label = labelManager.getLabelForViewIdFromCache(node.getViewIdResourceName()); + if (label != null) { + return label.getText(); + } + return null; + } + + @Override + public boolean needsLabel(AccessibilityNodeInfoCompat node) { + return labelManager.needsLabel(node); + } + + @Override + public @Nullable BrailleImeForBrailleDisplay getBrailleImeForBrailleDisplay() { + return brailleImeForTalkBack == null + ? null + : brailleImeForTalkBack.getBrailleImeForBrailleDisplay(); + } + + @Override + public void speak(CharSequence textToSpeak, int delayMs, SpeakOptions speakOptions) { + pipeline + .getFeedbackReturner() + .returnFeedback( + Performance.EVENT_ID_UNTRACKED, + Feedback.speech(textToSpeak, speakOptions).setDelayMs(delayMs)); + } + }; + + private final TalkBackForBrailleIme talkBackForBrailleIme = + new TalkBackForBrailleIme() { + @Override + public void onBrailleImeActivated( + BrailleImeForTalkBack brailleImeForTalkBack, + boolean disableEbt, + boolean usePassThrough, + Region passThroughRegion) { + isBrailleKeyboardActivated = true; + TalkBackService.this.brailleImeForTalkBack = brailleImeForTalkBack; + if (usePassThrough) { + pipeline + .getFeedbackReturner() + .returnFeedback( + Performance.EVENT_ID_UNTRACKED, + Feedback.passThroughMode(LOCK_PASS_THROUGH, passThroughRegion)); + } else { + requestTouchExploration(!disableEbt); + } + } + + @Override + public void onBrailleImeInactivated(boolean usePassThrough) { + if (getServiceStatus() != ServiceStatus.ON) { + return; + } + isBrailleKeyboardActivated = false; + TalkBackService.this.brailleImeForTalkBack = null; + if (usePassThrough) { + pipeline + .getFeedbackReturner() + .returnFeedback( + Performance.EVENT_ID_UNTRACKED, + Feedback.passThroughMode(LOCK_PASS_THROUGH, null)); + } else { + boolean ebtEnabled = + getBooleanPref( + R.string.pref_explore_by_touch_key, R.bool.pref_explore_by_touch_default); + if (ebtEnabled) { + requestTouchExploration(true); + } + } + } + + @Override + public boolean setInputMethodEnabled() { + if (FeatureSupport.supportEnableDisableIme() && getInstance() != null) { + return getSoftKeyboardController() + .setInputMethodEnabled( + KeyboardUtils.getImeId(getInstance(), getPackageName()), + /* enabled= */ true) + == SoftKeyboardController.ENABLE_IME_SUCCESS; + } + return false; + } + + @Override + public WindowManager getWindowManager() { + return (WindowManager) getSystemService(Context.WINDOW_SERVICE); + } + + @Override + public ServiceStatus getServiceStatus() { + return isServiceActive() ? ServiceStatus.ON : ServiceStatus.OFF; + } + + @Override + public void speak(CharSequence textToSpeak, int delayMs, SpeakOptions speakOptions) { + // TODO: For uses cases where the timer is meant to re-schedule text, we + // should create a centralized repeat-feedback feature, and have BrailleIme use that. + pipeline + .getFeedbackReturner() + .returnFeedback( + Performance.EVENT_ID_UNTRACKED, + Feedback.speech(textToSpeak, speakOptions).setDelayMs(delayMs)); + } + + @Override + public void interruptSpeak() { + interruptAllFeedback(false); + } + + @Override + public void playSound(int resId, int delayMs) { + pipeline + .getFeedbackReturner() + .returnFeedback( + Performance.EVENT_ID_UNTRACKED, Feedback.sound(resId).setDelayMs(delayMs)); + } + + @Override + public void disableSilenceOnProximity() { + proximitySensorListener.setSilenceOnProximity(false); + } + + @Override + public void restoreSilenceOnProximity() { + reloadSilenceOnProximity(); + } + + @Override + public boolean isContextMenuExist() { + return menuManager.isMenuExist(); + } + + @Override + public boolean isVibrationFeedbackEnabled() { + return FeatureSupport.isVibratorSupported(getApplicationContext()) + && getBooleanPref(R.string.pref_vibration_key, R.bool.pref_vibration_default); + } + + @Override + public boolean shouldAnnounceCharacter() { + @KeyboardEchoType + int echoType = + brailleDisplay + .getBrailleDisplayForBrailleIme() + .isBrailleDisplayConnectedAndNotSuspended() + ? readPhysicalKeyboardEcho() + : readOnScreenKeyboardEcho(); + return echoType == PREF_ECHO_CHARACTERS || echoType == PREF_ECHO_CHARACTERS_AND_WORDS; + } + + @Override + public boolean shouldSpeakPassword() { + return globalVariables.shouldSpeakPasswords(); + } + + @Override + public boolean shouldUseCharacterGranularity() { + CursorGranularity granularity = + directionNavigationActorStateReader.getCurrentGranularity(); + return granularity == CursorGranularity.CHARACTER || !granularity.isMicroGranularity(); + } + + @Override + public void moveCursorForward() { + if (directionNavigationActorStateReader.getCurrentGranularity().isMicroGranularity()) { + selectorController.adjustSelectedSetting(EVENT_ID_UNTRACKED, /* isNext= */ true); + } + } + + @Override + public void moveCursorBackward() { + if (directionNavigationActorStateReader.getCurrentGranularity().isMicroGranularity()) { + selectorController.adjustSelectedSetting(EVENT_ID_UNTRACKED, /* isNext= */ false); + } + } + }; + + @Compositor.Flavor + public int getCompositorFlavor() { + if (FeatureSupport.isArc()) { + return Compositor.FLAVOR_ARC; + } else if (FeatureSupport.isTv(this)) { + return Compositor.FLAVOR_TV; + } else { + return Compositor.FLAVOR_NONE; + } } - } - /** Unregisters the dialog from {@link RingerModeAndScreenMonitor} for screen monitor. */ - public void unregisterDialog(DialogInterface dialog) { - if (ringerModeAndScreenMonitor != null) { - ringerModeAndScreenMonitor.unregisterDialog(dialog); + public UniversalSearchManager getUniversalSearchManager() { + return universalSearchManager; } - } - @Override - public void onInterrupt() { - if (processorScreen != null && FeatureSupport.isArc()) { - // In Arc, we consider that focus goes out from Arc when onInterrupt is called. - processorScreen.clearScreenState(); + @VisibleForTesting + public SpeechLanguage getSpeechLanguage() { + return speechLanguage; } - interruptAllFeedback(false /* stopTtsSpeechCompletely */); - } - @Override - public boolean isAudioPlaybackActive() { - return voiceActionMonitor.isAudioPlaybackActive(); - } + // Gets the user preferred locale changed using language switcher. + public Locale getUserPreferredLocale() { + return compositor.getUserPreferredLanguage(); + } - @Override - public boolean isMicrophoneActiveAndHeadphoneOff() { - return voiceActionMonitor.isMicrophoneActiveAndHeadphoneOff(); - } + // Sets the user preferred locale changed using language switcher. + private void setUserPreferredLocale(Locale locale) { + compositor.setUserPreferredLanguage(locale); + } - @Override - public boolean isSsbActiveAndHeadphoneOff() { - return voiceActionMonitor.isSsbActiveAndHeadphoneOff(); - } + public ListMenuManager getMenuManager() { + return menuManager; + } - @Override - public boolean isPhoneCallActive() { - return voiceActionMonitor.isPhoneCallActive(); - } + /** + * 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() { + + // Load log-level preference early, so that we can log it and use it during startup. + reloadPreferenceLogLevel(); + // Log meta-data about service, disregarding log-level pref, in 1 line for easy log filtering. + if (Log.isLoggable(TAG, Log.INFO)) { + Log.i( + TAG, + "resumeInfrastructure() android Build.VERSION.SDK_INT=" + + Build.VERSION.SDK_INT + + " talkback getVersionName=" + + PackageManagerUtils.getVersionName(this) + + " LogUtils.getLogLevel=" + + LogUtils.getLogLevel() + + " utils.BuildConfig.DEBUG=" + + com.google.android.accessibility.utils.BuildConfig.DEBUG); + } - @Override - public void onSpeakingForcedFeedback() { - voiceActionMonitor.onSpeakingForcedFeedback(); - } + if (isServiceActive()) { + LogUtils.e(TAG, "Attempted to resume while not suspended"); + return; + } - // Interrupts all Talkback feedback. Stops speech from other apps if stopTtsSpeechCompletely - // is true. - public void interruptAllFeedback(boolean stopTtsSpeechCompletely) { - - if (fullScreenReadActor != null) { - fullScreenReadActor.interrupt(); - } - - if (pipeline != null) { - pipeline.interruptAllFeedback(stopTtsSpeechCompletely); - } - } - - @Override - protected void onServiceConnected() { - LogUtils.v(TAG, "System bound to service."); - - primesController = new PrimesController(); - primesController.initialize(getApplication()); - primesController.startTimer(Timer.START_UP); - - SharedPreferencesUtils.migrateSharedPreferences(this); - prefs = SharedPreferencesUtils.getSharedPreferences(this); - initializeInfrastructure(); - SharedKeyEvent.register(this); - - // Configure logs. - LogUtils.setTagPrefix("talkback: "); - LogUtils.setParameterCustomizer( - (object) -> { - if (object instanceof AccessibilityNodeInfoCompat) { - return AccessibilityNodeInfoUtils.toStringShort((AccessibilityNodeInfoCompat) object) - + " "; - } else if (object instanceof AccessibilityNodeInfo) { - return AccessibilityNodeInfoUtils.toStringShort((AccessibilityNodeInfo) object) - + " "; - } else if (object instanceof AccessibilityEvent) { - return AccessibilityEventUtils.toStringShort((AccessibilityEvent) object) + " "; - } else { - return object; - } - }); - - // The service must be connected before getFingerprintGestureController() is called, thus we - // cannot initialize fingerprint gesture detection in onCreate(). - initializeFingerprintGestureCallback(); - - resumeInfrastructure(); - - // Handle any update actions. - final TalkBackUpdateHelper helper = new TalkBackUpdateHelper(this); - helper.checkUpdate(); - - compositor.handleEvent(Compositor.EVENT_SPOKEN_FEEDBACK_ON, EVENT_ID_UNTRACKED); - - // If the locked-boot-completed intent was fired before onServiceConnected, we queued it, - // so now we need to run it. - if (lockedBootCompletedPending) { - onLockedBootCompletedInternal(); - lockedBootCompletedPending = false; - } - - // Shows tutorial or onboarding. - if (showTutorialIfNecessary()) { - // Avoids showing onboarding when user turns on TalkBack for the second time. - OnboardingInitiator.ignoreOnboarding(this); - return; - } - if (!FeatureSupport.isTv(getApplicationContext()) - && !FeatureSupport.isWatch(getApplicationContext())) { - OnboardingInitiator.showOnboardingIfNecessary(this); - } - - // Service gesture detection. - if (shouldUseTalkbackGestureDetection()) { - registerGestureDetection(); - } - - primesController.stopTimer(Timer.START_UP); - } - - /** - * @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 ServiceStateListener.SERVICE_STATE_INACTIVE; - } - - return service.serviceState; - } - - /** - * 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 serviceState == ServiceStateListener.SERVICE_STATE_ACTIVE; - } - - /** - * @return {@code true} if TalkBack is running and initialized, {@code false} otherwise. - */ - public static boolean isServiceActive() { - return (getServiceState() == ServiceStateListener.SERVICE_STATE_ACTIVE); - } - - /** Returns the active TalkBack instance, or {@code null} if not available. */ - public static @Nullable TalkBackService getInstance() { - return instance; - } - - /** Initialize {@link FingerprintGestureCallback} for detecting fingerprint gestures. */ - private void initializeFingerprintGestureCallback() { - if (fingerprintGestureCallback != null || !FeatureSupport.isFingerprintGestureSupported(this)) { - return; - } - fingerprintGestureCallback = - new FingerprintGestureCallback() { - @Override - public void onGestureDetected(int gesture) { - if (isServiceActive() && gestureController != null) { - Performance perf = Performance.getInstance(); - EventId eventId = perf.onFingerprintGestureEventReceived(gesture); - - LogUtils.v(TAG, "Recognized fingerprint gesture %s", gesture); - - // TODO: Update analytics data. - // TODO: Check if we should dismiss radial menu. - feedbackController.playAuditory(R.raw.gesture_end, eventId); - - gestureController.onFingerprintGesture(gesture, eventId); - - // Measure latency. - // Preceding event handling frequently initiates a framework action, which in turn - // cascades a focus event, which in turn generates feedback. - perf.onHandlerDone(eventId); + setServiceState(ServiceStateListener.SERVICE_STATE_ACTIVE); + stopForeground(true); + + AccessibilityServiceInfo info = getServiceInfo(); + if (info == null) { + LogUtils.e(TAG, "Fail to get service flag!"); + } else { + info.flags |= ExperimentalUtils.getAdditionalTalkBackServiceFlags(); + if (FeatureSupport.isMultiFingerGestureSupported()) { + info.flags |= + AccessibilityServiceInfo.FLAG_REQUEST_MULTI_FINGER_GESTURES + | AccessibilityServiceInfo.FLAG_REQUEST_2_FINGER_PASSTHROUGH; + resetTouchExplorePassThrough(); + } else { + info.flags &= + ~(AccessibilityServiceInfo.FLAG_REQUEST_MULTI_FINGER_GESTURES + | AccessibilityServiceInfo.FLAG_REQUEST_2_FINGER_PASSTHROUGH); + } + if (GestureReporter.ENABLED) { + info.flags |= AccessibilityServiceInfo.FLAG_SEND_MOTION_EVENTS; + } + info.notificationTimeout = 0; + if (BuildVersionUtils.isAtLeastQ()) { + info.setInteractiveUiTimeoutMillis(DEFAULT_INTERACTIVE_UI_TIMEOUT_MILLIS); } - } - @Override - public void onGestureDetectionAvailabilityChanged(boolean available) { - LogUtils.v( - TAG, - "Fingerprint gesture detection is now " - + (available ? "available" : "unavailable") - + "."); - } - }; - } - - /** - * Initializes the controllers, managers, and processors. This should only be called once from - * {@link #onServiceConnected()}. - */ - private void initializeInfrastructure() { - // TODO: we still need it keep true for TV until TouchExplore and Accessibility focus is - // not unpaired - // supportsTouchScreen = packageManager.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); - - accessibilityEventProcessor = new AccessibilityEventProcessor(this); - feedbackController = new FeedbackController(this); - speechController = new SpeechControllerImpl(this, this, feedbackController); - speechStateMonitor = new SpeechStateMonitor(); - diagnosticOverlayController = new DiagnosticOverlayControllerImpl(this); - - gestureShortcutMapping = new GestureShortcutMapping(this); - - globalVariables = - new GlobalVariables(this, inputModeManager, keyComboManager, gestureShortcutMapping); - - labelManager = new CustomLabelManager(this); - addEventListener(labelManager); - - ImageCaptionStorage imageCaptionStorage = new ImageCaptionStorage(); - imageContents = - ImageCaptioner.supportsImageCaption(this) - ? new ImageContents(labelManager, imageCaptionStorage) - : new ImageContents(labelManager, /* imageCaptionStorage= */ null); - - compositor = - new Compositor( - this, - /* speechController= */ null, - imageContents, - globalVariables, - getCompositorFlavor()); - // TODO: Make pipeline run Compositor, which returns speech feedback, no callback. - - analytics = new TalkBackAnalyticsImpl(this); - - processorPhoneticLetters = new ProcessorPhoneticLetters(this); - - FocusFinder focusFinder = new FocusFinder(this); - - // Construct system-monitors. - batteryMonitor = new BatteryMonitor(this); - callStateMonitor = new CallStateMonitor(this); - audioPlaybackMonitor = new AudioPlaybackMonitor(this); - @NonNull TouchMonitor touchMonitor = new TouchMonitor(); - - // Construct event-interpreters. - AutoScrollInterpreter autoScrollInterpreter = new AutoScrollInterpreter(); - screenStateMonitor = new ScreenStateMonitor(/* service= */ this); - FullScreenReadInterpreter fullScreenReadInterpreter = new FullScreenReadInterpreter(); - scrollPositionInterpreter = new ScrollPositionInterpreter(); - ScrollEventInterpreter scrollEventInterpreter = - new ScrollEventInterpreter(audioPlaybackMonitor, touchMonitor); - ManualScrollInterpreter manualScrollInterpreter = new ManualScrollInterpreter(); - - // Constructor output-actor-state. - textCursorTracker = new TextCursorTracker(); - editTextActionHistory = new EditTextActionHistory(); - AccessibilityFocusActionHistory focusHistory = new AccessibilityFocusActionHistory(this); - - // Construct output-actors. - AutoScrollActor scroller = new AutoScrollActor(); - accessibilityFocusMonitor = - new AccessibilityFocusMonitor(this, focusFinder, focusHistory.reader); - - imageCaptioner = new ImageCaptioner(this, imageCaptionStorage, accessibilityFocusMonitor); - - // TODO: ScreenState should be passed through pipeline. - focuser = - new FocusActor( - this, focusFinder, screenStateMonitor.state, focusHistory, accessibilityFocusMonitor); - DirectionNavigationActor directionNavigationActor = - new DirectionNavigationActor( - inputModeManager, - globalVariables, - analytics, - compositor, - this, - focusFinder, - processorPhoneticLetters, - accessibilityFocusMonitor, - screenStateMonitor.state); - directionNavigationActorStateReader = directionNavigationActor.state; - TextEditActor editor = - new TextEditActor( - this, - editTextActionHistory, - textCursorTracker, - getSystemService(ClipboardManager.class)); - fullScreenReadActor = - new FullScreenReadActor(accessibilityFocusMonitor, this, speechController); - dimScreenController = new DimScreenActor(this, gestureShortcutMapping); - - accessibilityFocusInterpreter = - new AccessibilityFocusInterpreter( - this, accessibilityFocusMonitor, screenStateMonitor.state); - - inputFocusInterpreter = - new InputFocusInterpreter(accessibilityFocusInterpreter, focusFinder, globalVariables); - - proximitySensorListener = new ProximitySensorListener(/* service= */ this); - speechLanguage = new SpeechLanguage(); - - DirectionNavigationInterpreter directionNavigationInterpreter = - new DirectionNavigationInterpreter(this); - - processorHints = new ProcessorAccessibilityHints(); - addEventListener(processorHints); - keyEventListeners.add(0, processorHints); // Needs to be first; will not catch any events. - - passThroughModeActor = new PassThroughModeActor(this); - - selectorController = - new SelectorController( - this, accessibilityFocusMonitor, analytics, gestureShortcutMapping, processorHints); - - UniversalSearchActor universalSearchActor = - new UniversalSearchActor(this, screenStateMonitor.state, focusFinder, labelManager); - - autoScrollInterpreter.setUniversalSearchActor(universalSearchActor); - - voiceCommandProcessor = - new VoiceCommandProcessor(this, accessibilityFocusMonitor, selectorController, analytics); - speechRecognizer = new SpeechRecognizerActor(this, voiceCommandProcessor, analytics); - UiChangeEventInterpreter uiChangeEventInterpreter = new UiChangeEventInterpreter(); - addEventListener(uiChangeEventInterpreter); - - UserInterface userInterface = new UserInterface(selectorController); - - // Construct pipeline. - pipeline = - new Pipeline( - this, - new Monitors(batteryMonitor, callStateMonitor, touchMonitor, speechStateMonitor), - new Interpreters( - inputFocusInterpreter, - scrollEventInterpreter, - manualScrollInterpreter, - autoScrollInterpreter, - scrollPositionInterpreter, - accessibilityFocusInterpreter, - fullScreenReadInterpreter, - new StateChangeEventInterpreter(), - directionNavigationInterpreter, - processorHints, - voiceCommandProcessor, - new PassThroughModeInterpreter(), - new SubtreeChangeEventInterpreter(screenStateMonitor.state), - new AccessibilityEventIdleInterpreter(), - uiChangeEventInterpreter), - new Mappers(this, compositor, focusFinder), - new Actors( - this, - accessibilityFocusMonitor, - dimScreenController, - speechController, - fullScreenReadActor, - feedbackController, - scroller, - focuser, - new FocusActorForScreenStateChange(focusFinder, primesController), - new FocusActorForTapAndTouchExploration(), - directionNavigationActor, - new SearchScreenNodeStrategy(/* observer= */ null, labelManager), - editor, - labelManager, - new NodeActionPerformer(), - new SystemActionPerformer(this), - new LanguageActor(this, speechLanguage), - passThroughModeActor, - new TalkBackUIActor(this), - new SpeechRateActor(this), - new NumberAdjustor(this, accessibilityFocusMonitor), - new VolumeAdjustor(this), - speechRecognizer, - new GestureReporter(this, new GestureHistory()), - imageCaptioner, - universalSearchActor, - this::requestServiceFlag), - proximitySensorListener, - speechController, - diagnosticOverlayController, - compositor, - userInterface); - - processorHints.setActorState(pipeline.getActorState()); - processorHints.setPipeline(pipeline.getFeedbackReturner()); - - voiceCommandProcessor.setActorState(pipeline.getActorState()); - voiceCommandProcessor.setPipeline(pipeline.getFeedbackReturner()); - - accessibilityEventProcessor.setActorState(pipeline.getActorState()); - accessibilityEventProcessor.setAccessibilityEventIdleListener(pipeline); - - autoScrollInterpreter.setDirectionNavigationActor(directionNavigationActor); - - nodeMenuRuleProcessor = - new NodeMenuRuleProcessor( - this, pipeline.getFeedbackReturner(), pipeline.getActorState(), analytics); - compositor.setNodeMenuProvider(nodeMenuRuleProcessor); - - compositor.setSpeaker(pipeline.getSpeaker()); - - TouchExplorationInterpreter touchExplorationInterpreter = - new TouchExplorationInterpreter(inputModeManager); - - if (FeatureSupport.supportMagnificationController()) { - processorMagnification = - new ProcessorMagnification( - getMagnificationController(), - globalVariables, - compositor, - FeatureSupport.supportWindowMagnification(this)); - } - - // Register AccessibilityEventListeners - addEventListener(touchExplorationInterpreter); - addEventListener(directionNavigationInterpreter); - if (processorMagnification != null) { - addEventListener(processorMagnification); - } - addEventListener(pipeline); - - touchExplorationInterpreter.addTouchExplorationActionListener(accessibilityFocusInterpreter); - screenStateMonitor.addScreenStateChangeListener(accessibilityFocusInterpreter); - - screenStateMonitor.addScreenStateChangeListener(inputFocusInterpreter); - - voiceActionMonitor = new VoiceActionMonitor(this, callStateMonitor, speechStateMonitor); - accessibilityEventProcessor.setVoiceActionMonitor(voiceActionMonitor); - - keyEventListeners.add(inputModeManager); - - menuManager = - new ListMenuManager( - this, - pipeline.getFeedbackReturner(), - pipeline.getActorState(), - accessibilityFocusMonitor, - nodeMenuRuleProcessor, - analytics); - voiceCommandProcessor.setListMenuManager(menuManager); - - keyComboManager = - new KeyComboManager( - this, - pipeline.getFeedbackReturner(), - selectorController, - menuManager, - fullScreenReadActor); - - ringerModeAndScreenMonitor = - new RingerModeAndScreenMonitor( - menuManager, - pipeline.getFeedbackReturner(), - proximitySensorListener, - callStateMonitor, - this); - accessibilityEventProcessor.setRingerModeAndScreenMonitor(ringerModeAndScreenMonitor); - - // Only use speak-pass talkback-preference on android O+. - if (FeatureSupport.useSpeakPasswordsServicePref()) { - headphoneStateMonitor = new HeadphoneStateMonitor(this); - speakPasswordsManager = - new SpeakPasswordsManager(this, headphoneStateMonitor, globalVariables); - } - - ProcessorVolumeStream processorVolumeStream = - new ProcessorVolumeStream(pipeline.getActorState(), this); - addEventListener(processorVolumeStream); - keyEventListeners.add(processorVolumeStream); - - gestureController = - new GestureController( - this, - pipeline.getFeedbackReturner(), - pipeline.getActorState(), - menuManager, - selectorController, - accessibilityFocusMonitor, - gestureShortcutMapping); - - audioPlaybackMonitor = new AudioPlaybackMonitor(this); - - // Add event processors. These will process incoming AccessibilityEvents - // in the order they are added. - eventFilter = new EventFilter(compositor, this, touchMonitor, globalVariables); - eventFilter.setVoiceActionDelegate(voiceActionMonitor); - eventFilter.setAccessibilityFocusEventInterpreter(accessibilityFocusInterpreter); - ActorStateProvider actorStateProvider = - new ActorStateProvider() { - @Override - public boolean resettingNodeCursor() { - return globalVariables.resettingNodeCursor(); - } - }; - PreferenceProvider preferenceProvider = - new PreferenceProvider() { - @Override - public boolean shouldSpeakPasswords() { - return globalVariables.shouldSpeakPasswords(); - } - }; - textEventInterpreter = - new TextEventInterpreter( - this, - textCursorTracker, - directionNavigationActor.state, - inputModeManager, - new TextEventHistory(), - editTextActionHistory.provider, - actorStateProvider, - preferenceProvider, - voiceActionMonitor); - processorEventQueue = new ProcessorEventQueue(eventFilter, textEventInterpreter); - - addEventListener(processorEventQueue); - addEventListener(processorPhoneticLetters); - - // Create window event interpreter and announcer. - processorScreen = - new ProcessorScreen( - this, - processorHints, - keyComboManager, - focusFinder, - gestureShortcutMapping, - pipeline.getFeedbackReturner()); - globalVariables.setWindowsDelegate(processorScreen.getWindowEventInterpreter()); - screenStateMonitor.setWindowsDelegate(processorScreen.getWindowEventInterpreter()); - addEventListener(processorScreen); - - // Monitor window transition status by registering listeners. - if (processorScreen != null && processorScreen.getWindowEventInterpreter() != null) { - processorScreen.getWindowEventInterpreter().addListener(menuManager); - processorScreen.getWindowEventInterpreter().addListener(screenStateMonitor); - processorScreen.getWindowEventInterpreter().addListener(uiChangeEventInterpreter); - processorScreen.getWindowEventInterpreter().addListener(imageCaptioner); - } - - processorCursorState = - new ProcessorCursorState(this, pipeline.getFeedbackReturner(), globalVariables); - processorPermissionsDialogs = - new ProcessorPermissionDialogs( - this, pipeline.getActorState(), pipeline.getFeedbackReturner()); - - volumeMonitor = new VolumeMonitor(pipeline.getFeedbackReturner(), this, callStateMonitor); - - // TODO: Move this into the custom label manager code - packageReceiver = new PackageRemovalReceiver(); - - addEventListener(new ProcessorGestureVibrator(pipeline.getFeedbackReturner())); - - universalSearchManager = - new UniversalSearchManager( - pipeline.getFeedbackReturner(), - ringerModeAndScreenMonitor, - processorScreen.getWindowEventInterpreter()); - - keyEventListeners.add(keyComboManager); - serviceStateListeners.add(keyComboManager); - - orientationMonitor = new OrientationMonitor(compositor, this); - orientationMonitor.addOnOrientationChangedListener(dimScreenController); - - KeyboardLockMonitor keyboardLockMonitor = new KeyboardLockMonitor(compositor); - keyEventListeners.add(keyboardLockMonitor); - - if (Build.VERSION.SDK_INT >= TelevisionNavigationController.MIN_API_LEVEL - && FeatureSupport.isTv(this)) { - televisionNavigationController = - new TelevisionNavigationController( - this, accessibilityFocusMonitor, pipeline.getFeedbackReturner()); - keyEventListeners.add(televisionNavigationController); - televisionDPadManager = new TelevisionDPadManager(televisionNavigationController, this); - addEventListener(televisionDPadManager); - } - - brailleDisplay = new BrailleDisplay(this, talkBackForBrailleDisplay); - - BrailleIme.initialize( - this, talkBackForBrailleIme, brailleDisplay.getBrailleDisplayForBrailleIme()); - analytics.onTalkBackServiceStarted(); - } - - private final TalkBackForBrailleDisplay talkBackForBrailleDisplay = - new TalkBackForBrailleDisplay() { - @Override - public boolean performAction(ScreenReaderAction action, Object... args) { - // TODO: implement the screen reader actions. - if (action == ScreenReaderAction.OPEN_TALKBACK_MENU) { - return menuManager.showMenu(R.menu.context_menu, EVENT_ID_UNTRACKED); - } - return BrailleDisplayHelper.performAction( - getInstance(), pipeline.getFeedbackReturner(), action, args); - } + // Ensure the initial touch exploration request mode is correct. + if (supportsTouchScreen + && getBooleanPref( + R.string.pref_explore_by_touch_key, R.bool.pref_explore_by_touch_default)) { + info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE; + } - @Override - public AccessibilityNodeInfoCompat getAccessibilityFocusNode(boolean fallbackOnRoot) { - return FocusFinder.getAccessibilityFocusNode(getInstance(), fallbackOnRoot); + LogUtils.v(TAG, "Accessibility Service flag set: 0x%X", info.flags); + setServiceInfo(info); } - @Override - public FocusFinder createFocusFinder() { - return new FocusFinder(getInstance()); + if (callStateMonitor != null) { + if (!isFirstTimeUser()) { + callStateMonitor.requestPhonePermissionIfNeeded(prefs); + } + callStateMonitor.startMonitoring(); } - @Override - public boolean showLabelDialog(CustomLabelAction action, AccessibilityNodeInfoCompat node) { - if (action == CustomLabelAction.ADD_LABEL) { - return LabelDialogManager.addLabel( - getInstance(), - node.getViewIdResourceName(), - /* needToRestoreFocus= */ true, - pipeline.getFeedbackReturner()); - } else if (action == CustomLabelAction.EDIT_LABEL) { - return LabelDialogManager.editLabel( - getInstance(), - labelManager.getLabelForViewIdFromCache(node.getViewIdResourceName()).getId(), - /* needToRestoreFocus= */ true, - pipeline.getFeedbackReturner()); - } - return false; + if (voiceActionMonitor != null) { + voiceActionMonitor.onResumeInfrastructure(); } - @Override - public CharSequence getCustomLabelText(AccessibilityNodeInfoCompat node) { - Label label = labelManager.getLabelForViewIdFromCache(node.getViewIdResourceName()); - if (label != null) { - return label.getText(); - } - return null; + if (audioPlaybackMonitor != null) { + audioPlaybackMonitor.onResumeInfrastructure(); } - @Override - public boolean needsLabel(AccessibilityNodeInfoCompat node) { - return labelManager.needsLabel(node); + if (ringerModeAndScreenMonitor != null) { + registerReceiver(ringerModeAndScreenMonitor, ringerModeAndScreenMonitor.getFilter()); + // It could now be confused with the current screen state + ringerModeAndScreenMonitor.updateScreenState(); } - @Override - public @Nullable BrailleImeForBrailleDisplay getBrailleImeForBrailleDisplay() { - return brailleImeForTalkBack == null - ? null - : brailleImeForTalkBack.getBrailleImeForBrailleDisplay(); + if (headphoneStateMonitor != null) { + headphoneStateMonitor.startMonitoring(); } - @Override - public void speak(CharSequence textToSpeak, int delayMs, SpeakOptions speakOptions) { - pipeline - .getFeedbackReturner() - .returnFeedback( - Performance.EVENT_ID_UNTRACKED, - Feedback.speech(textToSpeak, speakOptions).setDelayMs(delayMs)); + if (volumeMonitor != null) { + registerReceiver(volumeMonitor, volumeMonitor.getFilter()); + if (FeatureSupport.hasAccessibilityAudioStream(this)) { + // Cache the initial volume in case that the volume is never changed during runtime. + volumeMonitor.cacheAccessibilityStreamVolume(); + } } - }; - private final TalkBackForBrailleIme talkBackForBrailleIme = - new TalkBackForBrailleIme() { - @Override - public void onBrailleImeActivated( - BrailleImeForTalkBack brailleImeForTalkBack, - boolean disableEbt, - boolean usePassThrough, - Region passThroughRegion) { - isBrailleKeyboardActivated = true; - TalkBackService.this.brailleImeForTalkBack = brailleImeForTalkBack; - if (usePassThrough) { - pipeline - .getFeedbackReturner() - .returnFeedback( - Performance.EVENT_ID_UNTRACKED, - Feedback.passThroughMode(LOCK_PASS_THROUGH, passThroughRegion)); - } else { - requestTouchExploration(!disableEbt); - } + if (batteryMonitor != null) { + registerReceiver(batteryMonitor, batteryMonitor.getFilter()); } - @Override - public void onBrailleImeInactivated(boolean usePassThrough) { - if (getServiceStatus() != ServiceStatus.ON) { - return; - } - isBrailleKeyboardActivated = false; - TalkBackService.this.brailleImeForTalkBack = null; - if (usePassThrough) { - pipeline - .getFeedbackReturner() - .returnFeedback( - Performance.EVENT_ID_UNTRACKED, - Feedback.passThroughMode(LOCK_PASS_THROUGH, null)); - } else { - boolean ebtEnabled = - getBooleanPref( - R.string.pref_explore_by_touch_key, R.bool.pref_explore_by_touch_default); - if (ebtEnabled) { - requestTouchExploration(true); + if (packageReceiver != null) { + registerReceiver(packageReceiver, packageReceiver.getFilter()); + if (labelManager != null) { + labelManager.ensureDataConsistency(); } - } } - @Override - public boolean setInputMethodEnabled() { - if (FeatureSupport.supportEnableDisableIme() && getInstance() != null) { - return getSoftKeyboardController() - .setInputMethodEnabled( - KeyboardUtils.getImeId(getInstance(), getPackageName()), - /* enabled= */ true) - == SoftKeyboardController.ENABLE_IME_SUCCESS; - } - return false; + prefs.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); + prefs.registerOnSharedPreferenceChangeListener(analytics); + + if (processorMagnification != null) { + processorMagnification.onResumeInfrastructure(); } - @Override - public WindowManager getWindowManager() { - return (WindowManager) getSystemService(Context.WINDOW_SERVICE); + if ((fingerprintGestureCallback != null) && (getFingerprintGestureController() != null)) { + getFingerprintGestureController() + .registerFingerprintGestureCallback(fingerprintGestureCallback, null); } - @Override - public ServiceStatus getServiceStatus() { - return isServiceActive() ? ServiceStatus.ON : ServiceStatus.OFF; + reloadPreferences(); + + dimScreenController.resume(); + + inputFocusInterpreter.initLastEditableFocusForGlobalVariables(); + + if (brailleImeForTalkBack != null) { + brailleImeForTalkBack.onTalkBackResumed(); } + brailleDisplay.start(); + } - @Override - public void speak(CharSequence textToSpeak, int delayMs, SpeakOptions speakOptions) { - // TODO: For uses cases where the timer is meant to re-schedule text, we - // should create a centralized repeat-feedback feature, and have BrailleIme use that. - pipeline - .getFeedbackReturner() - .returnFeedback( - Performance.EVENT_ID_UNTRACKED, - Feedback.speech(textToSpeak, speakOptions).setDelayMs(delayMs)); + @Override + public void unregisterReceiver(BroadcastReceiver receiver) { + try { + if (receiver != null) { + super.unregisterReceiver(receiver); + } + } catch (IllegalArgumentException e) { + LogUtils.e( + TAG, + "Do not unregister receiver as it was never registered: " + + receiver.getClass().getSimpleName()); } + } - @Override - public void interruptSpeak() { - interruptAllFeedback(false); + private void unregisterReceivers(BroadcastReceiver... receivers) { + if (receivers == null) { + return; + } + for (BroadcastReceiver receiver : receivers) { + unregisterReceiver(receiver); } + } - @Override - public void playSound(int resId, int delayMs) { - pipeline - .getFeedbackReturner() - .returnFeedback( - Performance.EVENT_ID_UNTRACKED, Feedback.sound(resId).setDelayMs(delayMs)); + /** + * Registers listeners, sets service info, loads preferences. This should be called from {@link + * #onServiceConnected} and when TalkBack resumes from a suspended state. + */ + private void suspendInfrastructure() { + if (!isServiceActive()) { + LogUtils.e(TAG, "Attempted to suspend while already suspended"); + return; } - @Override - public void disableSilenceOnProximity() { - proximitySensorListener.setSilenceOnProximity(false); + setServiceState(ServiceStateListener.SERVICE_STATE_SHUTTING_DOWN); + + if (callStateMonitor != null) { + callStateMonitor.stopMonitoring(); } - @Override - public void restoreSilenceOnProximity() { - reloadSilenceOnProximity(); + if (voiceActionMonitor != null) { + voiceActionMonitor.onSuspendInfrastructure(); } - @Override - public boolean isContextMenuExist() { - return menuManager.isMenuExist(); + if (audioPlaybackMonitor != null) { + audioPlaybackMonitor.onSuspendInfrastructure(); } - @Override - public boolean isVibrationFeedbackEnabled() { - return FeatureSupport.isVibratorSupported(getApplicationContext()) - && getBooleanPref(R.string.pref_vibration_key, R.bool.pref_vibration_default); + dimScreenController.suspend(); + + interruptAllFeedback(/* stopTtsSpeechCompletely */ false); + + // Some apps depend on these being set to false when TalkBack is disabled. + if (supportsTouchScreen) { + requestTouchExploration(false); } - @Override - public boolean shouldAnnounceCharacter() { - @KeyboardEchoType - int echoType = - brailleDisplay - .getBrailleDisplayForBrailleIme() - .isBrailleDisplayConnectedAndNotSuspended() - ? readPhysicalKeyboardEcho() - : readOnScreenKeyboardEcho(); - return echoType == PREF_ECHO_CHARACTERS || echoType == PREF_ECHO_CHARACTERS_AND_WORDS; + prefs.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); + prefs.unregisterOnSharedPreferenceChangeListener(analytics); + + unregisterReceivers(ringerModeAndScreenMonitor, batteryMonitor, packageReceiver, volumeMonitor); + + if (volumeMonitor != null) { + volumeMonitor.releaseControl(); } - @Override - public boolean shouldSpeakPassword() { - return globalVariables.shouldSpeakPasswords(); + if (headphoneStateMonitor != null) { + headphoneStateMonitor.stopMonitoring(); } - @Override - public boolean shouldUseCharacterGranularity() { - CursorGranularity granularity = - directionNavigationActorStateReader.getCurrentGranularity(); - return granularity == CursorGranularity.CHARACTER || !granularity.isMicroGranularity(); + // Remove any pending notifications that shouldn't persist. + final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancelAll(); + + if (processorMagnification != null) { + processorMagnification.onSuspendInfrastructure(); } - @Override - public void moveCursorForward() { - if (directionNavigationActorStateReader.getCurrentGranularity().isMicroGranularity()) { - selectorController.adjustSelectedSetting(EVENT_ID_UNTRACKED, /* isNext= */ true); - } + if ((fingerprintGestureCallback != null) && (getFingerprintGestureController() != null)) { + getFingerprintGestureController() + .unregisterFingerprintGestureCallback(fingerprintGestureCallback); } - @Override - public void moveCursorBackward() { - if (directionNavigationActorStateReader.getCurrentGranularity().isMicroGranularity()) { - selectorController.adjustSelectedSetting(EVENT_ID_UNTRACKED, /* isNext= */ false); - } - } - }; - - @Compositor.Flavor - public int getCompositorFlavor() { - if (FeatureSupport.isArc()) { - return Compositor.FLAVOR_ARC; - } else if (FeatureSupport.isTv(this)) { - return Compositor.FLAVOR_TV; - } else { - return Compositor.FLAVOR_NONE; - } - } - - public UniversalSearchManager getUniversalSearchManager() { - return universalSearchManager; - } - - @VisibleForTesting - public SpeechLanguage getSpeechLanguage() { - return speechLanguage; - } - - // Gets the user preferred locale changed using language switcher. - public Locale getUserPreferredLocale() { - return compositor.getUserPreferredLanguage(); - } - - // Sets the user preferred locale changed using language switcher. - private void setUserPreferredLocale(Locale locale) { - compositor.setUserPreferredLanguage(locale); - } - - public ListMenuManager getMenuManager() { - return menuManager; - } - - /** - * 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() { - - // Load log-level preference early, so that we can log it and use it during startup. - reloadPreferenceLogLevel(); - // Log meta-data about service, disregarding log-level pref, in 1 line for easy log filtering. - if (Log.isLoggable(TAG, Log.INFO)) { - Log.i( - TAG, - "resumeInfrastructure() android Build.VERSION.SDK_INT=" - + Build.VERSION.SDK_INT - + " talkback getVersionName=" - + PackageManagerUtils.getVersionName(this) - + " LogUtils.getLogLevel=" - + LogUtils.getLogLevel() - + " utils.BuildConfig.DEBUG=" - + com.google.android.accessibility.utils.BuildConfig.DEBUG); - } + if (FeatureSupport.isFingerprintGestureSupported(this)) { + requestServiceFlag(AccessibilityServiceInfo.FLAG_REQUEST_FINGERPRINT_GESTURES, false); + } - if (isServiceActive()) { - LogUtils.e(TAG, "Attempted to resume while not suspended"); - return; + if (brailleImeForTalkBack != null) { + brailleImeForTalkBack.onTalkBackSuspended(); + } + brailleDisplay.stop(); } - setServiceState(ServiceStateListener.SERVICE_STATE_ACTIVE); - stopForeground(true); + /** + * Shuts down the infrastructure in case it has been initialized. + */ + private void shutdownInfrastructure() { + setServiceState(ServiceStateListener.SERVICE_STATE_SHUTTING_DOWN); + // 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 + // We check the instance against null to prevent the premature service destroy (aka destroy + // before connected). + if (dimScreenController != null) { + dimScreenController.shutdown(); + } - AccessibilityServiceInfo info = getServiceInfo(); - if (info == null) { - LogUtils.e(TAG, "Fail to get service flag!"); - } else { - info.flags |= ExperimentalUtils.getAdditionalTalkBackServiceFlags(); - if (FeatureSupport.isMultiFingerGestureSupported()) { - info.flags |= - AccessibilityServiceInfo.FLAG_REQUEST_MULTI_FINGER_GESTURES - | AccessibilityServiceInfo.FLAG_REQUEST_2_FINGER_PASSTHROUGH; - resetTouchExplorePassThrough(); - } else { - info.flags &= - ~(AccessibilityServiceInfo.FLAG_REQUEST_MULTI_FINGER_GESTURES - | AccessibilityServiceInfo.FLAG_REQUEST_2_FINGER_PASSTHROUGH); - } - if (GestureReporter.ENABLED) { - info.flags |= AccessibilityServiceInfo.FLAG_SEND_MOTION_EVENTS; - } - info.notificationTimeout = 0; - if (BuildVersionUtils.isAtLeastQ()) { - info.setInteractiveUiTimeoutMillis(DEFAULT_INTERACTIVE_UI_TIMEOUT_MILLIS); - } + if (fullScreenReadActor != null) { + fullScreenReadActor.shutdown(); + } - // Ensure the initial touch exploration request mode is correct. - if (supportsTouchScreen - && getBooleanPref( - R.string.pref_explore_by_touch_key, R.bool.pref_explore_by_touch_default)) { - info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE; - } + if (labelManager != null) { + labelManager.shutdown(); + } - LogUtils.v(TAG, "Accessibility Service flag set: 0x%X", info.flags); - setServiceInfo(info); - } + if (imageCaptioner != null) { + imageCaptioner.shutdown(); + } - if (callStateMonitor != null) { - if (!isFirstTimeUser()) { - callStateMonitor.requestPhonePermissionIfNeeded(prefs); - } - callStateMonitor.startMonitoring(); + if (proximitySensorListener != null) { + proximitySensorListener.shutdown(); + } + if (feedbackController != null) { + feedbackController.shutdown(); + } + if (pipeline != null) { + pipeline.shutdown(); + } + if (analytics != null) { + analytics.onTalkBackServiceStopped(); + } } - if (voiceActionMonitor != null) { - voiceActionMonitor.onResumeInfrastructure(); + /** + * Adds an event listener. + * + * @param listener The listener to add. + */ + public void addEventListener(AccessibilityEventListener listener) { + accessibilityEventProcessor.addAccessibilityEventListener(listener); } - if (audioPlaybackMonitor != null) { - audioPlaybackMonitor.onResumeInfrastructure(); + /** + * Posts a {@link Runnable} to removes an event listener. This is safe to call from inside {@link + * AccessibilityEventListener#onAccessibilityEvent(AccessibilityEvent, EventId)}. + * + * @param listener The listener to remove. + */ + public void postRemoveEventListener(final AccessibilityEventListener listener) { + accessibilityEventProcessor.postRemoveAccessibilityEventListener(listener); } - if (ringerModeAndScreenMonitor != null) { - registerReceiver(ringerModeAndScreenMonitor, ringerModeAndScreenMonitor.getFilter()); - // It could now be confused with the current screen state - ringerModeAndScreenMonitor.updateScreenState(); + /** + * Returns a boolean preference by resource id. + */ + private boolean getBooleanPref(int prefKeyResId, int prefDefaultResId) { + return SharedPreferencesUtils.getBooleanPref( + prefs, getResources(), prefKeyResId, prefDefaultResId); } - if (headphoneStateMonitor != null) { - headphoneStateMonitor.startMonitoring(); + /** + * When the device supports {@link AccessibilityService#setAnimationScale(float)}, system will + * determine to disable animation feature when TalkBack is on, and resume it after TalkBack is + * off. + * + * @param enable {@code false} to request the disable of animation, and {@code true} to resume the + * animation. + */ + private void enableAnimation(boolean enable) { + if (!FeatureSupport.supportsServiceControlOfGlobalAnimations()) { + return; + } + if (enable) { + if (prefs.contains(getString(R.string.pref_previous_global_window_animation_scale_key))) { + float scale = + SharedPreferencesUtils.getFloatFromStringPref( + prefs, + getResources(), + R.string.pref_previous_global_window_animation_scale_key, + R.string.pref_window_animation_scale_default); + if (scale > ANIMATION_OFF && SettingsUtils.isAnimationDisabled(this)) { + // Resume animation when the record value is meaningful (greater than zero); + setAnimationScale(scale); + } + prefs + .edit() + .remove(getString(R.string.pref_previous_global_window_animation_scale_key)) + .apply(); + } + } else { + if (!SettingsUtils.isAnimationDisabled(this)) { + prefs + .edit() + .putString( + getString(R.string.pref_previous_global_window_animation_scale_key), + Float.toString( + Settings.Global.getFloat( + getContentResolver(), Settings.Global.WINDOW_ANIMATION_SCALE, 1))) + .apply(); + } + // Disable animation; + setAnimationScale(ANIMATION_OFF); + } } - if (volumeMonitor != null) { - registerReceiver(volumeMonitor, volumeMonitor.getFilter()); - if (FeatureSupport.hasAccessibilityAudioStream(this)) { - // Cache the initial volume in case that the volume is never changed during runtime. - volumeMonitor.cacheAccessibilityStreamVolume(); - } - } + /** + * Reloads service preferences. + */ + private void reloadPreferences() { + final Resources res = getResources(); - if (batteryMonitor != null) { - registerReceiver(batteryMonitor, batteryMonitor.getFilter()); - } + LogUtils.v( + TAG, + "TalkBackService.reloadPreferences() diagnostic mode=%s", + PreferencesActivityUtils.isDiagnosisModeOn(prefs, res)); - if (packageReceiver != null) { - registerReceiver(packageReceiver, packageReceiver.getFilter()); - if (labelManager != null) { - labelManager.ensureDataConsistency(); - } - } + // Preferece to reduce window announcement delay. + boolean reduceDelayPref = + getBooleanPref( + R.string.pref_reduce_window_delay_key, R.bool.pref_reduce_window_delay_default); + if (processorScreen != null && processorScreen.getWindowEventInterpreter() != null) { + processorScreen.getWindowEventInterpreter().setReduceDelayPref(reduceDelayPref); + enableAnimation(!reduceDelayPref); + } - prefs.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); - prefs.registerOnSharedPreferenceChangeListener(analytics); + // If performance statistics changing enabled setting... clear collected stats. + boolean performanceEnabled = + getBooleanPref(R.string.pref_performance_stats_key, R.bool.pref_performance_stats_default); + Performance performance = Performance.getInstance(); + if (performance.getEnabled() != performanceEnabled) { + performance.clearRecentEvents(); + performance.clearAllStats(); + performance.setEnabled(performanceEnabled); + } - if (processorMagnification != null) { - processorMagnification.onResumeInfrastructure(); - } + boolean logOverlayEnabled = + PreferencesActivityUtils.getDiagnosticPref( + prefs, res, R.string.pref_log_overlay_key, R.bool.pref_log_overlay_default); + diagnosticOverlayController.setLogOverlayEnabled(logOverlayEnabled); + + accessibilityEventProcessor.setSpeakWhenScreenOff( + VerbosityPreferences.getPreferenceValueBool( + prefs, + res, + res.getString(R.string.pref_screenoff_key), + res.getBoolean(R.bool.pref_screenoff_default))); + + accessibilityEventProcessor.setDumpEventMask( + prefs.getInt(res.getString(R.string.pref_dump_event_mask_key), 0)); + + reloadSilenceOnProximity(); + reloadPreferenceLogLevel(); + + final boolean useSingleTap = + getBooleanPref(R.string.pref_single_tap_key, R.bool.pref_single_tap_default); + globalVariables.setUseSingleTap(useSingleTap); + accessibilityFocusInterpreter.setSingleTapEnabled(useSingleTap); + accessibilityFocusInterpreter.setTypingMethod( + SharedPreferencesUtils.getIntFromStringPref( + prefs, + res, + R.string.pref_typing_confirmation_key, + R.string.pref_typing_confirmation_default)); + accessibilityFocusInterpreter.setTypingLongPressDurationMs( + SharedPreferencesUtils.getIntFromStringPref( + prefs, + res, + R.string.pref_typing_long_press_duration_key, + R.string.pref_typing_long_press_duration_default)); + globalVariables.setInterpretAsEntryKey( + accessibilityFocusInterpreter.getTypingMethod() == FORCE_LIFT_TO_TYPE_ON_IME); + + if (supportsTouchScreen && !isBrailleKeyboardActivated) { + // Touch exploration *must* be enabled on TVs for TalkBack to function. + final boolean touchExploration = + (FeatureSupport.isTv(this) + || getBooleanPref( + R.string.pref_explore_by_touch_key, R.bool.pref_explore_by_touch_default)); + requestTouchExploration(touchExploration); + } - if ((fingerprintGestureCallback != null) && (getFingerprintGestureController() != null)) { - getFingerprintGestureController() - .registerFingerprintGestureCallback(fingerprintGestureCallback, null); - } + if (FeatureSupport.isMultiFingerGestureSupported()) { + requestServiceFlag( + AccessibilityServiceInfo.FLAG_REQUEST_MULTI_FINGER_GESTURES + | AccessibilityServiceInfo.FLAG_REQUEST_2_FINGER_PASSTHROUGH, + /* newValue= */ true); + resetTouchExplorePassThrough(); + } + + processorCursorState.onReloadPreferences(this); + processorPermissionsDialogs.onReloadPreferences(this); + + voiceCommandProcessor.setEchoRecognizedTextEnabled( + PreferencesActivityUtils.getDiagnosticPref( + this, + R.string.pref_echo_recognized_text_speech_key, + R.bool.pref_echo_recognized_text_default)); + + // Reload speech preferences. + pipeline.setOverlayEnabled( + PreferencesActivityUtils.getDiagnosticPref( + this, R.string.pref_tts_overlay_key, R.bool.pref_tts_overlay_default)); + pipeline.setUseIntonation( + VerbosityPreferences.getPreferenceValueBool( + prefs, + res, + res.getString(R.string.pref_intonation_key), + res.getBoolean(R.bool.pref_intonation_default))); + pipeline.setUsePunctuation( + getBooleanPref(R.string.pref_punctuation_key, R.bool.pref_punctuation_default)); + @CapitalLetterHandlingMethod + int capLetterFeedback = + Integer.parseInt( + VerbosityPreferences.getPreferenceValueString( + prefs, + res, + res.getString(R.string.pref_capital_letters_key), + res.getString(R.string.pref_capital_letters_default))); + speechController.setCapLetterFeedback(capLetterFeedback); + globalVariables.setGlobalSayCapital(capLetterFeedback == CAPITAL_LETTERS_TYPE_SPEAK_CAP); + pipeline.setSpeechPitch( + SharedPreferencesUtils.getFloatFromStringPref( + prefs, res, R.string.pref_speech_pitch_key, R.string.pref_speech_pitch_default)); + float speechRate = + SharedPreferencesUtils.getFloatFromStringPref( + prefs, res, R.string.pref_speech_rate_key, R.string.pref_speech_rate_default); + pipeline.setSpeechRate(speechRate); + globalVariables.setSpeechRate(speechRate); + int onScreenKeyboardPref = readOnScreenKeyboardEcho(); + textEventInterpreter.setOnScreenKeyboardEcho(onScreenKeyboardPref); + + int physicalKeyboardPref = readPhysicalKeyboardEcho(); + textEventInterpreter.setPhysicalKeyboardEcho(physicalKeyboardPref); + + boolean useAudioFocus = + getBooleanPref(R.string.pref_use_audio_focus_key, R.bool.pref_use_audio_focus_default); + pipeline.setUseAudioFocus(useAudioFocus); + globalVariables.setUseAudioFocus(useAudioFocus); + + // Speech volume is stored as int [0,100] and scaled to float [0,1]. + if (!FeatureSupport.hasAccessibilityAudioStream(this)) { + pipeline.setSpeechVolume( + SharedPreferencesUtils.getIntFromStringPref( + prefs, res, R.string.pref_speech_volume_key, R.string.pref_speech_volume_default) + / 100.0f); + } - reloadPreferences(); + if (speakPasswordsManager != null) { + speakPasswordsManager.onPreferencesChanged(); + } - dimScreenController.resume(); + // Reload feedback preferences. + int adjustment = + SharedPreferencesUtils.getIntFromStringPref( + prefs, res, R.string.pref_soundback_volume_key, R.string.pref_soundback_volume_default); + feedbackController.setVolumeAdjustment(adjustment / 100.0f); + + boolean hapticEnabled = + FeatureSupport.isVibratorSupported(getApplicationContext()) + && getBooleanPref(R.string.pref_vibration_key, R.bool.pref_vibration_default); + feedbackController.setHapticEnabled(hapticEnabled); + + boolean auditoryEnabled = + getBooleanPref(R.string.pref_soundback_key, R.bool.pref_soundback_default); + feedbackController.setAuditoryEnabled(auditoryEnabled); + + if (scrollPositionInterpreter != null) { + scrollPositionInterpreter.setVerboseAnnouncement( + VerbosityPreferences.getPreferenceValueBool( + prefs, + res, + res.getString(R.string.pref_verbose_scroll_announcement_key), + res.getBoolean(R.bool.pref_verbose_scroll_announcement_default))); + } - inputFocusInterpreter.initLastEditableFocusForGlobalVariables(); + boolean isFingerprintGestureAssigned = + FeatureSupport.isFingerprintGestureSupported(this) + && (gestureController.isFingerprintGestureAssigned( + FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_UP) + || gestureController.isFingerprintGestureAssigned( + FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_DOWN) + || gestureController.isFingerprintGestureAssigned( + FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_LEFT) + || gestureController.isFingerprintGestureAssigned( + FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_RIGHT)); + requestServiceFlag( + AccessibilityServiceInfo.FLAG_REQUEST_FINGERPRINT_GESTURES, isFingerprintGestureAssigned); + + // Update compositor preferences. + if (compositor != null) { + // Update preference: speak collection info. + boolean speakCollectionInfo = + VerbosityPreferences.getPreferenceValueBool( + prefs, + res, + res.getString(R.string.pref_speak_container_element_positions_key), + res.getBoolean(R.bool.pref_speak_container_element_positions_default)); + globalVariables.setSpeakCollectionInfo(speakCollectionInfo); + + // Update preference: speak roles. + boolean speakRoles = + VerbosityPreferences.getPreferenceValueBool( + prefs, + res, + res.getString(R.string.pref_speak_roles_key), + res.getBoolean(R.bool.pref_speak_roles_default)); + globalVariables.setSpeakRoles(speakRoles); + + // Update preference: speak system window titles. + boolean speakWindowTitle = + VerbosityPreferences.getPreferenceValueBool( + prefs, + res, + res.getString(R.string.pref_speak_system_window_titles_key), + res.getBoolean(R.bool.pref_speak_system_window_titles_default)); + globalVariables.setSpeakSystemWindowTitles(speakWindowTitle); + + // Update preference: description order. + String descriptionOrder = + SharedPreferencesUtils.getStringPref( + prefs, res, R.string.pref_node_desc_order_key, R.string.pref_node_desc_order_default); + globalVariables.setDescriptionOrder(prefValueToDescriptionOrder(res, descriptionOrder)); + + // Update preference: speak element IDs. + boolean speakElementIds = + getBooleanPref( + R.string.pref_speak_element_ids_key, R.bool.pref_speak_element_ids_default); + globalVariables.setSpeakElementIds(speakElementIds); + + // Update preference: speak usage hints. + boolean speakUsageHints = + VerbosityPreferences.getPreferenceValueBool( + prefs, + res, + res.getString(R.string.pref_a11y_hints_key), + res.getBoolean(R.bool.pref_a11y_hints_default)); + globalVariables.setUsageHintEnabled(speakUsageHints); + } - if (brailleImeForTalkBack != null) { - brailleImeForTalkBack.onTalkBackResumed(); + FocusIndicatorUtils.applyFocusAppearancePreference(this, prefs, res); } - brailleDisplay.start(); - } - @Override - public void unregisterReceiver(BroadcastReceiver receiver) { - try { - if (receiver != null) { - super.unregisterReceiver(receiver); - } - } catch (IllegalArgumentException e) { - LogUtils.e( - TAG, - "Do not unregister receiver as it was never registered: " - + receiver.getClass().getSimpleName()); + private int readOnScreenKeyboardEcho() { + return Integer.parseInt( + VerbosityPreferences.getPreferenceValueString( + prefs, + getResources(), + getResources().getString(R.string.pref_keyboard_echo_on_screen_key), + getResources().getString(R.string.pref_keyboard_echo_default))); } - } - private void unregisterReceivers(BroadcastReceiver... receivers) { - if (receivers == null) { - return; + private int readPhysicalKeyboardEcho() { + return Integer.parseInt( + VerbosityPreferences.getPreferenceValueString( + prefs, + getResources(), + getResources().getString(R.string.pref_keyboard_echo_physical_key), + getResources().getString(R.string.pref_keyboard_echo_default))); } - for (BroadcastReceiver receiver : receivers) { - unregisterReceiver(receiver); + + private void reloadPreferenceLogLevel() { + LogUtils.setLogLevel( + SharedPreferencesUtils.getIntFromStringPref( + prefs, getResources(), R.string.pref_log_level_key, R.string.pref_log_level_default)); + enforceDiagnosisModeLogging(); } - } - /** - * Registers listeners, sets service info, loads preferences. This should be called from {@link - * #onServiceConnected} and when TalkBack resumes from a suspended state. - */ - private void suspendInfrastructure() { - if (!isServiceActive()) { - LogUtils.e(TAG, "Attempted to suspend while already suspended"); - return; + private void enforceDiagnosisModeLogging() { + if ((LogUtils.getLogLevel() != Log.VERBOSE) + && PreferencesActivityUtils.isDiagnosisModeOn(prefs, getResources())) { + LogUtils.setLogLevel(Log.VERBOSE); + } } - setServiceState(ServiceStateListener.SERVICE_STATE_SHUTTING_DOWN); + private void reloadSilenceOnProximity() { + final boolean silenceOnProximity = + getBooleanPref(R.string.pref_proximity_key, R.bool.pref_proximity_default); + proximitySensorListener.setSilenceOnProximity(silenceOnProximity); + } - if (callStateMonitor != null) { - callStateMonitor.stopMonitoring(); + @Compositor.DescriptionOrder + private static int prefValueToDescriptionOrder(Resources resources, String value) { + if (TextUtils.equals( + value, resources.getString(R.string.pref_node_desc_order_value_role_name_state_pos))) { + return Compositor.DESC_ORDER_ROLE_NAME_STATE_POSITION; + } else if (TextUtils.equals( + value, resources.getString(R.string.pref_node_desc_order_value_state_name_role_pos))) { + return Compositor.DESC_ORDER_STATE_NAME_ROLE_POSITION; + } else if (TextUtils.equals( + value, resources.getString(R.string.pref_node_desc_order_value_name_role_state_pos))) { + return Compositor.DESC_ORDER_NAME_ROLE_STATE_POSITION; + } else { + LogUtils.e(TAG, "Unhandled description order preference value \"%s\"", value); + return Compositor.DESC_ORDER_STATE_NAME_ROLE_POSITION; + } } - if (voiceActionMonitor != null) { - voiceActionMonitor.onSuspendInfrastructure(); + /** + * Attempts to return the state of touch exploration. + * + *

Should only be called if {@link #supportsTouchScreen} is true. + * + * @return {@code true} if touch exploration is enabled, {@code false} if touch exploration is + * disabled or {@code null} if we couldn't get the state of touch exploration. + */ + private @Nullable Boolean isTouchExplorationEnabled() { + final AccessibilityServiceInfo info = getServiceInfo(); + if (info == null) { + LogUtils.e(TAG, "Failed to read touch exploration request state, service info was null"); + return null; + } + + return ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0); } - if (audioPlaybackMonitor != null) { - audioPlaybackMonitor.onSuspendInfrastructure(); + /** + * Attempts to change the state of touch exploration. + * + *

Should only be called if {@link #supportsTouchScreen} is true. + * + * @param requestedState {@code true} to request exploration. + * @return {@code true} if touch exploration is now enabled, {@code false} if touch exploration is + * now disabled or {@code null} if we couldn't get the state of touch exploration which means + * no change to touch exploration state occurred. + */ + private @Nullable Boolean requestTouchExploration(boolean requestedState) { + requestServiceFlag( + AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE, requestedState); + return isTouchExplorationEnabled(); } - dimScreenController.suspend(); + /** + * Attempts to change the service info flag. + * + * @param flags to specify the service flags to change. + * @param newValue {@code true} to request service flag change. + */ + private void requestServiceFlag(int flags, boolean newValue) { + final AccessibilityServiceInfo info = getServiceInfo(); + if (info == null) { + return; + } + + // No need to make changes if + // 1. newValue is true and current value of the requested flags are all set, or + // 2. newValue is false and current value of the requested flags are all clear. + boolean noChange = newValue ? ((info.flags & flags) == flags) : ((info.flags & flags) == 0); + if (noChange) { + return; + } - interruptAllFeedback(/* stopTtsSpeechCompletely */ false); + if (newValue) { + info.flags |= flags; + } else { + info.flags &= ~flags; + } - // Some apps depend on these being set to false when TalkBack is disabled. - if (supportsTouchScreen) { - requestTouchExploration(false); + LogUtils.v(TAG, "Accessibility Service flag changed: 0x%X", info.flags); + setServiceInfo(info); } - prefs.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); - prefs.unregisterOnSharedPreferenceChangeListener(analytics); + /** + * Launches the touch exploration tutorial if necessary. + * + * @return {@code true} if the tutorial is launched successfully. + */ + public boolean showTutorialIfNecessary() { + if (FeatureSupport.isArc() || FeatureSupport.isTv(getApplicationContext())) { + return false; + } - unregisterReceivers(ringerModeAndScreenMonitor, batteryMonitor, packageReceiver, volumeMonitor); + boolean isDeviceProvisioned = + Settings.Secure.getInt(getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 1) != 0; - if (volumeMonitor != null) { - volumeMonitor.releaseControl(); - } + if (isDeviceProvisioned && !isFirstTimeUser()) { + return false; + } - if (headphoneStateMonitor != null) { - headphoneStateMonitor.stopMonitoring(); - } + final int touchscreenState = getResources().getConfiguration().touchscreen; - // Remove any pending notifications that shouldn't persist. - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancelAll(); + if (touchscreenState != Configuration.TOUCHSCREEN_NOTOUCH && supportsTouchScreen) { + startActivity(TutorialInitiator.createFirstRunTutorialIntent(getApplicationContext())); + prefs.edit().putBoolean(PREF_FIRST_TIME_USER, false).apply(); + return true; + } - if (processorMagnification != null) { - processorMagnification.onSuspendInfrastructure(); + return false; } - if ((fingerprintGestureCallback != null) && (getFingerprintGestureController() != null)) { - getFingerprintGestureController() - .unregisterFingerprintGestureCallback(fingerprintGestureCallback); + private boolean isFirstTimeUser() { + return prefs.getBoolean(PREF_FIRST_TIME_USER, true); } - if (FeatureSupport.isFingerprintGestureSupported(this)) { - requestServiceFlag(AccessibilityServiceInfo.FLAG_REQUEST_FINGERPRINT_GESTURES, false); - } + /** + * Reloads preferences whenever their values change. + */ + private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = + new OnSharedPreferenceChangeListener() { + @Override + public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + LogUtils.d(TAG, "A shared preference changed: %s", key); + if (getResources() + .getString(R.string.pref_previous_global_window_animation_scale_key) + .equals(key)) { + // The stored animation factor is no related to TalkBack Settings at all. We skip to + // reloadPreferences to avoid the additional of Talkback re-configuration. + return; + } + reloadPreferences(); + } + }; - if (brailleImeForTalkBack != null) { - brailleImeForTalkBack.onTalkBackSuspended(); - } - brailleDisplay.stop(); - } + /** + * Called when the training page is switched. + * + *

This method should only be called by {@link IpcService}, which is the only class that can + * provide the ipcService argument. + */ + public static void handleTrainingPageSwitched(IpcService ipcService, @NonNull PageId pageId) { + if (ipcService == null) { + return; + } + + @Nullable TalkBackService talkBackService = TalkBackService.getInstance(); + if (talkBackService == null) { + return; + } + + @Nullable GestureController gestureController = talkBackService.gestureController; + if (gestureController != null) { + @Nullable PageConfig pageConfig = PageConfig.getPage(pageId); + gestureController.setCaptureGestureIdToAnnouncements( + pageConfig == null ? ImmutableMap.of() : pageConfig.getCaptureGestureIdToAnnouncements()); + } - /** Shuts down the infrastructure in case it has been initialized. */ - private void shutdownInfrastructure() { - setServiceState(ServiceStateListener.SERVICE_STATE_SHUTTING_DOWN); - // 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 - // We check the instance against null to prevent the premature service destroy (aka destroy - // before connected). - if (dimScreenController != null) { - dimScreenController.shutdown(); + // Request phone permission after TalkBack tutorial is finished. + @Nullable CallStateMonitor callStateMonitor = talkBackService.callStateMonitor; + @Nullable SharedPreferences prefs = talkBackService.prefs; + if (callStateMonitor != null && prefs != null && pageId == PAGE_ID_FINISHED) { + callStateMonitor.requestPhonePermissionIfNeeded(prefs); + } } - if (fullScreenReadActor != null) { - fullScreenReadActor.shutdown(); + public void onLockedBootCompleted(EventId eventId) { + if (serviceState == ServiceStateListener.SERVICE_STATE_INACTIVE) { + // onServiceConnected has not completed yet. We need to defer the boot completion + // callback until after onServiceConnected has run. + lockedBootCompletedPending = true; + } else { + // onServiceConnected has already completed, so we should run the callback now. + onLockedBootCompletedInternal(); + } } - if (labelManager != null) { - labelManager.shutdown(); + 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. + pipeline.onBoot(/* quiet= */ true); } - if (imageCaptioner != null) { - imageCaptioner.shutdown(); + 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. + if (pipeline != null) { + // pipeline can be null if a Boot Complete event arrives in between invocations of onCreated + // and onServiceConnected. + pipeline.onBoot(/* quiet= */ false); + } + + if (labelManager != null) { + labelManager.ensureLabelsLoaded(); + } + + // The invocation of installActivity() enables immediate access to code and resources of split + // APKs. It can be invoked even though we are a Service and not an Activity. + // Call SplitCompat.install after local filesystem accessible in boot process. + boolean splitCompatInstallSuccess = SplitCompatUtils.installActivity(this); + + // In theory, the boolean returned by installActivity will be false only for API 20 or lower. + if (!splitCompatInstallSuccess) { + Log.e(TAG, "SplitCompatUtils.installActivity() failed"); + } } - if (proximitySensorListener != null) { - proximitySensorListener.shutdown(); + @Override + public void uncaughtException(Thread thread, Throwable ex) { + try { + if (dimScreenController != null) { + dimScreenController.shutdown(); + } + + if (menuManager != null && menuManager.isMenuShowing()) { + menuManager.dismissAll(); + } + } catch (Exception e) { + // Do nothing. + } finally { + if (systemUeh != null + && getServiceState() != ServiceStateListener.SERVICE_STATE_SHUTTING_DOWN) { + systemUeh.uncaughtException(thread, ex); + } else { + // There is either no default exception handler available, or the service is in the middle + // of shutting down. + // If the service is in the middle of shutting down exceptions would cause the service to + // crash. + LogUtils.e(TAG, "Received exception while shutting down.", ex); + } + } } - if (feedbackController != null) { - feedbackController.shutdown(); + + public void setTestingListener(TalkBackListener testingListener) { + accessibilityEventProcessor.setTestingListener(testingListener); } - if (pipeline != null) { - pipeline.shutdown(); + + public boolean isScreenOrientationLandscape() { + Configuration config = getResources().getConfiguration(); + if (config == null) { + return false; + } + return config.orientation == Configuration.ORIENTATION_LANDSCAPE; } - if (analytics != null) { - analytics.onTalkBackServiceStopped(); + + public InputModeManager getInputModeManager() { + return inputModeManager; } - } - /** - * Adds an event listener. - * - * @param listener The listener to add. - */ - public void addEventListener(AccessibilityEventListener listener) { - accessibilityEventProcessor.addAccessibilityEventListener(listener); - } + /** + * Runnable to run after announcing "TalkBack off". + */ + private static final class DisableTalkBackCompleteAction implements UtteranceCompleteRunnable { + boolean isDone = false; - /** - * Posts a {@link Runnable} to removes an event listener. This is safe to call from inside {@link - * AccessibilityEventListener#onAccessibilityEvent(AccessibilityEvent, EventId)}. - * - * @param listener The listener to remove. - */ - public void postRemoveEventListener(final AccessibilityEventListener listener) { - accessibilityEventProcessor.postRemoveAccessibilityEventListener(listener); - } - - /** Returns a boolean preference by resource id. */ - private boolean getBooleanPref(int prefKeyResId, int prefDefaultResId) { - return SharedPreferencesUtils.getBooleanPref( - prefs, getResources(), prefKeyResId, prefDefaultResId); - } - - /** - * When the device supports {@link AccessibilityService#setAnimationScale(float)}, system will - * determine to disable animation feature when TalkBack is on, and resume it after TalkBack is - * off. - * - * @param enable {@code false} to request the disable of animation, and {@code true} to resume the - * animation. - */ - private void enableAnimation(boolean enable) { - if (!FeatureSupport.supportsServiceControlOfGlobalAnimations()) { - return; - } - if (enable) { - if (prefs.contains(getString(R.string.pref_previous_global_window_animation_scale_key))) { - float scale = - SharedPreferencesUtils.getFloatFromStringPref( - prefs, - getResources(), - R.string.pref_previous_global_window_animation_scale_key, - R.string.pref_window_animation_scale_default); - if (scale > ANIMATION_OFF && SettingsUtils.isAnimationDisabled(this)) { - // Resume animation when the record value is meaningful (greater than zero); - setAnimationScale(scale); - } - prefs - .edit() - .remove(getString(R.string.pref_previous_global_window_animation_scale_key)) - .apply(); - } - } else { - if (!SettingsUtils.isAnimationDisabled(this)) { - prefs - .edit() - .putString( - getString(R.string.pref_previous_global_window_animation_scale_key), - Float.toString( - Settings.Global.getFloat( - getContentResolver(), Settings.Global.WINDOW_ANIMATION_SCALE, 1))) - .apply(); - } - // Disable animation; - setAnimationScale(ANIMATION_OFF); - } - } - - /** Reloads service preferences. */ - private void reloadPreferences() { - final Resources res = getResources(); - - LogUtils.v( - TAG, - "TalkBackService.reloadPreferences() diagnostic mode=%s", - PreferencesActivityUtils.isDiagnosisModeOn(prefs, res)); - - // Preferece to reduce window announcement delay. - boolean reduceDelayPref = - getBooleanPref( - R.string.pref_reduce_window_delay_key, R.bool.pref_reduce_window_delay_default); - if (processorScreen != null && processorScreen.getWindowEventInterpreter() != null) { - processorScreen.getWindowEventInterpreter().setReduceDelayPref(reduceDelayPref); - enableAnimation(!reduceDelayPref); - } - - // If performance statistics changing enabled setting... clear collected stats. - boolean performanceEnabled = - getBooleanPref(R.string.pref_performance_stats_key, R.bool.pref_performance_stats_default); - Performance performance = Performance.getInstance(); - if (performance.getEnabled() != performanceEnabled) { - performance.clearRecentEvents(); - performance.clearAllStats(); - performance.setEnabled(performanceEnabled); - } - - boolean logOverlayEnabled = - PreferencesActivityUtils.getDiagnosticPref( - prefs, res, R.string.pref_log_overlay_key, R.bool.pref_log_overlay_default); - diagnosticOverlayController.setLogOverlayEnabled(logOverlayEnabled); - - accessibilityEventProcessor.setSpeakWhenScreenOff( - VerbosityPreferences.getPreferenceValueBool( - prefs, - res, - res.getString(R.string.pref_screenoff_key), - res.getBoolean(R.bool.pref_screenoff_default))); - - accessibilityEventProcessor.setDumpEventMask( - prefs.getInt(res.getString(R.string.pref_dump_event_mask_key), 0)); - - reloadSilenceOnProximity(); - reloadPreferenceLogLevel(); - - final boolean useSingleTap = - getBooleanPref(R.string.pref_single_tap_key, R.bool.pref_single_tap_default); - globalVariables.setUseSingleTap(useSingleTap); - accessibilityFocusInterpreter.setSingleTapEnabled(useSingleTap); - accessibilityFocusInterpreter.setTypingMethod( - SharedPreferencesUtils.getIntFromStringPref( - prefs, - res, - R.string.pref_typing_confirmation_key, - R.string.pref_typing_confirmation_default)); - accessibilityFocusInterpreter.setTypingLongPressDurationMs( - SharedPreferencesUtils.getIntFromStringPref( - prefs, - res, - R.string.pref_typing_long_press_duration_key, - R.string.pref_typing_long_press_duration_default)); - globalVariables.setInterpretAsEntryKey( - accessibilityFocusInterpreter.getTypingMethod() == FORCE_LIFT_TO_TYPE_ON_IME); - - if (supportsTouchScreen && !isBrailleKeyboardActivated) { - // Touch exploration *must* be enabled on TVs for TalkBack to function. - final boolean touchExploration = - (FeatureSupport.isTv(this) - || getBooleanPref( - R.string.pref_explore_by_touch_key, R.bool.pref_explore_by_touch_default)); - requestTouchExploration(touchExploration); - } - - if (FeatureSupport.isMultiFingerGestureSupported()) { - requestServiceFlag( - AccessibilityServiceInfo.FLAG_REQUEST_MULTI_FINGER_GESTURES - | AccessibilityServiceInfo.FLAG_REQUEST_2_FINGER_PASSTHROUGH, - /* newValue= */ true); - resetTouchExplorePassThrough(); - } - - processorCursorState.onReloadPreferences(this); - processorPermissionsDialogs.onReloadPreferences(this); - - voiceCommandProcessor.setEchoRecognizedTextEnabled( - PreferencesActivityUtils.getDiagnosticPref( - this, - R.string.pref_echo_recognized_text_speech_key, - R.bool.pref_echo_recognized_text_default)); - - // Reload speech preferences. - pipeline.setOverlayEnabled( - PreferencesActivityUtils.getDiagnosticPref( - this, R.string.pref_tts_overlay_key, R.bool.pref_tts_overlay_default)); - pipeline.setUseIntonation( - VerbosityPreferences.getPreferenceValueBool( - prefs, - res, - res.getString(R.string.pref_intonation_key), - res.getBoolean(R.bool.pref_intonation_default))); - pipeline.setUsePunctuation( - getBooleanPref(R.string.pref_punctuation_key, R.bool.pref_punctuation_default)); - @CapitalLetterHandlingMethod - int capLetterFeedback = - Integer.parseInt( - VerbosityPreferences.getPreferenceValueString( - prefs, - res, - res.getString(R.string.pref_capital_letters_key), - res.getString(R.string.pref_capital_letters_default))); - speechController.setCapLetterFeedback(capLetterFeedback); - globalVariables.setGlobalSayCapital(capLetterFeedback == CAPITAL_LETTERS_TYPE_SPEAK_CAP); - pipeline.setSpeechPitch( - SharedPreferencesUtils.getFloatFromStringPref( - prefs, res, R.string.pref_speech_pitch_key, R.string.pref_speech_pitch_default)); - float speechRate = - SharedPreferencesUtils.getFloatFromStringPref( - prefs, res, R.string.pref_speech_rate_key, R.string.pref_speech_rate_default); - pipeline.setSpeechRate(speechRate); - globalVariables.setSpeechRate(speechRate); - int onScreenKeyboardPref = readOnScreenKeyboardEcho(); - textEventInterpreter.setOnScreenKeyboardEcho(onScreenKeyboardPref); - - int physicalKeyboardPref = readPhysicalKeyboardEcho(); - textEventInterpreter.setPhysicalKeyboardEcho(physicalKeyboardPref); - - boolean useAudioFocus = - getBooleanPref(R.string.pref_use_audio_focus_key, R.bool.pref_use_audio_focus_default); - pipeline.setUseAudioFocus(useAudioFocus); - globalVariables.setUseAudioFocus(useAudioFocus); - - // Speech volume is stored as int [0,100] and scaled to float [0,1]. - if (!FeatureSupport.hasAccessibilityAudioStream(this)) { - pipeline.setSpeechVolume( - SharedPreferencesUtils.getIntFromStringPref( - prefs, res, R.string.pref_speech_volume_key, R.string.pref_speech_volume_default) - / 100.0f); - } - - if (speakPasswordsManager != null) { - speakPasswordsManager.onPreferencesChanged(); - } - - // Reload feedback preferences. - int adjustment = - SharedPreferencesUtils.getIntFromStringPref( - prefs, res, R.string.pref_soundback_volume_key, R.string.pref_soundback_volume_default); - feedbackController.setVolumeAdjustment(adjustment / 100.0f); - - boolean hapticEnabled = - FeatureSupport.isVibratorSupported(getApplicationContext()) - && getBooleanPref(R.string.pref_vibration_key, R.bool.pref_vibration_default); - feedbackController.setHapticEnabled(hapticEnabled); - - boolean auditoryEnabled = - getBooleanPref(R.string.pref_soundback_key, R.bool.pref_soundback_default); - feedbackController.setAuditoryEnabled(auditoryEnabled); - - if (scrollPositionInterpreter != null) { - scrollPositionInterpreter.setVerboseAnnouncement( - VerbosityPreferences.getPreferenceValueBool( - prefs, - res, - res.getString(R.string.pref_verbose_scroll_announcement_key), - res.getBoolean(R.bool.pref_verbose_scroll_announcement_default))); - } - - boolean isFingerprintGestureAssigned = - FeatureSupport.isFingerprintGestureSupported(this) - && (gestureController.isFingerprintGestureAssigned( - FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_UP) - || gestureController.isFingerprintGestureAssigned( - FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_DOWN) - || gestureController.isFingerprintGestureAssigned( - FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_LEFT) - || gestureController.isFingerprintGestureAssigned( - FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_RIGHT)); - requestServiceFlag( - AccessibilityServiceInfo.FLAG_REQUEST_FINGERPRINT_GESTURES, isFingerprintGestureAssigned); - - // Update compositor preferences. - if (compositor != null) { - // Update preference: speak collection info. - boolean speakCollectionInfo = - VerbosityPreferences.getPreferenceValueBool( - prefs, - res, - res.getString(R.string.pref_speak_container_element_positions_key), - res.getBoolean(R.bool.pref_speak_container_element_positions_default)); - globalVariables.setSpeakCollectionInfo(speakCollectionInfo); - - // Update preference: speak roles. - boolean speakRoles = - VerbosityPreferences.getPreferenceValueBool( - prefs, - res, - res.getString(R.string.pref_speak_roles_key), - res.getBoolean(R.bool.pref_speak_roles_default)); - globalVariables.setSpeakRoles(speakRoles); - - // Update preference: speak system window titles. - boolean speakWindowTitle = - VerbosityPreferences.getPreferenceValueBool( - prefs, - res, - res.getString(R.string.pref_speak_system_window_titles_key), - res.getBoolean(R.bool.pref_speak_system_window_titles_default)); - globalVariables.setSpeakSystemWindowTitles(speakWindowTitle); - - // Update preference: description order. - String descriptionOrder = - SharedPreferencesUtils.getStringPref( - prefs, res, R.string.pref_node_desc_order_key, R.string.pref_node_desc_order_default); - globalVariables.setDescriptionOrder(prefValueToDescriptionOrder(res, descriptionOrder)); - - // Update preference: speak element IDs. - boolean speakElementIds = - getBooleanPref( - R.string.pref_speak_element_ids_key, R.bool.pref_speak_element_ids_default); - globalVariables.setSpeakElementIds(speakElementIds); - - // Update preference: speak usage hints. - boolean speakUsageHints = - VerbosityPreferences.getPreferenceValueBool( - prefs, - res, - res.getString(R.string.pref_a11y_hints_key), - res.getBoolean(R.bool.pref_a11y_hints_default)); - globalVariables.setUsageHintEnabled(speakUsageHints); - } - - FocusIndicatorUtils.applyFocusAppearancePreference(this, prefs, res); - } - - private int readOnScreenKeyboardEcho() { - return Integer.parseInt( - VerbosityPreferences.getPreferenceValueString( - prefs, - getResources(), - getResources().getString(R.string.pref_keyboard_echo_on_screen_key), - getResources().getString(R.string.pref_keyboard_echo_default))); - } - - private int readPhysicalKeyboardEcho() { - return Integer.parseInt( - VerbosityPreferences.getPreferenceValueString( - prefs, - getResources(), - getResources().getString(R.string.pref_keyboard_echo_physical_key), - getResources().getString(R.string.pref_keyboard_echo_default))); - } - - private void reloadPreferenceLogLevel() { - LogUtils.setLogLevel( - SharedPreferencesUtils.getIntFromStringPref( - prefs, getResources(), R.string.pref_log_level_key, R.string.pref_log_level_default)); - enforceDiagnosisModeLogging(); - } - - private void enforceDiagnosisModeLogging() { - if ((LogUtils.getLogLevel() != Log.VERBOSE) - && PreferencesActivityUtils.isDiagnosisModeOn(prefs, getResources())) { - LogUtils.setLogLevel(Log.VERBOSE); - } - } - - private void reloadSilenceOnProximity() { - final boolean silenceOnProximity = - getBooleanPref(R.string.pref_proximity_key, R.bool.pref_proximity_default); - proximitySensorListener.setSilenceOnProximity(silenceOnProximity); - } - - @Compositor.DescriptionOrder - private static int prefValueToDescriptionOrder(Resources resources, String value) { - if (TextUtils.equals( - value, resources.getString(R.string.pref_node_desc_order_value_role_name_state_pos))) { - return Compositor.DESC_ORDER_ROLE_NAME_STATE_POSITION; - } else if (TextUtils.equals( - value, resources.getString(R.string.pref_node_desc_order_value_state_name_role_pos))) { - return Compositor.DESC_ORDER_STATE_NAME_ROLE_POSITION; - } else if (TextUtils.equals( - value, resources.getString(R.string.pref_node_desc_order_value_name_role_state_pos))) { - return Compositor.DESC_ORDER_NAME_ROLE_STATE_POSITION; - } else { - LogUtils.e(TAG, "Unhandled description order preference value \"%s\"", value); - return Compositor.DESC_ORDER_STATE_NAME_ROLE_POSITION; - } - } - - /** - * Attempts to return the state of touch exploration. - * - *

Should only be called if {@link #supportsTouchScreen} is true. - * - * @return {@code true} if touch exploration is enabled, {@code false} if touch exploration is - * disabled or {@code null} if we couldn't get the state of touch exploration. - */ - private @Nullable Boolean isTouchExplorationEnabled() { - final AccessibilityServiceInfo info = getServiceInfo(); - if (info == null) { - LogUtils.e(TAG, "Failed to read touch exploration request state, service info was null"); - return null; - } - - return ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0); - } - - /** - * Attempts to change the state of touch exploration. - * - *

Should only be called if {@link #supportsTouchScreen} is true. - * - * @param requestedState {@code true} to request exploration. - * @return {@code true} if touch exploration is now enabled, {@code false} if touch exploration is - * now disabled or {@code null} if we couldn't get the state of touch exploration which means - * no change to touch exploration state occurred. - */ - private @Nullable Boolean requestTouchExploration(boolean requestedState) { - requestServiceFlag( - AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE, requestedState); - return isTouchExplorationEnabled(); - } - - /** - * Attempts to change the service info flag. - * - * @param flags to specify the service flags to change. - * @param newValue {@code true} to request service flag change. - */ - private void requestServiceFlag(int flags, boolean newValue) { - final AccessibilityServiceInfo info = getServiceInfo(); - if (info == null) { - return; - } - - // No need to make changes if - // 1. newValue is true and current value of the requested flags are all set, or - // 2. newValue is false and current value of the requested flags are all clear. - boolean noChange = newValue ? ((info.flags & flags) == flags) : ((info.flags & flags) == 0); - if (noChange) { - return; - } - - if (newValue) { - info.flags |= flags; - } else { - info.flags &= ~flags; - } - - LogUtils.v(TAG, "Accessibility Service flag changed: 0x%X", info.flags); - setServiceInfo(info); - } - - /** - * Launches the touch exploration tutorial if necessary. - * - * @return {@code true} if the tutorial is launched successfully. - */ - public boolean showTutorialIfNecessary() { - if (FeatureSupport.isArc() || FeatureSupport.isTv(getApplicationContext())) { - return false; - } - - boolean 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 && supportsTouchScreen) { - startActivity(TutorialInitiator.createFirstRunTutorialIntent(getApplicationContext())); - prefs.edit().putBoolean(PREF_FIRST_TIME_USER, false).apply(); - return true; - } - - return false; - } - - private boolean isFirstTimeUser() { - return prefs.getBoolean(PREF_FIRST_TIME_USER, true); - } - - /** Reloads preferences whenever their values change. */ - private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = - new OnSharedPreferenceChangeListener() { @Override - public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { - LogUtils.d(TAG, "A shared preference changed: %s", key); - if (getResources() - .getString(R.string.pref_previous_global_window_animation_scale_key) - .equals(key)) { - // The stored animation factor is no related to TalkBack Settings at all. We skip to - // reloadPreferences to avoid the additional of Talkback re-configuration. - return; - } - reloadPreferences(); - } - }; - - /** - * Called when the training page is switched. - * - *

This method should only be called by {@link IpcService}, which is the only class that can - * provide the ipcService argument. - */ - public static void handleTrainingPageSwitched(IpcService ipcService, @NonNull PageId pageId) { - if (ipcService == null) { - return; - } - - @Nullable TalkBackService talkBackService = TalkBackService.getInstance(); - if (talkBackService == null) { - return; - } - - @Nullable GestureController gestureController = talkBackService.gestureController; - if (gestureController != null) { - @Nullable PageConfig pageConfig = PageConfig.getPage(pageId); - gestureController.setCaptureGestureIdToAnnouncements( - pageConfig == null ? ImmutableMap.of() : pageConfig.getCaptureGestureIdToAnnouncements()); - } - - // Request phone permission after TalkBack tutorial is finished. - @Nullable CallStateMonitor callStateMonitor = talkBackService.callStateMonitor; - @Nullable SharedPreferences prefs = talkBackService.prefs; - if (callStateMonitor != null && prefs != null && pageId == PAGE_ID_FINISHED) { - callStateMonitor.requestPhonePermissionIfNeeded(prefs); - } - } - - public void onLockedBootCompleted(EventId eventId) { - if (serviceState == ServiceStateListener.SERVICE_STATE_INACTIVE) { - // onServiceConnected has not completed yet. We need to defer the boot completion - // callback until after onServiceConnected has run. - lockedBootCompletedPending = 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. - pipeline.onBoot(/* quiet= */ true); - } - - 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. - if (pipeline != null) { - // pipeline can be null if a Boot Complete event arrives in between invocations of onCreated - // and onServiceConnected. - pipeline.onBoot(/* quiet= */ false); - } - - if (labelManager != null) { - labelManager.ensureLabelsLoaded(); - } - - // The invocation of installActivity() enables immediate access to code and resources of split - // APKs. It can be invoked even though we are a Service and not an Activity. - // Call SplitCompat.install after local filesystem accessible in boot process. - boolean splitCompatInstallSuccess = SplitCompatUtils.installActivity(this); - - // In theory, the boolean returned by installActivity will be false only for API 20 or lower. - if (!splitCompatInstallSuccess) { - Log.e(TAG, "SplitCompatUtils.installActivity() failed"); - } - } - - @Override - public void uncaughtException(Thread thread, Throwable ex) { - try { - if (dimScreenController != null) { - dimScreenController.shutdown(); - } - - if (menuManager != null && menuManager.isMenuShowing()) { - menuManager.dismissAll(); - } - } catch (Exception e) { - // Do nothing. - } finally { - if (systemUeh != null - && getServiceState() != ServiceStateListener.SERVICE_STATE_SHUTTING_DOWN) { - systemUeh.uncaughtException(thread, ex); - } else { - // There is either no default exception handler available, or the service is in the middle - // of shutting down. - // If the service is in the middle of shutting down exceptions would cause the service to - // crash. - LogUtils.e(TAG, "Received exception while shutting down.", ex); - } - } - } - - public void setTestingListener(TalkBackListener testingListener) { - accessibilityEventProcessor.setTestingListener(testingListener); - } - - public boolean isScreenOrientationLandscape() { - Configuration config = getResources().getConfiguration(); - if (config == null) { - return false; - } - return config.orientation == Configuration.ORIENTATION_LANDSCAPE; - } - - public InputModeManager getInputModeManager() { - return inputModeManager; - } - - /** Runnable to run after announcing "TalkBack off". */ - private static final class DisableTalkBackCompleteAction implements UtteranceCompleteRunnable { - boolean isDone = false; - - @Override - public void run(int status) { - synchronized (DisableTalkBackCompleteAction.this) { - isDone = true; - DisableTalkBackCompleteAction.this.notifyAll(); - } + public void run(int status) { + synchronized (DisableTalkBackCompleteAction.this) { + isDone = true; + DisableTalkBackCompleteAction.this.notifyAll(); + } + } } - } - /** Watches the proximity sensor, and silences speech when it's triggered. */ - public class ProximitySensorListener { - /** Proximity sensor for implementing "shut up" functionality. */ - private @Nullable ProximitySensor proximitySensor; + /** + * Watches the proximity sensor, and silences speech when it's triggered. + */ + public class ProximitySensorListener { + /** + * Proximity sensor for implementing "shut up" functionality. + */ + private @Nullable ProximitySensor proximitySensor; + + private TalkBackService service; + + /** + * Whether to use the proximity sensor to silence speech. + */ + private boolean silenceOnProximity; + + /** + * Whether or not the screen is on. This is set by RingerModeAndScreenMonitor and used by + * SpeechControllerImpl to determine if the ProximitySensor should be on or off. + */ + private boolean screenIsOn; + + public ProximitySensorListener(TalkBackService service) { + this.service = service; + screenIsOn = true; + } - private TalkBackService service; + public void shutdown() { + setProximitySensorState(false); + } - /** Whether to use the proximity sensor to silence speech. */ - private boolean silenceOnProximity; + public void setScreenIsOn(boolean screenIsOn) { + this.screenIsOn = screenIsOn; - /** - * Whether or not the screen is on. This is set by RingerModeAndScreenMonitor and used by - * SpeechControllerImpl to determine if the ProximitySensor should be on or off. - */ - private boolean screenIsOn; + // The proximity sensor should always be on when the screen is on so + // that the proximity gesture can be used to silence all apps. + if (this.screenIsOn) { + setProximitySensorState(true); + } + } - public ProximitySensorListener(TalkBackService service) { - this.service = service; - screenIsOn = true; - } + /** + * Sets whether or not the proximity sensor should be used to silence speech. + * + *

This should be called when the user changes the state of the "silence on proximity" + * preference. + */ + public void setSilenceOnProximity(boolean silenceOnProximity) { + this.silenceOnProximity = silenceOnProximity; + + // Propagate the proximity sensor change. + setProximitySensorState(silenceOnProximity); + } + + /** + * Enables/disables the proximity sensor. The proximity sensor should be disabled when not in + * use to save battery. + * + *

This is a no-op if the user has turned off the "silence on proximity" preference. + * + * @param enabled {@code true} if the proximity sensor should be enabled, {@code false} + * otherwise. + */ + // TODO: Rewrite for readability. + public void setProximitySensorState(boolean enabled) { + if (proximitySensor != null) { + // Should we be using the proximity sensor at all? + if (!silenceOnProximity) { + proximitySensor.stop(); + proximitySensor = null; + return; + } + + if (!service.isInstanceActive()) { + proximitySensor.stop(); + return; + } + } else { + // Do we need to initialize the proximity sensor? + if (enabled && silenceOnProximity) { + proximitySensor = new ProximitySensor(service); + proximitySensor.setProximityChangeListener(pipeline.getProximityChangeListener()); + } else { + // Since proximitySensor is null, we can return here. + return; + } + } - public void shutdown() { - setProximitySensorState(false); + // Manage the proximity sensor state. + if (enabled) { + proximitySensor.start(); + } else { + proximitySensor.stop(); + } + } + + public void setProximitySensorStateByScreen() { + setProximitySensorState(screenIsOn); + } } - public void setScreenIsOn(boolean screenIsOn) { - this.screenIsOn = screenIsOn; + private void resetTouchExplorePassThrough() { + if (FeatureSupport.supportPassthrough()) { + if (isBrailleKeyboardActivated) { + return; + } + pipeline + .getFeedbackReturner() + .returnFeedback( + Performance.EVENT_ID_UNTRACKED, Feedback.passThroughMode(DISABLE_PASSTHROUGH)); + } + } - // The proximity sensor should always be on when the screen is on so - // that the proximity gesture can be used to silence all apps. - if (this.screenIsOn) { - setProximitySensorState(true); - } + protected boolean shouldUseTalkbackGestureDetection() { + // TODO: Control the feature by p/h flag. + if (useServiceGestureDetection == null) { + SharedPreferences sharedPreferences = SharedPreferencesUtils.getSharedPreferences(this); + useServiceGestureDetection = + sharedPreferences.getBoolean( + getString(R.string.pref_talkback_gesture_detection_key), true); + } + return useServiceGestureDetection; } - /** - * Sets whether or not the proximity sensor should be used to silence speech. - * - *

This should be called when the user changes the state of the "silence on proximity" - * preference. - */ - public void setSilenceOnProximity(boolean silenceOnProximity) { - this.silenceOnProximity = silenceOnProximity; + // INFO: TalkBack For Developers modification + public void performGesture(String gestureString) { + Performance perf = Performance.getInstance(); + EventId eventId = perf.onEventReceived(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_UNKNOWN)); + gestureController.performAction(gestureString, eventId); + } - // Propagate the proximity sensor change. - setProximitySensorState(silenceOnProximity); + public void moveAtGranularity(SelectorController.Granularity granularity, boolean isNext) { + Performance perf = Performance.getInstance(); + EventId eventId = perf.onEventReceived(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_UNKNOWN)); + selectorController.moveAtGranularity(eventId, granularity, isNext); } /** - * Enables/disables the proximity sensor. The proximity sensor should be disabled when not in - * use to save battery. + * The build needs to be on 33 to work. However the registration of the + * [TouchInteractionController] instance will prevent single touch to focus from working. + * This is not the touch exploration, that will work fine. * - *

This is a no-op if the user has turned off the "silence on proximity" preference. + * I am not sure what benefit this listener brings, and I am reluctant to remove it, but since + * it's only Tiramisu builds and most of what I need works fine - I will just do this in the + * meantime. This little "bug" has cost me at least three weeks of work, including weekends * - * @param enabled {@code true} if the proximity sensor should be enabled, {@code false} - * otherwise. - */ - // TODO: Rewrite for readability. - public void setProximitySensorState(boolean enabled) { - if (proximitySensor != null) { - // Should we be using the proximity sensor at all? - if (!silenceOnProximity) { - proximitySensor.stop(); - proximitySensor = null; - return; - } - - if (!service.isInstanceActive()) { - proximitySensor.stop(); - return; - } - } else { - // Do we need to initialize the proximity sensor? - if (enabled && silenceOnProximity) { - proximitySensor = new ProximitySensor(service); - proximitySensor.setProximityChangeListener(pipeline.getProximityChangeListener()); - } else { - // Since proximitySensor is null, we can return here. - return; - } - } - - // Manage the proximity sensor state. - if (enabled) { - proximitySensor.start(); - } else { - proximitySensor.stop(); - } - } - - public void setProximitySensorStateByScreen() { - setProximitySensorState(screenIsOn); - } - } - - private void resetTouchExplorePassThrough() { - if (FeatureSupport.supportPassthrough()) { - if (isBrailleKeyboardActivated) { - return; - } - pipeline - .getFeedbackReturner() - .returnFeedback( - Performance.EVENT_ID_UNTRACKED, Feedback.passThroughMode(DISABLE_PASSTHROUGH)); - } - } - - protected boolean shouldUseTalkbackGestureDetection() { - // TODO: Control the feature by p/h flag. - if (useServiceGestureDetection == null) { - SharedPreferences sharedPreferences = SharedPreferencesUtils.getSharedPreferences(this); - useServiceGestureDetection = - sharedPreferences.getBoolean( - getString(R.string.pref_talkback_gesture_detection_key), true); - } - return useServiceGestureDetection; - } - - private void registerGestureDetection() { - if (BuildVersionUtils.isAtLeastT()) { - List displays = WindowUtils.getAllDisplays(getApplicationContext()); - Executor gestureExecutor = Executors.newSingleThreadExecutor(); - for (Display display : displays) { - Context context = createDisplayContext(display); - @Nullable TouchInteractionController touchInteractionController = - getTouchInteractionController(display.getDisplayId()); - if (touchInteractionController == null) { - continue; - } - TouchInteractionMonitor touchInteractionMonitor = - new TouchInteractionMonitor(context, touchInteractionController, this); - touchInteractionMonitor.setMultiFingerGesturesEnabled(true); - touchInteractionMonitor.setTwoFingerPassthroughEnabled(true); - touchInteractionMonitor.setServiceHandlesDoubleTap(true); - touchInteractionController.registerCallback(gestureExecutor, touchInteractionMonitor); - displayIdToTouchInteractionMonitor.put(display.getDisplayId(), touchInteractionMonitor); - LogUtils.i(TAG, "Enabling service gesture detection on display %d", display.getDisplayId()); - } - } - } - - private void unregisterGestureDetection() { - if (BuildVersionUtils.isAtLeastT()) { - List displays = WindowUtils.getAllDisplays(getApplicationContext()); - for (Display display : displays) { - @Nullable TouchInteractionController touchInteractionController = - getTouchInteractionController(display.getDisplayId()); - TouchInteractionMonitor touchInteractionMonitor = - displayIdToTouchInteractionMonitor.get(display.getDisplayId()); - if (touchInteractionController == null || touchInteractionMonitor == null) { - continue; - } - touchInteractionController.unregisterCallback(touchInteractionMonitor); - } - displayIdToTouchInteractionMonitor.clear(); - } - } + * It is odd to me that this is not registered if the tutorial is shown + * + * @return if the [TouchInteractionController] registration breaks TalkBack + */ + private boolean hasOneDisplay() { + List displays = WindowUtils.getAllDisplays(getApplicationContext()); + return displays.size() == 1; + } + // ------------------------------------------ + + private void registerGestureDetection() { + if (hasOneDisplay()) return; + if (BuildVersionUtils.isAtLeastT()) { + List displays = WindowUtils.getAllDisplays(getApplicationContext()); + Executor gestureExecutor = Executors.newSingleThreadExecutor(); + for (Display display : displays) { + Context context = createDisplayContext(display); + @Nullable TouchInteractionController touchInteractionController = + getTouchInteractionController(display.getDisplayId()); + if (touchInteractionController == null) { + continue; + } + TouchInteractionMonitor touchInteractionMonitor = + null; + touchInteractionMonitor = new TouchInteractionMonitor(context, touchInteractionController, this); + touchInteractionMonitor.setMultiFingerGesturesEnabled(true); + touchInteractionMonitor.setTwoFingerPassthroughEnabled(true); + touchInteractionMonitor.setServiceHandlesDoubleTap(true); + touchInteractionController.registerCallback(gestureExecutor, touchInteractionMonitor); + displayIdToTouchInteractionMonitor.put(display.getDisplayId(), touchInteractionMonitor); + LogUtils.i(TAG, "Enabling service gesture detection on display %d", display.getDisplayId()); + } + } + } + + private void unregisterGestureDetection() { + if (hasOneDisplay()) return; + if (BuildVersionUtils.isAtLeastT()) { + List displays = WindowUtils.getAllDisplays(getApplicationContext()); + for (Display display : displays) { + @Nullable TouchInteractionController touchInteractionController = + getTouchInteractionController(display.getDisplayId()); + TouchInteractionMonitor touchInteractionMonitor = + displayIdToTouchInteractionMonitor.get(display.getDisplayId()); + if (touchInteractionController == null || touchInteractionMonitor == null) { + continue; + } + touchInteractionController.unregisterCallback(touchInteractionMonitor); + } + displayIdToTouchInteractionMonitor.clear(); + } + } } diff --git a/talkback/src/main/java/com/google/android/accessibility/talkback/adb/A11yAction.java b/talkback/src/main/java/com/google/android/accessibility/talkback/adb/A11yAction.java new file mode 100644 index 000000000..d7e2e71ef --- /dev/null +++ b/talkback/src/main/java/com/google/android/accessibility/talkback/adb/A11yAction.java @@ -0,0 +1,81 @@ +package com.google.android.accessibility.talkback.adb; + +import com.google.android.accessibility.talkback.R; +import com.google.android.accessibility.talkback.selector.SelectorController; + +enum A11yAction { + UNASSIGNED(R.string.shortcut_value_unassigned), + PREVIOUS(R.string.shortcut_value_previous), + NEXT(R.string.shortcut_value_next), + BACK(R.string.shortcut_value_back), + FIRST_IN_SCREEN(R.string.shortcut_value_first_in_screen), + SUMMARY(R.string.shortcut_value_summary), + VOICE_COMMANDS(R.string.shortcut_value_voice_commands), + LAST_IN_SCREEN(R.string.shortcut_value_last_in_screen), + NEXT_GRANULARITY(R.string.shortcut_value_next_granularity), + PREVIOUS_GRANULARITY(R.string.shortcut_value_previous_granularity), + PREVIOUS_WINDOW(R.string.shortcut_value_previous_window), + NEXT_WINDOW(R.string.shortcut_value_next_window), + SCROLL_BACK(R.string.shortcut_value_scroll_back), + SCROLL_FORWARD(R.string.shortcut_value_scroll_forward), + SCROLL_UP(R.string.shortcut_value_scroll_up), + SCROLL_DOWN(R.string.shortcut_value_scroll_down), + SCROLL_LEFT(R.string.shortcut_value_scroll_left), + SCROLL_RIGHT(R.string.shortcut_value_scroll_right), + HOME(R.string.shortcut_value_home), + OVERVIEW(R.string.shortcut_value_overview), + NOTIFICATIONS(R.string.shortcut_value_notifications), + QUICK_SETTINGS(R.string.shortcut_value_quick_settings), + SHOW_CUSTOM_ACTIONS(R.string.shortcut_value_show_custom_actions), + EDITING(R.string.shortcut_value_editing), + TALKBACK_BREAKOUT(R.string.shortcut_value_talkback_breakout), + LOCAL_BREAKOUT(R.string.shortcut_value_local_breakout), + READ_FROM_TOP(R.string.shortcut_value_read_from_top), + READ_FROM_CURRENT(R.string.shortcut_value_read_from_current), + PERFORM_CLICK_ACTION(R.string.shortcut_value_perform_click_action), + PERFORM_LONG_CLICK_ACTION(R.string.shortcut_value_perform_long_click_action), + PRINT_NODE_TREE(R.string.shortcut_value_print_node_tree), + PRINT_PERFORMANCE_STATS(R.string.shortcut_value_print_performance_stats), + SHOW_LANGUAGE_OPTIONS(R.string.shortcut_value_show_language_options), + SELECT_NEXT_SETTING(R.string.shortcut_value_select_next_setting), + SELECT_PREVIOUS_SETTING(R.string.shortcut_value_select_previous_setting), + SELECTED_SETTING_NEXT_ACTION(R.string.shortcut_value_selected_setting_next_action), + SELECTED_SETTING_PREVIOUS_ACTION(R.string.shortcut_value_selected_setting_previous_action), + SCREEN_SEARCH(R.string.shortcut_value_screen_search), + MEDIA_CONTROL(R.string.shortcut_value_media_control), + ENABLE_PASS_THROUGH(R.string.shortcut_value_pass_through_next_gesture), + ACCESSIBILITY_BUTTON(R.string.shortcut_value_a11y_button), + ACCESSIBILITY_BUTTON_CHOOSER(R.string.shortcut_value_a11y_button_long_press), + START_SELECTION_MODE(R.string.shortcut_value_start_selection_mode), + MOVE_CURSOR_TO_BEGINNING(R.string.shortcut_value_move_cursor_to_beginning), + MOVE_CURSOR_TO_END(R.string.shortcut_value_move_cursor_to_end), + TOGGLE_VOICE_FEEDBACK(R.string.shortcut_value_toggle_voice_feedback), + SELECT_ALL(R.string.shortcut_value_select_all), + COPY(R.string.shortcut_value_copy), + CUT(R.string.shortcut_value_cut), + PASTE(R.string.shortcut_value_paste), + COPY_LAST_SPOKEN_UTTERANCE(R.string.shortcut_value_copy_last_spoken_phrase), + PAUSE_FEEDBACK(R.string.shortcut_value_pause_or_resume_feedback), + ALL_APPS(R.string.shortcut_value_all_apps), + BRAILLE_KEYBOARD(R.string.shortcut_value_braille_keyboard), + TUTORIAL(R.string.shortcut_value_tutorial), + GESTURE_DEBUG(R.string.shortcut_value_report_gesture), + PRACTICE_GESTURES(R.string.shortcut_value_practice_gestures); + + private A11yAction(int gestureMappingReference) { + this.gestureMappingReference = gestureMappingReference; + } + + public int gestureMappingReference; + + public static final String granularityParameter = "mode"; + + public static SelectorController.Granularity granularityFrom(String name) { + for(SelectorController.Granularity granularity : SelectorController.Granularity.values()) { + if (name.toLowerCase().equals(granularity.name().toLowerCase())) { + return granularity; + } + } + return SelectorController.Granularity.DEFAULT; + } +} diff --git a/talkback/src/main/java/com/google/android/accessibility/talkback/adb/AdbReceiver.java b/talkback/src/main/java/com/google/android/accessibility/talkback/adb/AdbReceiver.java new file mode 100644 index 000000000..63c004f3b --- /dev/null +++ b/talkback/src/main/java/com/google/android/accessibility/talkback/adb/AdbReceiver.java @@ -0,0 +1,182 @@ +package com.google.android.accessibility.talkback.adb; + +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.media.AudioManager; + +import com.google.android.accessibility.talkback.TalkBackService; +import com.google.android.accessibility.utils.SharedPreferencesUtils; + +import java.lang.ref.WeakReference; + +/** + * A [BroadcastReceiver] that interprets command line instructions and forwards them to + * Google TalkBack. + * + * GENERAL USAGE: + * adb shell am broadcast -a com.a11y.adb.[A11yAction] [-e mode [SelectorController.Granularity]] + * adb shell am broadcast -a com.a11y.adb.[DeveloperSetting] + * adb shell am broadcast -a com.a11y.adb.[VolumeSetting] + * + * EXAMPLES + * -- A11yAction -- + * adb shell am broadcast -a com.a11y.adb.next + * adb shell am broadcast -a com.a11y.adb.next -e mode headings + * adb shell am broadcast -a com.a11y.adb.perform_click_action + * + * -- DeveloperSetting -- + * adb shell am broadcast -a com.a11y.adb.toggle_speech_output + * + * -- VolumeSetting -- + * adb shell am broadcast -a com.a11y.adb.volume_max + * adb shell am broadcast -a com.a11y.adb.volume_min + */ +public class AdbReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null || context == null) return; + TalkBackService talkBackServiceInstance = TalkBackService.getInstance(); + if (talkBackServiceInstance == null) return; + + String action = intent.getAction().replace(IntentActionPrefix + ".", "").toLowerCase(); + + if (performA11yAction(context, action, intent, talkBackServiceInstance)) return; + if (toggleDeveloperSetting(context, action, intent, talkBackServiceInstance)) return; + if (setAccessibilityVolume(context, action, intent, talkBackServiceInstance)) return; + + Log.tb4d(String.format("INVALID ACTION: %s", action)); + } + + private boolean performA11yAction(Context context, + String action, + Intent intent, + TalkBackService talkBackServiceInstance) { + for (A11yAction a11yAction : A11yAction.values()) { + if (a11yAction.name().toLowerCase().equals(action)) { + if (a11yAction == A11yAction.PREVIOUS || a11yAction == A11yAction.NEXT) { + String granularityParam = "default"; + if (intent.hasExtra(A11yAction.granularityParameter)) { + String granularityParamExtra = intent.getStringExtra(A11yAction.granularityParameter); + if (granularityParamExtra != null || granularityParamExtra.isEmpty()) { + granularityParam = granularityParamExtra; + } + } + + talkBackServiceInstance.moveAtGranularity( + A11yAction.granularityFrom(granularityParam), + a11yAction == A11yAction.NEXT + ); + } else { + talkBackServiceInstance.performGesture(context.getString(a11yAction.gestureMappingReference)); + } + return true; + } + } + return false; + } + + private boolean toggleDeveloperSetting(Context context, + String action, + Intent intent, + TalkBackService talkBackServiceInstance) { + DeveloperSetting developerSetting = DeveloperSetting.fromString(action); + if (developerSetting == DeveloperSetting.UNKNOWN) return false; + + SharedPreferences preferences = SharedPreferencesUtils.getSharedPreferences(context); + Resources resources = context.getResources(); + boolean prefValue = !SharedPreferencesUtils.getBooleanPref( + preferences, + resources, + developerSetting.keyId, + developerSetting.defaultKey); + SharedPreferencesUtils.putBooleanPref( + preferences, + resources, + developerSetting.keyId, + prefValue + ); + return true; + } + + private boolean setAccessibilityVolume(Context context, + String action, + Intent intent, + TalkBackService talkBackServiceInstance) { + VolumeSetting volumeControl = VolumeSetting.fromString(action); + int stream = AudioManager.STREAM_ACCESSIBILITY; + AudioManager audioManager = (AudioManager) context.getSystemService(Service.AUDIO_SERVICE); + int max = audioManager.getStreamMaxVolume(stream); + int currentVolume = audioManager.getStreamVolume(stream); + switch (volumeControl) { + case VOLUME_MAX: { + audioManager.setStreamVolume(stream, max, 0); + return true; + } + case VOLUME_MIN: { + audioManager.setStreamVolume(stream, 1, 0); + return true; + } + case VOLUME_QUARTER: { + audioManager.setStreamVolume(stream, (int)(max * 0.25), 0); + return true; + } + case VOLUME_HALF: { + audioManager.setStreamVolume(stream, (int)(max * 0.5), 0); + return true; + } + case VOLUME_THREE_QUARTER: { + audioManager.setStreamVolume(stream, (int)(max * 0.75), 0); + return true; + } + case VOLUME_TOGGLE: { + if (currentVolume > 1) { + audioManager.setStreamVolume(stream, 1, 0); + } else { + audioManager.setStreamVolume(stream, (int)(max * 0.5), 0); + } + return true; + } + default: return false; + } + } + + public static void registerAdbReceiver(Context context) { + Log.tb4d("Receiver registered"); + instance = new WeakReference(new AdbReceiver()); + context.registerReceiver(instance.get(), createIntentFilter()); + } + + public static void unregisterAdbReceiver(Context context) { + if (instance == null || instance.get() == null){ + return; + } + Log.tb4d("Receiver unregistered"); + context.unregisterReceiver(instance.get()); + instance = null; + } + + + private static IntentFilter createIntentFilter() { + IntentFilter intentFilter = new IntentFilter(); + for (A11yAction action : A11yAction.values()) { + intentFilter.addAction(String.format("%s.%s", IntentActionPrefix, action.name().toLowerCase())); + } + for (DeveloperSetting setting : DeveloperSetting.values()) { + intentFilter.addAction(String.format("%s.%s", IntentActionPrefix, setting.name().toLowerCase())); + } + for (VolumeSetting setting : VolumeSetting.values()) { + if (setting != VolumeSetting.NONE) { + intentFilter.addAction(String.format("%s.%s", IntentActionPrefix, setting.name().toLowerCase())); + } + } + return intentFilter; + } + + private static String IntentActionPrefix = "com.a11y.adb"; + private static WeakReference instance = null; +} diff --git a/talkback/src/main/java/com/google/android/accessibility/talkback/adb/DeveloperSetting.java b/talkback/src/main/java/com/google/android/accessibility/talkback/adb/DeveloperSetting.java new file mode 100644 index 000000000..47a70b6c1 --- /dev/null +++ b/talkback/src/main/java/com/google/android/accessibility/talkback/adb/DeveloperSetting.java @@ -0,0 +1,32 @@ +package com.google.android.accessibility.talkback.adb; + +import com.google.android.accessibility.talkback.R; + +enum DeveloperSetting { + // developer_preferences.xml + UNKNOWN(-1, -1), + TOGGLE_SPEECH_OUTPUT(R.string.pref_tts_overlay_key, R.bool.pref_tts_overlay_default), + ECHO_RECOGNIZED_SPEECH(R.string.pref_echo_recognized_text_speech_key, R.bool.pref_echo_recognized_text_default), + REDUCE_WINDOW_ANNOUNCEMENT_DELAY(R.string.pref_reduce_window_delay_key, R.bool.pref_reduce_window_delay_default), + ENABLE_PERFORMANCE_STATISTICS(R.string.pref_performance_stats_key, R.bool.pref_performance_stats_default), + EXPLORE_BY_TOUCH(R.string.pref_explore_by_touch_key, R.bool.pref_explore_by_touch_default), + NODE_TREE_DEBUGGING(R.string.title_pref_tree_debug, R.bool.pref_tree_debug_default); + + + private DeveloperSetting(int keyId, int defaultKey) { + this.keyId = keyId; + this.defaultKey = defaultKey; + } + + public int keyId; + public int defaultKey; + + public static DeveloperSetting fromString(String name) { + for (DeveloperSetting setting: DeveloperSetting.values()) { + if (setting.name().equalsIgnoreCase(name)) { + return setting; + } + } + return UNKNOWN; + } +} diff --git a/talkback/src/main/java/com/google/android/accessibility/talkback/adb/Log.java b/talkback/src/main/java/com/google/android/accessibility/talkback/adb/Log.java new file mode 100644 index 000000000..a606db380 --- /dev/null +++ b/talkback/src/main/java/com/google/android/accessibility/talkback/adb/Log.java @@ -0,0 +1,7 @@ +package com.google.android.accessibility.talkback.adb; + +public class Log { + public static void tb4d(String text) { + android.util.Log.d("TB4Dev", text); + } +} diff --git a/talkback/src/main/java/com/google/android/accessibility/talkback/adb/VolumeSetting.java b/talkback/src/main/java/com/google/android/accessibility/talkback/adb/VolumeSetting.java new file mode 100644 index 000000000..350720a24 --- /dev/null +++ b/talkback/src/main/java/com/google/android/accessibility/talkback/adb/VolumeSetting.java @@ -0,0 +1,20 @@ +package com.google.android.accessibility.talkback.adb; + +enum VolumeSetting { + NONE, + VOLUME_MIN, + VOLUME_QUARTER, + VOLUME_HALF, + VOLUME_THREE_QUARTER, + VOLUME_MAX, + VOLUME_TOGGLE; + + public static VolumeSetting fromString(String name) { + for (VolumeSetting setting: VolumeSetting.values()) { + if (setting.name().equalsIgnoreCase(name)) { + return setting; + } + } + return NONE; + } +} diff --git a/talkback/src/main/java/com/google/android/accessibility/talkback/preference/base/FocusIndicatorPrefFragment.java b/talkback/src/main/java/com/google/android/accessibility/talkback/preference/base/FocusIndicatorPrefFragment.java index 5b23653bc..e5f35c7a3 100644 --- a/talkback/src/main/java/com/google/android/accessibility/talkback/preference/base/FocusIndicatorPrefFragment.java +++ b/talkback/src/main/java/com/google/android/accessibility/talkback/preference/base/FocusIndicatorPrefFragment.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.android.accessibility.talkback.preference; +package com.google.android.accessibility.talkback.preference.base; import android.content.SharedPreferences; import android.os.Bundle; @@ -40,6 +40,13 @@ public class FocusIndicatorPrefFragment extends TalkbackBaseFragment { private PreferenceCategory colorPrefCategory; private SharedPreferences prefs; + // INFO: TalkBack For Developers modification + @Override + public CharSequence getTitle() { + return getString(R.string.title_pref_category_manage_focus_indicator); + } + // ------------------------------------------ + /** Preference items for focus indicator colors. */ @VisibleForTesting public enum FocusIndicatorPref { diff --git a/talkback/src/main/java/com/google/android/accessibility/talkback/selector/SelectorController.java b/talkback/src/main/java/com/google/android/accessibility/talkback/selector/SelectorController.java index af78dbc2d..1f5d1caea 100644 --- a/talkback/src/main/java/com/google/android/accessibility/talkback/selector/SelectorController.java +++ b/talkback/src/main/java/com/google/android/accessibility/talkback/selector/SelectorController.java @@ -1132,7 +1132,8 @@ private void handleAdjustable(EventId eventId, boolean isNext) { } /** Moves to the next or previous at specific granularity. */ - private void moveAtGranularity(EventId eventId, Granularity granularity, boolean isNext) { + // INFO: TalkBack For Developers modification + public void moveAtGranularity(EventId eventId, Granularity granularity, boolean isNext) { // Sets granularity and locks navigate within the focused node. pipeline.returnFeedback(eventId, Feedback.granularity(granularity.cursorGranularity)); diff --git a/talkback/src/main/java/com/google/android/accessibility/talkback/utils/FocusIndicatorUtils.java b/talkback/src/main/java/com/google/android/accessibility/talkback/utils/FocusIndicatorUtils.java index fdb68cbfc..8eef54113 100644 --- a/talkback/src/main/java/com/google/android/accessibility/talkback/utils/FocusIndicatorUtils.java +++ b/talkback/src/main/java/com/google/android/accessibility/talkback/utils/FocusIndicatorUtils.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; +import android.os.Build; import android.view.accessibility.AccessibilityManager; import androidx.annotation.ColorInt; import com.google.android.accessibility.talkback.R; @@ -55,6 +56,12 @@ public static void applyFocusAppearancePreference( public static void setAccessibilityFocusAppearance( AccessibilityService service, int borderWidth, int borderColor) { // TODO: Uses the public API of SDK 31, AccessibilityService#setAccessibilityFocusAppearance + + // INFO: TalkBack For Developers modification + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + service.setAccessibilityFocusAppearance(borderWidth, borderColor); + } + // ------------------------------------------ } /** diff --git a/talkback/src/main/res/drawable-anydpi-v26/icon_tb4d.xml b/talkback/src/main/res/drawable-anydpi-v26/icon_tb4d.xml new file mode 100644 index 000000000..c935e802d --- /dev/null +++ b/talkback/src/main/res/drawable-anydpi-v26/icon_tb4d.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/talkback/src/main/res/drawable-anydpi-v26/icon_tb4d_round.xml b/talkback/src/main/res/drawable-anydpi-v26/icon_tb4d_round.xml new file mode 100644 index 000000000..c935e802d --- /dev/null +++ b/talkback/src/main/res/drawable-anydpi-v26/icon_tb4d_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/talkback/src/main/res/drawable-hdpi/icon_tb4d.png b/talkback/src/main/res/drawable-hdpi/icon_tb4d.png new file mode 100644 index 000000000..8779d33aa Binary files /dev/null and b/talkback/src/main/res/drawable-hdpi/icon_tb4d.png differ diff --git a/talkback/src/main/res/drawable-hdpi/icon_tb4d_round.png b/talkback/src/main/res/drawable-hdpi/icon_tb4d_round.png new file mode 100644 index 000000000..240ffb199 Binary files /dev/null and b/talkback/src/main/res/drawable-hdpi/icon_tb4d_round.png differ diff --git a/talkback/src/main/res/drawable-ldpi/icon_tb4d.png b/talkback/src/main/res/drawable-ldpi/icon_tb4d.png new file mode 100644 index 000000000..30f1aca1c Binary files /dev/null and b/talkback/src/main/res/drawable-ldpi/icon_tb4d.png differ diff --git a/talkback/src/main/res/drawable-ldpi/icon_tb4d_round.png b/talkback/src/main/res/drawable-ldpi/icon_tb4d_round.png new file mode 100644 index 000000000..afd0a4cc7 Binary files /dev/null and b/talkback/src/main/res/drawable-ldpi/icon_tb4d_round.png differ diff --git a/talkback/src/main/res/drawable-mdpi/icon_tb4d.png b/talkback/src/main/res/drawable-mdpi/icon_tb4d.png new file mode 100644 index 000000000..30f1aca1c Binary files /dev/null and b/talkback/src/main/res/drawable-mdpi/icon_tb4d.png differ diff --git a/talkback/src/main/res/drawable-mdpi/icon_tb4d_round.png b/talkback/src/main/res/drawable-mdpi/icon_tb4d_round.png new file mode 100644 index 000000000..afd0a4cc7 Binary files /dev/null and b/talkback/src/main/res/drawable-mdpi/icon_tb4d_round.png differ diff --git a/talkback/src/main/res/drawable-xhdpi/icon_tb4d.png b/talkback/src/main/res/drawable-xhdpi/icon_tb4d.png new file mode 100644 index 000000000..08019763e Binary files /dev/null and b/talkback/src/main/res/drawable-xhdpi/icon_tb4d.png differ diff --git a/talkback/src/main/res/drawable-xhdpi/icon_tb4d_round.png b/talkback/src/main/res/drawable-xhdpi/icon_tb4d_round.png new file mode 100644 index 000000000..472644ab9 Binary files /dev/null and b/talkback/src/main/res/drawable-xhdpi/icon_tb4d_round.png differ diff --git a/talkback/src/main/res/drawable-xxhdpi/icon_tb4d.png b/talkback/src/main/res/drawable-xxhdpi/icon_tb4d.png new file mode 100644 index 000000000..94a5ae634 Binary files /dev/null and b/talkback/src/main/res/drawable-xxhdpi/icon_tb4d.png differ diff --git a/talkback/src/main/res/drawable-xxhdpi/icon_tb4d_round.png b/talkback/src/main/res/drawable-xxhdpi/icon_tb4d_round.png new file mode 100644 index 000000000..04ae51bbd Binary files /dev/null and b/talkback/src/main/res/drawable-xxhdpi/icon_tb4d_round.png differ diff --git a/talkback/src/main/res/drawable-xxxhdpi/icon_tb4d.png b/talkback/src/main/res/drawable-xxxhdpi/icon_tb4d.png new file mode 100644 index 000000000..1054f549b Binary files /dev/null and b/talkback/src/main/res/drawable-xxxhdpi/icon_tb4d.png differ diff --git a/talkback/src/main/res/drawable-xxxhdpi/icon_tb4d_round.png b/talkback/src/main/res/drawable-xxxhdpi/icon_tb4d_round.png new file mode 100644 index 000000000..9ac07c1d4 Binary files /dev/null and b/talkback/src/main/res/drawable-xxxhdpi/icon_tb4d_round.png differ diff --git a/talkback/src/main/res/drawable/ic_tb4d_launcher_background.xml b/talkback/src/main/res/drawable/ic_tb4d_launcher_background.xml new file mode 100644 index 000000000..e99b90a77 --- /dev/null +++ b/talkback/src/main/res/drawable/ic_tb4d_launcher_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/talkback/src/main/res/drawable/ic_tb4d_launcher_foreground.xml b/talkback/src/main/res/drawable/ic_tb4d_launcher_foreground.xml new file mode 100644 index 000000000..07259b0a4 --- /dev/null +++ b/talkback/src/main/res/drawable/ic_tb4d_launcher_foreground.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/talkback/src/main/res/drawable/talkback_intro.png b/talkback/src/main/res/drawable/talkback_intro.png index 5127821f2..094aa9308 100644 Binary files a/talkback/src/main/res/drawable/talkback_intro.png and b/talkback/src/main/res/drawable/talkback_intro.png differ diff --git a/talkback/src/main/res/values-v31/styles.xml b/talkback/src/main/res/values-v31/styles.xml index 823b25abe..4cf1c0280 100644 --- a/talkback/src/main/res/values-v31/styles.xml +++ b/talkback/src/main/res/values-v31/styles.xml @@ -17,9 +17,28 @@ + + + + - + + + + diff --git a/talkback/src/main/res/values/donottranslate.xml b/talkback/src/main/res/values/donottranslate.xml index 8729dc09c..a70180c70 100644 --- a/talkback/src/main/res/values/donottranslate.xml +++ b/talkback/src/main/res/values/donottranslate.xml @@ -820,7 +820,7 @@ pref_node_desc_order_value_state_name_role_pos false false - false + true false true false diff --git a/talkback/src/main/res/values/strings.xml b/talkback/src/main/res/values/strings.xml index cbd93546c..c910d73ce 100644 --- a/talkback/src/main/res/values/strings.xml +++ b/talkback/src/main/res/values/strings.xml @@ -3,7 +3,7 @@ - TalkBack_TfP + TalkBack For Developers + + @color/google_grey200 + @color/google_grey200 + #00de7a + @color/google_white diff --git a/utils/src/main/res/values/integers.xml b/utils/src/main/res/values/integers.xml new file mode 100644 index 000000000..bd018902c --- /dev/null +++ b/utils/src/main/res/values/integers.xml @@ -0,0 +1,7 @@ + + + + + + 80 + diff --git a/version.gradle b/version.gradle index d2c395006..b7d6d0cb7 100644 --- a/version.gradle +++ b/version.gradle @@ -1 +1,2 @@ ext { talkbackVersionName = "TfPu_release_13_0" } +ext { talkback4DevVersionName = "TB4D_0_0_2" }