From e70202e606be3101da33d28f5403443c3e84749e Mon Sep 17 00:00:00 2001 From: oddlyspaced Date: Mon, 11 Nov 2024 07:38:21 -0800 Subject: [PATCH] feat(Android): add support for detecting grayscale mode enabled on android (#47497) Summary: On android the isGrayScaleEnabled method of AccessibilityInfo always returns false due to missing implementation. This PR fills the gap by providing the native module logic for checking grayscale mode. ## Changelog: - Added native module code to check for grayscale mode on android - Updated js accessibility info module to return the correct promise instead of default false for isGrayScaleEnabled() - Moved the test for isGrayScaleEnabled() out of ios scope to common Android and iOS scope [ANDROID] [ADDED] - logic to check for grayscale mode on android Pull Request resolved: https://github.com/facebook/react-native/pull/47497 Test Plan: Tested on : - Google Pixel 7 Pro (Android 14) - OnePlus 12 (Android 14) - Pixel 6 (Android 15) Reviewed By: cortinico Differential Revision: D65662583 Pulled By: javache fbshipit-source-id: 39f9ce37c9375b5380257847395393045eedbadc --- .../AccessibilityInfo/AccessibilityInfo.js | 9 ++++- .../AccessibilityInfoModule.kt | 34 +++++++++++++++++++ .../specs/modules/NativeAccessibilityInfo.js | 3 ++ .../Accessibility/AccessibilityExample.js | 9 ++--- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js index 1e7c70d265a..ccae5960ac2 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js @@ -56,6 +56,7 @@ const EventNames: Map< ['screenReaderChanged', 'touchExplorationDidChange'], ['accessibilityServiceChanged', 'accessibilityServiceDidChange'], ['invertColorsChanged', 'invertColorDidChange'], + ['grayscaleChanged', 'grayscaleModeDidChange'], ]) : new Map([ ['announcementFinished', 'announcementFinished'], @@ -114,7 +115,13 @@ const AccessibilityInfo = { */ isGrayscaleEnabled(): Promise { if (Platform.OS === 'android') { - return Promise.resolve(false); + return new Promise((resolve, reject) => { + if (NativeAccessibilityInfoAndroid?.isGrayscaleEnabled != null) { + NativeAccessibilityInfoAndroid.isGrayscaleEnabled(resolve); + } else { + reject(null); + } + }); } else { return new Promise((resolve, reject) => { if (NativeAccessibilityManagerIOS != null) { 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 index 4f8ed62c7cd..9c835f1e9b4 100644 --- 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 @@ -87,6 +87,7 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : private var accessibilityServiceEnabled = false private var recommendedTimeout = 0 private var invertColorsEnabled = false + private var grayscaleModeEnabled = false init { val appContext = context.applicationContext @@ -97,6 +98,7 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : accessibilityServiceEnabled = accessibilityManager.isEnabled reduceMotionEnabled = isReduceMotionEnabledValue highTextContrastEnabled = isHighTextContrastEnabledValue + grayscaleModeEnabled = isGrayscaleEnabledValue } @get:TargetApi(Build.VERSION_CODES.LOLLIPOP) @@ -123,6 +125,21 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : } } + @get:TargetApi(Build.VERSION_CODES.LOLLIPOP) + private val isGrayscaleEnabledValue: Boolean + get() { + try { + val colorCorrectionSettingKey = "accessibility_display_daltonizer_enabled" + val colorModeSettingKey = "accessibility_display_daltonizer" + // for grayscale mode to be detected, the color correction accessibility setting should be + // on and the color correction mode should be set to grayscale (0) + return Settings.Secure.getInt(contentResolver, colorCorrectionSettingKey) == 1 && + Settings.Secure.getInt(contentResolver, colorModeSettingKey) == 0 + } catch (e: Settings.SettingNotFoundException) { + return false + } + } + @get:TargetApi(Build.VERSION_CODES.LOLLIPOP) private val isHighTextContrastEnabledValue: Boolean get() { @@ -141,6 +158,10 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : successCallback.invoke(invertColorsEnabled) } + override fun isGrayscaleEnabled(successCallback: Callback) { + successCallback.invoke(grayscaleModeEnabled) + } + override fun isHighTextContrastEnabled(successCallback: Callback) { successCallback.invoke(highTextContrastEnabled) } @@ -211,6 +232,17 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : } } + private fun updateAndSendGrayscaleModeChangeEvent() { + val isGrayscaleModeEnabled = isGrayscaleEnabledValue + if (grayscaleModeEnabled != isGrayscaleModeEnabled) { + grayscaleModeEnabled = isGrayscaleModeEnabled + val reactApplicationContext = getReactApplicationContextIfActiveOrWarn() + if (reactApplicationContext != null) { + reactApplicationContext.emitDeviceEvent(GRAYSCALE_MODE_EVENT_NAME, grayscaleModeEnabled) + } + } + } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) override fun onHostResume() { accessibilityManager?.addTouchExplorationStateChangeListener( @@ -227,6 +259,7 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : updateAndSendReduceMotionChangeEvent() updateAndSendHighTextContrastChangeEvent() updateAndSendInvertColorsChangeEvent() + updateAndSendGrayscaleModeChangeEvent() } @TargetApi(Build.VERSION_CODES.LOLLIPOP) @@ -292,5 +325,6 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : private const val ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED_CONSTANT = "high_text_contrast_enabled" // constant is marked with @hide private const val INVERT_COLOR_EVENT_NAME = "invertColorDidChange" + private const val GRAYSCALE_MODE_EVENT_NAME = "grayscaleModeDidChange" } } diff --git a/packages/react-native/src/private/specs/modules/NativeAccessibilityInfo.js b/packages/react-native/src/private/specs/modules/NativeAccessibilityInfo.js index 07b4b17622a..358134c9ed1 100644 --- a/packages/react-native/src/private/specs/modules/NativeAccessibilityInfo.js +++ b/packages/react-native/src/private/specs/modules/NativeAccessibilityInfo.js @@ -34,6 +34,9 @@ export interface Spec extends TurboModule { mSec: number, onSuccess: (recommendedTimeoutMillis: number) => void, ) => void; + +isGrayscaleEnabled?: ( + onSuccess: (isGrayscaleEnabled: boolean) => void, + ) => void; } export default (TurboModuleRegistry.get('AccessibilityInfo'): ?Spec); diff --git a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js index 49090ce8365..576f82ce201 100644 --- a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js +++ b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js @@ -1336,12 +1336,6 @@ class EnabledExamples extends React.Component<{}> { eventListener="boldTextChanged" /> - - - { eventListener="screenReaderChanged" /> + + + ); }