diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index a3a849e0704358..0b0146c40221ec 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2948,7 +2948,7 @@ public abstract interface class com/facebook/react/module/model/ReactModuleInfoP public abstract fun getReactModuleInfos ()Ljava/util/Map; } -public class com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule : com/facebook/fbreact/specs/NativeAccessibilityInfoSpec, com/facebook/react/bridge/LifecycleEventListener { +public final class com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule : com/facebook/fbreact/specs/NativeAccessibilityInfoSpec, com/facebook/react/bridge/LifecycleEventListener { public fun (Lcom/facebook/react/bridge/ReactApplicationContext;)V public fun announceForAccessibility (Ljava/lang/String;)V public fun getRecommendedTimeoutMillis (DLcom/facebook/react/bridge/Callback;)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.java deleted file mode 100644 index 58f11229eaac16..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.java +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.modules.accessibilityinfo; - -import android.annotation.TargetApi; -import android.content.ContentResolver; -import android.content.Context; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.Build; -import android.provider.Settings; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import androidx.annotation.Nullable; -import com.facebook.fbreact.specs.NativeAccessibilityInfoSpec; -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.LifecycleEventListener; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.module.annotations.ReactModule; - -/** - * Module that monitors and provides information about the state of Touch Exploration service on the - * device. For API >= 19. - */ -@ReactModule(name = NativeAccessibilityInfoSpec.NAME) -public class AccessibilityInfoModule extends NativeAccessibilityInfoSpec - implements LifecycleEventListener { - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private class ReactTouchExplorationStateChangeListener - implements AccessibilityManager.TouchExplorationStateChangeListener { - - @Override - public void onTouchExplorationStateChanged(boolean enabled) { - updateAndSendTouchExplorationChangeEvent(enabled); - } - } - - // Android can listen for accessibility service enable with `accessibilityStateChange`, but - // `accessibilityState` conflicts with React Native props and confuses developers. Therefore, the - // name `accessibilityServiceChange` is used here instead. - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private class ReactAccessibilityServiceChangeListener - implements AccessibilityManager.AccessibilityStateChangeListener { - - @Override - public void onAccessibilityStateChanged(boolean enabled) { - updateAndSendAccessibilityServiceChangeEvent(enabled); - } - } - - // Listener that is notified when the global TRANSITION_ANIMATION_SCALE. - private final ContentObserver animationScaleObserver = - new ContentObserver(UiThreadUtil.getUiThreadHandler()) { - @Override - public void onChange(boolean selfChange) { - this.onChange(selfChange, null); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - if (getReactApplicationContext().hasActiveReactInstance()) { - AccessibilityInfoModule.this.updateAndSendReduceMotionChangeEvent(); - } - } - }; - - private @Nullable AccessibilityManager mAccessibilityManager; - private @Nullable ReactTouchExplorationStateChangeListener mTouchExplorationStateChangeListener; - private @Nullable ReactAccessibilityServiceChangeListener mAccessibilityServiceChangeListener; - private final ContentResolver mContentResolver; - private boolean mReduceMotionEnabled = false; - private boolean mTouchExplorationEnabled = false; - private boolean mAccessibilityServiceEnabled = false; - private int mRecommendedTimeout; - - private static final String REDUCE_MOTION_EVENT_NAME = "reduceMotionDidChange"; - private static final String TOUCH_EXPLORATION_EVENT_NAME = "touchExplorationDidChange"; - private static final String ACCESSIBILITY_SERVICE_EVENT_NAME = "accessibilityServiceDidChange"; - - public AccessibilityInfoModule(ReactApplicationContext context) { - super(context); - Context appContext = context.getApplicationContext(); - mAccessibilityManager = - (AccessibilityManager) appContext.getSystemService(Context.ACCESSIBILITY_SERVICE); - mContentResolver = getReactApplicationContext().getContentResolver(); - mTouchExplorationEnabled = mAccessibilityManager.isTouchExplorationEnabled(); - mAccessibilityServiceEnabled = mAccessibilityManager.isEnabled(); - mReduceMotionEnabled = this.getIsReduceMotionEnabledValue(); - mTouchExplorationStateChangeListener = new ReactTouchExplorationStateChangeListener(); - mAccessibilityServiceChangeListener = new ReactAccessibilityServiceChangeListener(); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private boolean getIsReduceMotionEnabledValue() { - // Disabling animations in developer settings will set the animation scale to "0.0" - // but setting "reduce motion" / "disable animations" will set the animation scale to "0". - String rawValue = - Settings.Global.getString(mContentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE); - - // Parse the value as a float so we can check for a single value. - Float parsedValue = rawValue != null ? Float.parseFloat(rawValue) : 1f; - - return parsedValue == 0f; - } - - @Override - public void isReduceMotionEnabled(Callback successCallback) { - successCallback.invoke(mReduceMotionEnabled); - } - - @Override - public void isTouchExplorationEnabled(Callback successCallback) { - successCallback.invoke(mTouchExplorationEnabled); - } - - @Override - public void isAccessibilityServiceEnabled(Callback successCallback) { - successCallback.invoke(mAccessibilityServiceEnabled); - } - - private void updateAndSendReduceMotionChangeEvent() { - boolean isReduceMotionEnabled = this.getIsReduceMotionEnabledValue(); - - if (mReduceMotionEnabled != isReduceMotionEnabled) { - mReduceMotionEnabled = isReduceMotionEnabled; - - ReactApplicationContext reactApplicationContext = getReactApplicationContextIfActiveOrWarn(); - if (reactApplicationContext != null) { - reactApplicationContext.emitDeviceEvent(REDUCE_MOTION_EVENT_NAME, mReduceMotionEnabled); - } - } - } - - private void updateAndSendTouchExplorationChangeEvent(boolean enabled) { - if (mTouchExplorationEnabled != enabled) { - mTouchExplorationEnabled = enabled; - - ReactApplicationContext reactApplicationContext = getReactApplicationContextIfActiveOrWarn(); - if (reactApplicationContext != null) { - getReactApplicationContext() - .emitDeviceEvent(TOUCH_EXPLORATION_EVENT_NAME, mTouchExplorationEnabled); - } - } - } - - private void updateAndSendAccessibilityServiceChangeEvent(boolean enabled) { - if (mAccessibilityServiceEnabled != enabled) { - mAccessibilityServiceEnabled = enabled; - - ReactApplicationContext reactApplicationContext = getReactApplicationContextIfActiveOrWarn(); - if (reactApplicationContext != null) { - getReactApplicationContext() - .emitDeviceEvent(ACCESSIBILITY_SERVICE_EVENT_NAME, mAccessibilityServiceEnabled); - } - } - } - - @Override - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - public void onHostResume() { - mAccessibilityManager.addTouchExplorationStateChangeListener( - mTouchExplorationStateChangeListener); - mAccessibilityManager.addAccessibilityStateChangeListener(mAccessibilityServiceChangeListener); - - Uri transitionUri = Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE); - mContentResolver.registerContentObserver(transitionUri, false, animationScaleObserver); - - updateAndSendTouchExplorationChangeEvent(mAccessibilityManager.isTouchExplorationEnabled()); - updateAndSendAccessibilityServiceChangeEvent(mAccessibilityManager.isEnabled()); - updateAndSendReduceMotionChangeEvent(); - } - - @Override - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - public void onHostPause() { - mAccessibilityManager.removeTouchExplorationStateChangeListener( - mTouchExplorationStateChangeListener); - mAccessibilityManager.removeAccessibilityStateChangeListener( - mAccessibilityServiceChangeListener); - - mContentResolver.unregisterContentObserver(animationScaleObserver); - } - - @Override - public void initialize() { - getReactApplicationContext().addLifecycleEventListener(this); - updateAndSendTouchExplorationChangeEvent(mAccessibilityManager.isTouchExplorationEnabled()); - updateAndSendAccessibilityServiceChangeEvent(mAccessibilityManager.isEnabled()); - updateAndSendReduceMotionChangeEvent(); - } - - @Override - public void invalidate() { - getReactApplicationContext().removeLifecycleEventListener(this); - super.invalidate(); - } - - @Override - public void onHostDestroy() {} - - @Override - public void announceForAccessibility(String message) { - if (mAccessibilityManager == null || !mAccessibilityManager.isEnabled()) { - return; - } - - AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT); - event.getText().add(message); - event.setClassName(AccessibilityInfoModule.class.getName()); - event.setPackageName(getReactApplicationContext().getPackageName()); - - mAccessibilityManager.sendAccessibilityEvent(event); - } - - @Override - public void setAccessibilityFocus(double reactTag) { - // iOS only - } - - @Override - public void getRecommendedTimeoutMillis(double originalTimeout, Callback successCallback) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - successCallback.invoke((int) originalTimeout); - return; - } - mRecommendedTimeout = - mAccessibilityManager.getRecommendedTimeoutMillis( - (int) originalTimeout, AccessibilityManager.FLAG_CONTENT_CONTROLS); - successCallback.invoke(mRecommendedTimeout); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.kt new file mode 100644 index 00000000000000..5454972c487e9c --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.kt @@ -0,0 +1,212 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.modules.accessibilityinfo + +import android.annotation.TargetApi +import android.content.ContentResolver +import android.content.Context +import android.database.ContentObserver +import android.net.Uri +import android.os.Build +import android.provider.Settings +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityManager +import com.facebook.fbreact.specs.NativeAccessibilityInfoSpec +import com.facebook.react.bridge.Callback +import com.facebook.react.bridge.LifecycleEventListener +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.module.annotations.ReactModule + +/** + * Module that monitors and provides information about the state of Touch Exploration service on the + * device. For API >= 19. + */ +@ReactModule(name = NativeAccessibilityInfoSpec.NAME) +public class AccessibilityInfoModule(context: ReactApplicationContext) : + NativeAccessibilityInfoSpec(context), LifecycleEventListener { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private inner class ReactTouchExplorationStateChangeListener : + AccessibilityManager.TouchExplorationStateChangeListener { + override fun onTouchExplorationStateChanged(enabled: Boolean) { + updateAndSendTouchExplorationChangeEvent(enabled) + } + } + + // Android can listen for accessibility service enable with `accessibilityStateChange`, but + // `accessibilityState` conflicts with React Native props and confuses developers. Therefore, the + // name `accessibilityServiceChange` is used here instead. + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private inner class ReactAccessibilityServiceChangeListener : + AccessibilityManager.AccessibilityStateChangeListener { + override fun onAccessibilityStateChanged(enabled: Boolean) { + updateAndSendAccessibilityServiceChangeEvent(enabled) + } + } + + // Listener that is notified when the global TRANSITION_ANIMATION_SCALE. + private val animationScaleObserver: ContentObserver = + object : ContentObserver(UiThreadUtil.getUiThreadHandler()) { + override fun onChange(selfChange: Boolean) { + this.onChange(selfChange, null) + } + + override fun onChange(selfChange: Boolean, uri: Uri?) { + if (getReactApplicationContext().hasActiveReactInstance()) { + updateAndSendReduceMotionChangeEvent() + } + } + } + private val accessibilityManager: AccessibilityManager? + private val touchExplorationStateChangeListener: ReactTouchExplorationStateChangeListener = + ReactTouchExplorationStateChangeListener() + private val accessibilityServiceChangeListener: ReactAccessibilityServiceChangeListener = + ReactAccessibilityServiceChangeListener() + private val contentResolver: ContentResolver + private var reduceMotionEnabled = false + private var touchExplorationEnabled = false + private var accessibilityServiceEnabled = false + private var recommendedTimeout = 0 + + init { + val appContext = context.applicationContext + accessibilityManager = + appContext.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager + contentResolver = getReactApplicationContext().getContentResolver() + touchExplorationEnabled = accessibilityManager.isTouchExplorationEnabled + accessibilityServiceEnabled = accessibilityManager.isEnabled + reduceMotionEnabled = isReduceMotionEnabledValue + } + + @get:TargetApi(Build.VERSION_CODES.LOLLIPOP) + private val isReduceMotionEnabledValue: Boolean + get() { + // Disabling animations in developer settings will set the animation scale to "0.0" + // but setting "reduce motion" / "disable animations" will set the animation scale to "0". + val rawValue = + Settings.Global.getString(contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) + + // Parse the value as a float so we can check for a single value. + val parsedValue = rawValue?.toFloat() ?: 1f + return parsedValue == 0f + } + + override fun isReduceMotionEnabled(successCallback: Callback) { + successCallback.invoke(reduceMotionEnabled) + } + + override fun isTouchExplorationEnabled(successCallback: Callback) { + successCallback.invoke(touchExplorationEnabled) + } + + override fun isAccessibilityServiceEnabled(successCallback: Callback) { + successCallback.invoke(accessibilityServiceEnabled) + } + + private fun updateAndSendReduceMotionChangeEvent() { + val isReduceMotionEnabled = isReduceMotionEnabledValue + if (reduceMotionEnabled != isReduceMotionEnabled) { + reduceMotionEnabled = isReduceMotionEnabled + val reactApplicationContext = getReactApplicationContextIfActiveOrWarn() + if (reactApplicationContext != null) { + reactApplicationContext.emitDeviceEvent(REDUCE_MOTION_EVENT_NAME, reduceMotionEnabled) + } + } + } + + private fun updateAndSendTouchExplorationChangeEvent(enabled: Boolean) { + if (touchExplorationEnabled != enabled) { + touchExplorationEnabled = enabled + val reactApplicationContext = getReactApplicationContextIfActiveOrWarn() + if (reactApplicationContext != null) { + getReactApplicationContext() + .emitDeviceEvent(TOUCH_EXPLORATION_EVENT_NAME, touchExplorationEnabled) + } + } + } + + private fun updateAndSendAccessibilityServiceChangeEvent(enabled: Boolean) { + if (accessibilityServiceEnabled != enabled) { + accessibilityServiceEnabled = enabled + val reactApplicationContext = getReactApplicationContextIfActiveOrWarn() + if (reactApplicationContext != null) { + getReactApplicationContext() + .emitDeviceEvent(ACCESSIBILITY_SERVICE_EVENT_NAME, accessibilityServiceEnabled) + } + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + override fun onHostResume() { + accessibilityManager?.addTouchExplorationStateChangeListener( + touchExplorationStateChangeListener) + accessibilityManager?.addAccessibilityStateChangeListener(accessibilityServiceChangeListener) + val transitionUri = Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE) + contentResolver.registerContentObserver(transitionUri, false, animationScaleObserver) + updateAndSendTouchExplorationChangeEvent( + accessibilityManager?.isTouchExplorationEnabled == true) + updateAndSendAccessibilityServiceChangeEvent(accessibilityManager?.isEnabled == true) + updateAndSendReduceMotionChangeEvent() + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + override fun onHostPause() { + accessibilityManager?.removeTouchExplorationStateChangeListener( + touchExplorationStateChangeListener) + accessibilityManager?.removeAccessibilityStateChangeListener(accessibilityServiceChangeListener) + contentResolver.unregisterContentObserver(animationScaleObserver) + } + + override fun initialize() { + getReactApplicationContext().addLifecycleEventListener(this) + updateAndSendTouchExplorationChangeEvent( + accessibilityManager?.isTouchExplorationEnabled == true) + updateAndSendAccessibilityServiceChangeEvent(accessibilityManager?.isEnabled == true) + updateAndSendReduceMotionChangeEvent() + } + + override fun invalidate() { + getReactApplicationContext().removeLifecycleEventListener(this) + super.invalidate() + } + + override fun onHostDestroy(): Unit = Unit + + override fun announceForAccessibility(message: String?) { + if (accessibilityManager == null || !accessibilityManager.isEnabled) { + return + } + @Suppress("DEPRECATION") + val event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT) + event.text.add(message) + event.className = AccessibilityInfoModule::class.java.name + event.packageName = getReactApplicationContext().getPackageName() + accessibilityManager.sendAccessibilityEvent(event) + } + + override fun setAccessibilityFocus(reactTag: Double) { + // iOS only + } + + override fun getRecommendedTimeoutMillis(originalTimeout: Double, successCallback: Callback) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + successCallback.invoke(originalTimeout.toInt()) + return + } + recommendedTimeout = + accessibilityManager?.getRecommendedTimeoutMillis( + originalTimeout.toInt(), AccessibilityManager.FLAG_CONTENT_CONTROLS) ?: 0 + successCallback.invoke(recommendedTimeout) + } + + private companion object { + private const val REDUCE_MOTION_EVENT_NAME = "reduceMotionDidChange" + private const val TOUCH_EXPLORATION_EVENT_NAME = "touchExplorationDidChange" + private const val ACCESSIBILITY_SERVICE_EVENT_NAME = "accessibilityServiceDidChange" + } +}