From c02513d800d1ab8ecd0ccb25edaac71e83a42c65 Mon Sep 17 00:00:00 2001 From: Quintin Balsdon Date: Sun, 18 Dec 2022 19:48:22 +0000 Subject: [PATCH 1/5] Bare minimum fixes to get TB building. Fixes Dark Mode not working in menu, Back in Menus not working, Color preference screen crash, Color preference not being applied, and Touch to focus fix --- .gitignore | 9 + build.gradle | 4 +- build.sh | 71 +- shared.gradle | 8 +- .../talkback/TalkBackService.java | 4554 +++++++++-------- .../interpreters/InputFocusInterpreter.java | 1 - .../base/FocusIndicatorPrefFragment.java | 7 +- .../talkback/utils/FocusIndicatorUtils.java | 7 + talkback/src/main/res/values-v31/styles.xml | 21 +- .../utils/PreferencesActivity.java | 11 + 10 files changed, 2471 insertions(+), 2222 deletions(-) create mode 100644 .gitignore 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/build.gradle b/build.gradle index 53105a71f..7faec39e1 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,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,7 +31,7 @@ allprojects { } android { - buildToolsVersion '29.0.0' + buildToolsVersion '30.0.2' defaultConfig { applicationId talkbackApplicationId versionName talkbackVersionName + "-" + BUILD_TIMESTAMP 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/shared.gradle b/shared.gradle index 1cdba1a15..42a6fbc1f 100644 --- a/shared.gradle +++ b/shared.gradle @@ -5,7 +5,7 @@ ext { } 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/talkback/src/main/java/com/google/android/accessibility/talkback/TalkBackService.java b/talkback/src/main/java/com/google/android/accessibility/talkback/TalkBackService.java index 99c3ffd42..3fdb97f15 100644 --- a/talkback/src/main/java/com/google/android/accessibility/talkback/TalkBackService.java +++ b/talkback/src/main/java/com/google/android/accessibility/talkback/TalkBackService.java @@ -52,8 +52,10 @@ import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; + import androidx.annotation.VisibleForTesting; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + import com.google.android.accessibility.braille.brailledisplay.BrailleDisplay; import com.google.android.accessibility.braille.interfaces.BrailleDisplayForTalkBack; import com.google.android.accessibility.braille.interfaces.BrailleImeForBrailleDisplay; @@ -182,6 +184,7 @@ import com.google.android.accessibility.utils.output.SpeechControllerImpl.CapitalLetterHandlingMethod; import com.google.android.libraries.accessibility.utils.log.LogUtils; import com.google.common.collect.ImmutableMap; + import java.lang.Thread.UncaughtExceptionHandler; import java.util.ArrayList; import java.util.HashMap; @@ -190,2501 +193,2652 @@ import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.Executors; + import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -/** An {@link AccessibilityService} that provides spoken, haptic, and audible feedback. */ +/** + * An {@link AccessibilityService} that provides spoken, haptic, and audible feedback. + */ public class TalkBackService extends AccessibilityService - implements Thread.UncaughtExceptionHandler, SpeechController.Delegate, SharedKeyEvent.Listener { - /** Accesses the current speech language. */ - public class SpeechLanguage { - /** Gets the current speech language. */ - public @Nullable Locale getCurrentLanguage() { - return TalkBackService.this.getUserPreferredLocale(); + implements Thread.UncaughtExceptionHandler, SpeechController.Delegate, SharedKeyEvent.Listener { + /** + * Accesses the current speech language. + */ + public class SpeechLanguage { + /** + * Gets the current speech language. + */ + public @Nullable Locale getCurrentLanguage() { + return TalkBackService.this.getUserPreferredLocale(); + } + + /** + * Sets the current speech language. + * + * @param speechLanguage null is using the system language. + */ + public void setCurrentLanguage(@Nullable Locale speechLanguage) { + TalkBackService.this.setUserPreferredLocale(speechLanguage); + } } /** - * Sets the current speech language. - * - * @param speechLanguage null is using the system language. + * Interface for asking service flags to an {@link AccessibilityService}. */ - public void setCurrentLanguage(@Nullable Locale speechLanguage) { - TalkBackService.this.setUserPreferredLocale(speechLanguage); + public interface ServiceFlagRequester { + /** + * Attempts to change the service info flag. + * + * @param flag to specify the service flag to change. + * @param requestedState {@code true} to request the service flag, or {@code false} to disable + * the flag from the service. + */ + void requestFlag(int flag, boolean requestedState); } - } - /** Interface for asking service flags to an {@link AccessibilityService}. */ - public interface ServiceFlagRequester { /** - * Attempts to change the service info flag. - * - * @param flag to specify the service flag to change. - * @param requestedState {@code true} to request the service flag, or {@code false} to disable - * the flag from the service. + * Whether the user has seen the TalkBack tutorial. */ - void requestFlag(int flag, boolean requestedState); - } + public static final String PREF_FIRST_TIME_USER = "first_time_user"; - /** Whether the user has seen the TalkBack tutorial. */ - public static final String PREF_FIRST_TIME_USER = "first_time_user"; + /** + * Intent to open text-to-speech settings. + */ + public static final String INTENT_TTS_SETTINGS = "com.android.settings.TTS_SETTINGS"; - /** Intent to open text-to-speech settings. */ - public static final String INTENT_TTS_SETTINGS = "com.android.settings.TTS_SETTINGS"; + /** + * Intent to open text-to-speech settings. + */ + public static final String INTENT_TTS_TV_SETTINGS = "android.settings.TTS_SETTINGS"; - /** Intent to open text-to-speech settings. */ - public static final String INTENT_TTS_TV_SETTINGS = "android.settings.TTS_SETTINGS"; + /** + * Default interactive UI timeout in milliseconds. + */ + public static final int DEFAULT_INTERACTIVE_UI_TIMEOUT_MILLIS = 10000; - /** Default interactive UI timeout in milliseconds. */ - public static final int DEFAULT_INTERACTIVE_UI_TIMEOUT_MILLIS = 10000; + /** + * Timeout to turn off TalkBack without waiting for callback from TTS. + */ + private static final long TURN_OFF_TIMEOUT_MS = 5000; - /** Timeout to turn off TalkBack without waiting for callback from TTS. */ - private static final long TURN_OFF_TIMEOUT_MS = 5000; + private static final long TURN_OFF_WAIT_PERIOD_MS = 1000; - private static final long TURN_OFF_WAIT_PERIOD_MS = 1000; + /** + * An active instance of TalkBack. + */ + private static @Nullable TalkBackService instance = null; - /** An active instance of TalkBack. */ - private static @Nullable TalkBackService instance = null; + /* Call setAnimationScale with this value will disable animation. */ + private static float ANIMATION_OFF = 0; - /* Call setAnimationScale with this value will disable animation. */ - private static float ANIMATION_OFF = 0; + private static final String TAG = "TalkBackService"; - private static final String TAG = "TalkBackService"; + /** + * 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<>(); - /** - * 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; + + /** + * Staged pipeline for separating interpreters, feedback-mappers, and actors. + */ + private Pipeline pipeline; + + /** + * Controller for audio and haptic feedback. + */ + private FeedbackController feedbackController; + + /** + * 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; + + /** + * Interface for monitoring current and previous cursor position in editable node + */ + private TextCursorTracker textCursorTracker; - /** Controller for diagnostic overlay (developer mode). */ - private DiagnosticOverlayControllerImpl diagnosticOverlayController; + /** + * Monitors the call state for the phone device. + */ + private CallStateMonitor callStateMonitor; + + /** + * Monitors voice actions from other applications + */ + private VoiceActionMonitor voiceActionMonitor; + + /** + * Monitors speech actions from other applications + */ + private SpeechStateMonitor speechStateMonitor; + + /** + * 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; + + /** + * Controller for manage keyboard commands + */ + private KeyComboManager keyComboManager; - /** Staged pipeline for separating interpreters, feedback-mappers, and actors. */ - private Pipeline pipeline; + /** + * Manager for showing radial menus. + */ + private ListMenuManager menuManager; - /** Controller for audio and haptic feedback. */ - private FeedbackController feedbackController; + /** + * Manager for handling custom labels. + */ + private CustomLabelManager labelManager; - /** Watches the proximity sensor, and silences feedback when triggered. */ - private ProximitySensorListener proximitySensorListener; + /** + * Manager for the screen search feature. + */ + private UniversalSearchManager universalSearchManager; - 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; + /** + * Orientation monitor for watching orientation changes. + */ + private OrientationMonitor orientationMonitor; - /** Interface for monitoring current and previous cursor position in editable node */ - private TextCursorTracker textCursorTracker; + /** + * {@link BroadcastReceiver} for tracking the ringer and screen states. + */ + private RingerModeAndScreenMonitor ringerModeAndScreenMonitor; - /** Monitors the call state for the phone device. */ - private CallStateMonitor callStateMonitor; + /** + * {@link BroadcastReceiver} for tracking volume changes. + */ + private VolumeMonitor volumeMonitor; - /** Monitors voice actions from other applications */ - private VoiceActionMonitor voiceActionMonitor; + /** + * {@link android.content.BroadcastReceiver} for tracking battery status changes. + */ + private BatteryMonitor batteryMonitor; - /** Monitors speech actions from other applications */ - private SpeechStateMonitor speechStateMonitor; + /** + * {@link BroadcastReceiver} for tracking headphone connected status changes. + */ + private HeadphoneStateMonitor headphoneStateMonitor; - /** Maintains cursor state during explore-by-touch by working around EBT problems. */ - private ProcessorCursorState processorCursorState; + /** + * Tracks changes to audio output and provides information on what types of audio are playing. + */ + private AudioPlaybackMonitor audioPlaybackMonitor; - /** Processor for allowing clicking on buttons in permissions dialogs. */ - private ProcessorPermissionDialogs processorPermissionsDialogs; + /** + * Manages screen dimming + */ + private DimScreenActor dimScreenController; - /** Controller for manage keyboard commands */ - private KeyComboManager keyComboManager; + /** + * The television controller; non-null if the device is a television (Android TV). + */ + private TelevisionNavigationController televisionNavigationController; - /** Manager for showing radial menus. */ - private ListMenuManager menuManager; + private TelevisionDPadManager televisionDPadManager; - /** Manager for handling custom labels. */ - private CustomLabelManager labelManager; + /** + * {@link BroadcastReceiver} for tracking package removals for custom label data consistency. + */ + private PackageRemovalReceiver packageReceiver; - /** Manager for the screen search feature. */ - private UniversalSearchManager universalSearchManager; + /** + * The analytics instance, used for sending data to Google Analytics. + */ + private TalkBackAnalyticsImpl analytics; - /** Orientation monitor for watching orientation changes. */ - private OrientationMonitor orientationMonitor; + /** + * Callback to be invoked when fingerprint gestures are being used for accessibility. + */ + private FingerprintGestureCallback fingerprintGestureCallback; - /** {@link BroadcastReceiver} for tracking the ringer and screen states. */ - private RingerModeAndScreenMonitor ringerModeAndScreenMonitor; + /** + * Controller for the selector + */ + private SelectorController selectorController; - /** {@link BroadcastReceiver} for tracking volume changes. */ - private VolumeMonitor volumeMonitor; + /** + * Controller for handling gestures + */ + private GestureController gestureController; - /** {@link android.content.BroadcastReceiver} for tracking battery status changes. */ - private BatteryMonitor batteryMonitor; + /** + * Speech recognition wrapper for voice commands + */ + private SpeechRecognizerActor speechRecognizer; - /** {@link BroadcastReceiver} for tracking headphone connected status changes. */ - private HeadphoneStateMonitor headphoneStateMonitor; + /** + * Processor for voice commands + */ + private VoiceCommandProcessor voiceCommandProcessor; - /** Tracks changes to audio output and provides information on what types of audio are playing. */ - private AudioPlaybackMonitor audioPlaybackMonitor; + /** + * Shared preferences used within TalkBack. + */ + private SharedPreferences prefs; - /** Manages screen dimming */ - private DimScreenActor dimScreenController; + /** + * The system's uncaught exception handler + */ + private UncaughtExceptionHandler systemUeh; - /** The television controller; non-null if the device is a television (Android TV). */ - private TelevisionNavigationController televisionNavigationController; + /** + * The system feature if the device supports touch screen + */ + private boolean supportsTouchScreen = true; - private TelevisionDPadManager televisionDPadManager; + /** + * Whether the current root node is dirty or not. + */ + private boolean isRootNodeDirty = true; + /** + * Keep Track of current root node. + */ + private AccessibilityNodeInfo rootNode; - /** {@link BroadcastReceiver} for tracking package removals for custom label data consistency. */ - private PackageRemovalReceiver packageReceiver; + private AccessibilityEventProcessor accessibilityEventProcessor; - /** The analytics instance, used for sending data to Google Analytics. */ - private TalkBackAnalyticsImpl analytics; + /** + * 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; - /** Callback to be invoked when fingerprint gestures are being used for accessibility. */ - private FingerprintGestureCallback fingerprintGestureCallback; + /** + * A reference to the active Braille IME if any. + */ + private @Nullable BrailleImeForTalkBack brailleImeForTalkBack; - /** Controller for the selector */ - private SelectorController selectorController; + private BrailleDisplayForTalkBack brailleDisplay; - /** Controller for handling gestures */ - private GestureController gestureController; + 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; - /** Speech recognition wrapper for voice commands */ - private SpeechRecognizerActor speechRecognizer; + private final @NonNull Map displayIdToTouchInteractionMonitor = + new HashMap<>(); - /** Processor for voice commands */ - private VoiceCommandProcessor voiceCommandProcessor; + @Override + public void onCreate() { + super.onCreate(); - /** Shared preferences used within TalkBack. */ - private SharedPreferences prefs; + this.setTheme(R.style.TalkbackBaseTheme); - /** The system's uncaught exception handler */ - private UncaughtExceptionHandler systemUeh; + instance = this; + setServiceState(ServiceStateListener.SERVICE_STATE_INACTIVE); - /** The system feature if the device supports touch screen */ - private boolean supportsTouchScreen = true; + systemUeh = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(this); + } - /** Whether the current root node is dirty or not. */ - private boolean isRootNodeDirty = true; - /** Keep Track of current root node. */ - private AccessibilityNodeInfo rootNode; + /** + * 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; + } - private AccessibilityEventProcessor accessibilityEventProcessor; + // 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; + } - /** Keeps track of whether we need to run the locked-boot-completed callback when connected. */ - private boolean lockedBootCompletedPending; + // 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); + } - 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 + @Override + public boolean onUnbind(Intent intent) { + final long turningOffTime = System.currentTimeMillis(); + interruptAllFeedback(false /* stopTtsSpeechCompletely */); + if (pipeline != null) { + pipeline.onUnbind(calculateFinalAnnouncementVolume(), disableTalkBackCompleteAction); } - if (System.currentTimeMillis() - turningOffTime > TURN_OFF_TIMEOUT_MS - || disableTalkBackCompleteAction.isDone) { - break; + 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; } - // Resume animation if necessary. - enableAnimation(/* enable= */ true); - return false; - } - @Override - public void onDestroy() { - if (shouldUseTalkbackGestureDetection()) { - unregisterGestureDetection(); + @Override + public void onDestroy() { + 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(); + } } - if (passThroughModeActor != null) { - passThroughModeActor.onDestroy(); + @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 (pipeline != null) { + resetTouchExplorePassThrough(); + pipeline + .getFeedbackReturner() + .returnFeedback( + EVENT_ID_UNTRACKED, Feedback.deviceInfo(Action.CONFIG_CHANGED, newConfig)); + } } - super.onDestroy(); - SharedKeyEvent.unregister(this); + @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(); - if (isServiceActive()) { - suspendInfrastructure(); + if (diagnosticOverlayController != null) { + diagnosticOverlayController.displayEvent(event); + } } - instance = null; + public boolean supportsTouchScreen() { + return supportsTouchScreen; + } - // Shutdown and unregister all components. - shutdownInfrastructure(); - setServiceState(ServiceStateListener.SERVICE_STATE_INACTIVE); - serviceStateListeners.clear(); - if (televisionNavigationController != null) { - televisionNavigationController.onDestroy(); + @Override + public @Nullable AccessibilityNodeInfo getRootInActiveWindow() { + if (isRootNodeDirty || rootNode == null) { + rootNode = super.getRootInActiveWindow(); + isRootNodeDirty = false; + } + return rootNode == null ? null : AccessibilityNodeInfo.obtain(rootNode); + } + + public void setRootDirty(boolean rootIsDirty) { + isRootNodeDirty = rootIsDirty; } - } - @Override - public void onConfigurationChanged(Configuration newConfig) { - this.getTheme().applyStyle(R.style.TalkbackBaseTheme, /* force= */ true); + private void setServiceState(int newState) { + if (serviceState == newState) { + return; + } - // 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)); + serviceState = newState; + for (ServiceStateListener listener : serviceStateListeners) { + listener.onServiceStateChanged(newState); + } } - if (isServiceActive() && (orientationMonitor != null)) { - orientationMonitor.onConfigurationChanged(newConfig); + public void addServiceStateListener(ServiceStateListener listener) { + if (listener != null) { + serviceStateListeners.add(listener); + } } - if (gestureShortcutMapping != null) { - gestureShortcutMapping.onConfigurationChanged(newConfig); + public void removeServiceStateListener(ServiceStateListener listener) { + if (listener != null) { + serviceStateListeners.remove(listener); + } } - if (pipeline != null) { - resetTouchExplorePassThrough(); - pipeline - .getFeedbackReturner() - .returnFeedback( - EVENT_ID_UNTRACKED, Feedback.deviceInfo(Action.CONFIG_CHANGED, newConfig)); + /** + * 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. } - } - @Override - public void onAccessibilityEvent(AccessibilityEvent event) { - Performance perf = Performance.getInstance(); - EventId eventId = perf.onEventReceived(event); - accessibilityEventProcessor.onAccessibilityEvent(event, eventId); - perf.onHandlerDone(eventId); + private boolean shouldInterruptByAnyKeyEvent() { + return !fullScreenReadActor.isActive(); + } - if (brailleDisplay != null) { - brailleDisplay.onAccessibilityEvent(event); + /** + * 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; } - // Re-apply diagnosis-mode logging, in case other accessibility-services changed the shared - // log-level preference. - enforceDiagnosisModeLogging(); + @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); - @Override - public @Nullable AccessibilityNodeInfo getRootInActiveWindow() { - if (isRootNodeDirty || rootNode == null) { - rootNode = super.getRootInActiveWindow(); - isRootNodeDirty = false; + gestureController.onGesture(gestureId, 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); + 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) { - public FeedbackController getFeedbackController() { - if (feedbackController == null) { - throw new RuntimeException("mFeedbackController has not been initialized"); + if (fullScreenReadActor != null) { + fullScreenReadActor.interrupt(); + } + + if (pipeline != null) { + pipeline.interruptAllFeedback(stopTtsSpeechCompletely); + } } - return feedbackController; - } + @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); + } - 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; + } + + return service.serviceState; + } - public KeyComboManager getKeyComboManager() { - return keyComboManager; - } + /** + * 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; + } - public CustomLabelManager getLabelManager() { - if (labelManager == null) { - throw new RuntimeException("mLabelManager has not been initialized"); + /** + * @return {@code true} if TalkBack is running and initialized, {@code false} otherwise. + */ + public static boolean isServiceActive() { + return (getServiceState() == ServiceStateListener.SERVICE_STATE_ACTIVE); } - return labelManager; - } + /** + * 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()); + + volumeMonitor = new VolumeMonitor(pipeline.getFeedbackReturner(), this, callStateMonitor); + + // TODO: Move this into the custom label manager code + packageReceiver = new PackageRemovalReceiver(); + + addEventListener(new ProcessorGestureVibrator(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; - } + universalSearchManager = + new UniversalSearchManager( + pipeline.getFeedbackReturner(), + ringerModeAndScreenMonitor, + processorScreen.getWindowEventInterpreter()); - @VisibleForTesting - public TextCursorTracker getTextCursorTracker() { - return textCursorTracker; - } + keyEventListeners.add(keyComboManager); + serviceStateListeners.add(keyComboManager); - @VisibleForTesting - public RingerModeAndScreenMonitor getRingerModeAndScreenMonitor() { - return ringerModeAndScreenMonitor; - } + orientationMonitor = new OrientationMonitor(compositor, this); + orientationMonitor.addOnOrientationChangedListener(dimScreenController); - @VisibleForTesting - public GlobalVariables getGlobalVariables() { - return globalVariables; - } + KeyboardLockMonitor keyboardLockMonitor = new KeyboardLockMonitor(compositor); + keyEventListeners.add(keyboardLockMonitor); - @VisibleForTesting - public ProcessorScreen getProcessorScreen() { - return processorScreen; - } + 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); + } + + if (isServiceActive()) { + LogUtils.e(TAG, "Attempted to resume while not suspended"); + return; + } - @Override - public void onSpeakingForcedFeedback() { - voiceActionMonitor.onSpeakingForcedFeedback(); - } + setServiceState(ServiceStateListener.SERVICE_STATE_ACTIVE); + stopForeground(true); - // 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); + 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); + } + + // 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); + } - prefs.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); - prefs.registerOnSharedPreferenceChangeListener(analytics); + 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 (processorMagnification != null) { - processorMagnification.onResumeInfrastructure(); - } + if (FeatureSupport.isMultiFingerGestureSupported()) { + requestServiceFlag( + AccessibilityServiceInfo.FLAG_REQUEST_MULTI_FINGER_GESTURES + | AccessibilityServiceInfo.FLAG_REQUEST_2_FINGER_PASSTHROUGH, + /* newValue= */ true); + resetTouchExplorePassThrough(); + } - if ((fingerprintGestureCallback != null) && (getFingerprintGestureController() != null)) { - getFingerprintGestureController() - .registerFingerprintGestureCallback(fingerprintGestureCallback, null); - } + 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; + } - /** 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(); + @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); + } } - 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); + } - public void shutdown() { - setProximitySensorState(false); - } + /** + * 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 setScreenIsOn(boolean screenIsOn) { - this.screenIsOn = screenIsOn; + // Manage the proximity sensor state. + if (enabled) { + proximitySensor.start(); + } else { + proximitySensor.stop(); + } + } - // 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 void setProximitySensorStateByScreen() { + setProximitySensorState(screenIsOn); + } } - /** - * 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; + private void resetTouchExplorePassThrough() { + if (FeatureSupport.supportPassthrough()) { + if (isBrailleKeyboardActivated) { + return; + } + pipeline + .getFeedbackReturner() + .returnFeedback( + Performance.EVENT_ID_UNTRACKED, Feedback.passThroughMode(DISABLE_PASSTHROUGH)); + } + } - // Propagate the proximity sensor change. - setProximitySensorState(silenceOnProximity); + 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; } + // INFO: TalkBack For Developers modification + /** - * 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 hasMoreThanOneDisplay() { + List displays = WindowUtils.getAllDisplays(getApplicationContext()); + return displays.size() > 1; + } + // ------------------------------------------ + + private void registerGestureDetection() { + if (hasMoreThanOneDisplay()) 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 (!hasMoreThanOneDisplay()) 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/interpreters/InputFocusInterpreter.java b/talkback/src/main/java/com/google/android/accessibility/talkback/interpreters/InputFocusInterpreter.java index a85978dc5..836d28bb3 100644 --- a/talkback/src/main/java/com/google/android/accessibility/talkback/interpreters/InputFocusInterpreter.java +++ b/talkback/src/main/java/com/google/android/accessibility/talkback/interpreters/InputFocusInterpreter.java @@ -293,7 +293,6 @@ private boolean isFromSavedFocusAction(AccessibilityEvent event) { boolean isFromFocusAction = ((timeDiff >= 0L) && (timeDiff < INPUT_FOCUS_ACTION_TIMEOUT)) && node.equals(actorState.getInputFocusActionRecord().inputFocusedNode); - return isFromFocusAction; } 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..3e4513807 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,11 @@ public class FocusIndicatorPrefFragment extends TalkbackBaseFragment { private PreferenceCategory colorPrefCategory; private SharedPreferences prefs; + @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/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/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/utils/src/main/java/com/google/android/accessibility/utils/PreferencesActivity.java b/utils/src/main/java/com/google/android/accessibility/utils/PreferencesActivity.java index 3453fcba9..3652296d3 100644 --- a/utils/src/main/java/com/google/android/accessibility/utils/PreferencesActivity.java +++ b/utils/src/main/java/com/google/android/accessibility/utils/PreferencesActivity.java @@ -82,6 +82,17 @@ public boolean onNavigateUp() { } return true; } + // INFO: TalkBack For Developers modification + @Override + public boolean onSupportNavigateUp() { + return onNavigateUp(); + } + + @Override + public void onBackPressed() { + onNavigateUp(); + } + // ------------------------------------------ @Override protected void onStart() { From 9d82f75245623a4d08dd1a11761daa5e312249d5 Mon Sep 17 00:00:00 2001 From: Quintin Balsdon Date: Sun, 18 Dec 2022 19:58:04 +0000 Subject: [PATCH 2/5] Correcting the check --- .../android/accessibility/talkback/TalkBackService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/talkback/src/main/java/com/google/android/accessibility/talkback/TalkBackService.java b/talkback/src/main/java/com/google/android/accessibility/talkback/TalkBackService.java index 3fdb97f15..42591e401 100644 --- a/talkback/src/main/java/com/google/android/accessibility/talkback/TalkBackService.java +++ b/talkback/src/main/java/com/google/android/accessibility/talkback/TalkBackService.java @@ -2793,14 +2793,14 @@ protected boolean shouldUseTalkbackGestureDetection() { * * @return if the [TouchInteractionController] registration breaks TalkBack */ - private boolean hasMoreThanOneDisplay() { + private boolean hasOneDisplay() { List displays = WindowUtils.getAllDisplays(getApplicationContext()); - return displays.size() > 1; + return displays.size() == 1; } // ------------------------------------------ private void registerGestureDetection() { - if (hasMoreThanOneDisplay()) return; + if (hasOneDisplay()) return; if (BuildVersionUtils.isAtLeastT()) { List displays = WindowUtils.getAllDisplays(getApplicationContext()); Executor gestureExecutor = Executors.newSingleThreadExecutor(); @@ -2825,7 +2825,7 @@ private void registerGestureDetection() { } private void unregisterGestureDetection() { - if (!hasMoreThanOneDisplay()) return; + if (!hasOneDisplay()) return; if (BuildVersionUtils.isAtLeastT()) { List displays = WindowUtils.getAllDisplays(getApplicationContext()); for (Display display : displays) { From 3c11fb27378951ca49ccf256f033571990180bcb Mon Sep 17 00:00:00 2001 From: Quintin Balsdon Date: Sun, 18 Dec 2022 19:58:35 +0000 Subject: [PATCH 3/5] Correcting the check --- .../google/android/accessibility/talkback/TalkBackService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/talkback/src/main/java/com/google/android/accessibility/talkback/TalkBackService.java b/talkback/src/main/java/com/google/android/accessibility/talkback/TalkBackService.java index 42591e401..1badc5f45 100644 --- a/talkback/src/main/java/com/google/android/accessibility/talkback/TalkBackService.java +++ b/talkback/src/main/java/com/google/android/accessibility/talkback/TalkBackService.java @@ -2825,7 +2825,7 @@ private void registerGestureDetection() { } private void unregisterGestureDetection() { - if (!hasOneDisplay()) return; + if (hasOneDisplay()) return; if (BuildVersionUtils.isAtLeastT()) { List displays = WindowUtils.getAllDisplays(getApplicationContext()); for (Display display : displays) { From 7cc8855cff55c45a858d5008f43e255249dcd744 Mon Sep 17 00:00:00 2001 From: Quintin Balsdon Date: Sun, 18 Dec 2022 20:02:34 +0000 Subject: [PATCH 4/5] Correcting the check --- .../talkback/interpreters/InputFocusInterpreter.java | 1 + .../talkback/preference/base/FocusIndicatorPrefFragment.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/talkback/src/main/java/com/google/android/accessibility/talkback/interpreters/InputFocusInterpreter.java b/talkback/src/main/java/com/google/android/accessibility/talkback/interpreters/InputFocusInterpreter.java index 836d28bb3..a85978dc5 100644 --- a/talkback/src/main/java/com/google/android/accessibility/talkback/interpreters/InputFocusInterpreter.java +++ b/talkback/src/main/java/com/google/android/accessibility/talkback/interpreters/InputFocusInterpreter.java @@ -293,6 +293,7 @@ private boolean isFromSavedFocusAction(AccessibilityEvent event) { boolean isFromFocusAction = ((timeDiff >= 0L) && (timeDiff < INPUT_FOCUS_ACTION_TIMEOUT)) && node.equals(actorState.getInputFocusActionRecord().inputFocusedNode); + return isFromFocusAction; } 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 3e4513807..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 @@ -40,10 +40,12 @@ 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 From 8f2e897846a4523ad827ebba3850d75c62b542d5 Mon Sep 17 00:00:00 2001 From: Quintin Balsdon Date: Sun, 18 Dec 2022 23:02:00 +0000 Subject: [PATCH 5/5] Added TB4D back --- README.md | 68 ++++++- build.gradle | 5 +- download_resources.sh | 45 +++++ shared.gradle | 2 +- src/main/AndroidManifest.xml | 2 +- talkback/src/main/AndroidManifest.xml | 8 +- .../talkback/TalkBackService.java | 19 ++ .../talkback/adb/A11yAction.java | 81 ++++++++ .../talkback/adb/AdbReceiver.java | 182 ++++++++++++++++++ .../talkback/adb/DeveloperSetting.java | 32 +++ .../accessibility/talkback/adb/Log.java | 7 + .../talkback/adb/VolumeSetting.java | 20 ++ .../talkback/selector/SelectorController.java | 3 +- .../res/drawable-anydpi-v26/icon_tb4d.xml | 5 + .../drawable-anydpi-v26/icon_tb4d_round.xml | 5 + .../src/main/res/drawable-hdpi/icon_tb4d.png | Bin 0 -> 2933 bytes .../res/drawable-hdpi/icon_tb4d_round.png | Bin 0 -> 3713 bytes .../src/main/res/drawable-ldpi/icon_tb4d.png | Bin 0 -> 1873 bytes .../res/drawable-ldpi/icon_tb4d_round.png | Bin 0 -> 2373 bytes .../src/main/res/drawable-mdpi/icon_tb4d.png | Bin 0 -> 1873 bytes .../res/drawable-mdpi/icon_tb4d_round.png | Bin 0 -> 2373 bytes .../src/main/res/drawable-xhdpi/icon_tb4d.png | Bin 0 -> 3845 bytes .../res/drawable-xhdpi/icon_tb4d_round.png | Bin 0 -> 5254 bytes .../main/res/drawable-xxhdpi/icon_tb4d.png | Bin 0 -> 6010 bytes .../res/drawable-xxhdpi/icon_tb4d_round.png | Bin 0 -> 8447 bytes .../main/res/drawable-xxxhdpi/icon_tb4d.png | Bin 0 -> 8176 bytes .../res/drawable-xxxhdpi/icon_tb4d_round.png | Bin 0 -> 12046 bytes .../drawable/ic_tb4d_launcher_background.xml | 10 + .../drawable/ic_tb4d_launcher_foreground.xml | 25 +++ .../src/main/res/drawable/talkback_intro.png | Bin 2987 -> 34727 bytes .../src/main/res/values/donottranslate.xml | 2 +- talkback/src/main/res/values/strings.xml | 2 +- .../talkback/TalkBackDevService.java | 25 +++ .../utils/PackageManagerUtils.java | 5 +- .../utils/output/TextToSpeechOverlay.java | 17 +- .../src/main/res/drawable/rounded_corner.xml | 30 +++ .../main/res/drawable/rounded_corner_1.xml | 27 +++ .../main/res/drawable/rounded_corner_10.xml | 27 +++ .../main/res/drawable/rounded_corner_11.xml | 27 +++ .../main/res/drawable/rounded_corner_12.xml | 27 +++ .../main/res/drawable/rounded_corner_13.xml | 27 +++ .../main/res/drawable/rounded_corner_14.xml | 27 +++ .../main/res/drawable/rounded_corner_15.xml | 27 +++ .../main/res/drawable/rounded_corner_16.xml | 27 +++ .../main/res/drawable/rounded_corner_2.xml | 27 +++ .../main/res/drawable/rounded_corner_3.xml | 27 +++ .../main/res/drawable/rounded_corner_4.xml | 27 +++ .../main/res/drawable/rounded_corner_5.xml | 27 +++ .../main/res/drawable/rounded_corner_6.xml | 27 +++ .../main/res/drawable/rounded_corner_7.xml | 27 +++ .../main/res/drawable/rounded_corner_8.xml | 27 +++ .../main/res/drawable/rounded_corner_9.xml | 27 +++ .../main/res/drawable/toast_transition.xml | 20 ++ utils/src/main/res/values/colors.xml | 6 + utils/src/main/res/values/integers.xml | 7 + version.gradle | 1 + 56 files changed, 1048 insertions(+), 18 deletions(-) create mode 100755 download_resources.sh create mode 100644 talkback/src/main/java/com/google/android/accessibility/talkback/adb/A11yAction.java create mode 100644 talkback/src/main/java/com/google/android/accessibility/talkback/adb/AdbReceiver.java create mode 100644 talkback/src/main/java/com/google/android/accessibility/talkback/adb/DeveloperSetting.java create mode 100644 talkback/src/main/java/com/google/android/accessibility/talkback/adb/Log.java create mode 100644 talkback/src/main/java/com/google/android/accessibility/talkback/adb/VolumeSetting.java create mode 100644 talkback/src/main/res/drawable-anydpi-v26/icon_tb4d.xml create mode 100644 talkback/src/main/res/drawable-anydpi-v26/icon_tb4d_round.xml create mode 100644 talkback/src/main/res/drawable-hdpi/icon_tb4d.png create mode 100644 talkback/src/main/res/drawable-hdpi/icon_tb4d_round.png create mode 100644 talkback/src/main/res/drawable-ldpi/icon_tb4d.png create mode 100644 talkback/src/main/res/drawable-ldpi/icon_tb4d_round.png create mode 100644 talkback/src/main/res/drawable-mdpi/icon_tb4d.png create mode 100644 talkback/src/main/res/drawable-mdpi/icon_tb4d_round.png create mode 100644 talkback/src/main/res/drawable-xhdpi/icon_tb4d.png create mode 100644 talkback/src/main/res/drawable-xhdpi/icon_tb4d_round.png create mode 100644 talkback/src/main/res/drawable-xxhdpi/icon_tb4d.png create mode 100644 talkback/src/main/res/drawable-xxhdpi/icon_tb4d_round.png create mode 100644 talkback/src/main/res/drawable-xxxhdpi/icon_tb4d.png create mode 100644 talkback/src/main/res/drawable-xxxhdpi/icon_tb4d_round.png create mode 100644 talkback/src/main/res/drawable/ic_tb4d_launcher_background.xml create mode 100644 talkback/src/main/res/drawable/ic_tb4d_launcher_foreground.xml create mode 100644 talkback/src/phone/java/com/developer/talkback/TalkBackDevService.java create mode 100644 utils/src/main/res/drawable/rounded_corner.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_1.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_10.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_11.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_12.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_13.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_14.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_15.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_16.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_2.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_3.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_4.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_5.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_6.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_7.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_8.xml create mode 100644 utils/src/main/res/drawable/rounded_corner_9.xml create mode 100644 utils/src/main/res/drawable/toast_transition.xml create mode 100644 utils/src/main/res/values/integers.xml 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 7faec39e1..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') @@ -34,7 +31,7 @@ android { 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/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 42a6fbc1f..62817949d 100644 --- a/shared.gradle +++ b/shared.gradle @@ -1,7 +1,7 @@ // For building the open-source release of TalkBack. ext { - talkbackApplicationId = "com.android.talkback" + talkbackApplicationId = "com.android.talkback4d" } android { 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 @@ 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/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/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 0000000000000000000000000000000000000000..8779d33aa81e0b7696b6e1e019a2f97761c27dc6 GIT binary patch literal 2933 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 f(Hzau9*Fioz9@lLe?9w?00000NkvXXu0mjfIU literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..240ffb199ae1c1f479eb936e335d646661c00eff GIT binary patch literal 3713 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 fnc|)!rMU6J8C1?}lebHEL zs;H&bnrf=lQWRp1Ra15QLwB0kI)GTRR0u5UxQ_ z@H!#LQm_%#)XN+{MKO-g$f-9FjId{DHCYS|7>l=e4gd$-0CDUd{IL4W0ME`&iTf#~rUxOBv~u%K`1#2*#P#kN z9zPp?d*LE1uiH-4>39xAaoc!y4$8hTAmIdtR--5XkBLMymKwnAm~~yCDVP%!R%NvT zu1CNOG^gJt5*W}g1~6!%;JS{VVvPX_kR~({ff@9h0gNQRf!1M(0qz9B9P}#h6G0jD zs{yo5aJ_=(14|50()fMmEK>by01ar4ICd*wYk)W6%@{N<_t^LK^s51S3zBjZ*t@6! z8v`6)Ss}dqUbFY z-gq$sngAQ<=;(lt9zTIc&oslmCrb-e+3VjEy%04S3m5C%}7aqE{t(Wy$vkAEq4XiO$u#}vc{{@WQ) z{QYV;_QO2mI=um9(x1R=12#4R^l8dO7aBBWI&EOw+9iYtYw+Au07IH8pv~kA<2qvl zj!dzpip~H&HIW7|BT5!RMXLtdre+d#mN0bsOcYNGotI@1b$SCpo_GzI{RZ$DP))ts zodIrCIt)C#0#4fb4&;Qt>^g&!XF#W=3!s}e7kbqfLho%wUEu%mb;rE<^nLm^oQO2& zyT1hbHIzVZWIB<{U<@<^RtDsWC%ZF%8NOgHRJ3T|$kJ?j-~}970VQWwL&eV;sA{k2 z7F>_-^E*%iN0nvMlLC%oH)|G=!^nVi@-e{100-^Idoo~?kO4!!UJ2ztX`u3wmJn(V zxE&TRXe>vQ0;BIU3~+*@HUm}$e5K3ialiv3So<3ZU|$W!=SG@N6{pgFHhM&(W^Qf4zR=keUuZ&ZXZ>?sMp!0ufdap%kqV2_K81W&5=?(JW`k@JzII3 znE)d=wh{_YRub3ErmcoveFeu?l645tsfs(p!e#iUN6QWHZTS+P`m9>TMeV-Gzi98}hjusuouN@~5 zeYXm_6fH!n@M0+ap^6Y^4tPbz?eS_ZJ*SI-;s+loKJF;-jxo#+tTljfRC~t+e^J!QILKv+P>=h3dZvAyyzeUv<}~E^DJ< zw=rRW)f*s!bb$uenCzID!p4%*$`;KdMZ31cVp^&nijI|hbi%|%W2k?+mo~k|f5+m6z(dR1 z#iz<{$<9^$rAO3MdL!toqUL~IIol_^pHb~3_nFF!!9brg3=xGijn`>2t6-<@2qEZv zb%x;n*q<=-VGUS|Sx?`v@DM{m=Xa@B3D$tMSXA&}c^w!iuWk7c9xMP{&4;xw00000 LNkvXXu0mjffyZVy literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..afd0a4cc752e85b31c4c72572502db628c1c7ec9 GIT binary patch literal 2373 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..30f1aca1c13f72c18bab07627d562ad7605347aa GIT binary patch literal 1873 zcmV-X2d?;uP)|)!rMU6J8C1?}lebHEL zs;H&bnrf=lQWRp1Ra15QLwB0kI)GTRR0u5UxQ_ z@H!#LQm_%#)XN+{MKO-g$f-9FjId{DHCYS|7>l=e4gd$-0CDUd{IL4W0ME`&iTf#~rUxOBv~u%K`1#2*#P#kN z9zPp?d*LE1uiH-4>39xAaoc!y4$8hTAmIdtR--5XkBLMymKwnAm~~yCDVP%!R%NvT zu1CNOG^gJt5*W}g1~6!%;JS{VVvPX_kR~({ff@9h0gNQRf!1M(0qz9B9P}#h6G0jD zs{yo5aJ_=(14|50()fMmEK>by01ar4ICd*wYk)W6%@{N<_t^LK^s51S3zBjZ*t@6! z8v`6)Ss}dqUbFY z-gq$sngAQ<=;(lt9zTIc&oslmCrb-e+3VjEy%04S3m5C%}7aqE{t(Wy$vkAEq4XiO$u#}vc{{@WQ) z{QYV;_QO2mI=um9(x1R=12#4R^l8dO7aBBWI&EOw+9iYtYw+Au07IH8pv~kA<2qvl zj!dzpip~H&HIW7|BT5!RMXLtdre+d#mN0bsOcYNGotI@1b$SCpo_GzI{RZ$DP))ts zodIrCIt)C#0#4fb4&;Qt>^g&!XF#W=3!s}e7kbqfLho%wUEu%mb;rE<^nLm^oQO2& zyT1hbHIzVZWIB<{U<@<^RtDsWC%ZF%8NOgHRJ3T|$kJ?j-~}970VQWwL&eV;sA{k2 z7F>_-^E*%iN0nvMlLC%oH)|G=!^nVi@-e{100-^Idoo~?kO4!!UJ2ztX`u3wmJn(V zxE&TRXe>vQ0;BIU3~+*@HUm}$e5K3ialiv3So<3ZU|$W!=SG@N6{pgFHhM&(W^Qf4zR=keUuZ&ZXZ>?sMp!0ufdap%kqV2_K81W&5=?(JW`k@JzII3 znE)d=wh{_YRub3ErmcoveFeu?l645tsfs(p!e#iUN6QWHZTS+P`m9>TMeV-Gzi98}hjusuouN@~5 zeYXm_6fH!n@M0+ap^6Y^4tPbz?eS_ZJ*SI-;s+loKJF;-jxo#+tTljfRC~t+e^J!QILKv+P>=h3dZvAyyzeUv<}~E^DJ< zw=rRW)f*s!bb$uenCzID!p4%*$`;KdMZ31cVp^&nijI|hbi%|%W2k?+mo~k|f5+m6z(dR1 z#iz<{$<9^$rAO3MdL!toqUL~IIol_^pHb~3_nFF!!9brg3=xGijn`>2t6-<@2qEZv zb%x;n*q<=-VGUS|Sx?`v@DM{m=Xa@B3D$tMSXA&}c^w!iuWk7c9xMP{&4;xw00000 LNkvXXu0mjffyZVy literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..afd0a4cc752e85b31c4c72572502db628c1c7ec9 GIT binary patch literal 2373 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..08019763e5b1e2191e19e0ff572d1246a713021f GIT binary patch literal 3845 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 M07*qoM6N<$f<&Ysk^lez literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..94a5ae6346f7d7574f0ae7da6628a3ac1d76c8a2 GIT binary patch literal 6010 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 VR$KpQ|K_Q~$w(-QSBZWN_#YJdc0K?A literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..04ae51bbd37f0dbe3c09902ce9b2352b281850de GIT binary patch literal 8447 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1054f549b8544c1726fc2aa7648a8837e24ebb80 GIT binary patch literal 8176 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 Tx#9UgcYvy*mO{0hRoMRlMLEsq literal 0 HcmV?d00001 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 5127821f26b5a8d30d0322e0cbf1eaf7c463dcaf..094aa930874111f976406c3c27593001290d3f12 100644 GIT binary patch literal 34727 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 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" }