Skip to content

Commit

Permalink
add support for Android's "High Text Contrast" setting into Accessibi…
Browse files Browse the repository at this point in the history
…lityInfo (#46746)

Summary:
Pull Request resolved: #46746

This change adds `isHighTextContrastEnabled()` to `AccessibilityInfo` to enable access to Android OS's "High contrast text" setting option. It also adds a new event, `highContrastTextChanged`, to enable listeners to subscribe to changes on this setting.

## Changelog

[Android][Added] - Added `isHighTextContrastEnabled()` to `AccessibilityInfo` to read `ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED` setting value

Reviewed By: NickGerleman

Differential Revision: D63155444

fbshipit-source-id: 9829b40e6c183f6beba732190dc318894e9d9a3f
  • Loading branch information
Ariel Lin authored and facebook-github-bot committed Oct 2, 2024
1 parent d19a217 commit d4ea147
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type AccessibilityChangeEventName =
| 'grayscaleChanged' // iOS-only Event
| 'invertColorsChanged' // iOS-only Event
| 'reduceMotionChanged'
| 'highTextContrastChanged' // Android-only Event
| 'screenReaderChanged'
| 'reduceTransparencyChanged'; // iOS-only Event

Expand Down Expand Up @@ -69,6 +70,14 @@ export interface AccessibilityInfoStatic {
*/
isReduceMotionEnabled: () => Promise<boolean>;

/**
*
* Query whether high text contrast is currently enabled.
*
* @platform android
*/
isHighTextContrastEnabled: () => Promise<boolean>;

/**
* Query whether reduce motion and prefer cross-fade transitions settings are currently enabled.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import NativeAccessibilityManagerIOS from './NativeAccessibilityManager';
// Events that are only supported on Android.
type AccessibilityEventDefinitionsAndroid = {
accessibilityServiceChanged: [boolean],
highTextContrastChanged: [boolean],
};

// Events that are only supported on iOS.
Expand Down Expand Up @@ -51,6 +52,7 @@ const EventNames: Map<
? new Map([
['change', 'touchExplorationDidChange'],
['reduceMotionChanged', 'reduceMotionDidChange'],
['highTextContrastChanged', 'highTextContrastDidChange'],
['screenReaderChanged', 'touchExplorationDidChange'],
['accessibilityServiceChanged', 'accessibilityServiceDidChange'],
])
Expand Down Expand Up @@ -179,6 +181,26 @@ const AccessibilityInfo = {
});
},

/**
* Query whether high text contrast is currently enabled. Android only.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when high text contrast is enabled and `false` otherwise.
*/
isHighTextContrastEnabled(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (Platform.OS === 'android') {
if (NativeAccessibilityInfoAndroid?.isHighTextContrastEnabled != null) {
NativeAccessibilityInfoAndroid.isHighTextContrastEnabled(resolve);
} else {
reject(null);
}
} else {
return Promise.resolve(false);
}
});
},

/**
* Query whether reduce motion and prefer cross-fade transitions settings are currently enabled.
*
Expand Down Expand Up @@ -320,6 +342,12 @@ const AccessibilityInfo = {
* - `success`: A boolean indicating whether the announcement was
* successfully made.
*
* These events are only supported on Android:
*
* - `highTextContrastChanged`: Android-only event. Fires when the state of the high text contrast
* toggle changes. The argument to the event handler is a boolean. The boolean is `true` when
* high text contrast is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#addeventlistener
*/
addEventListener<K: $Keys<AccessibilityEventDefinitions>>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1559,6 +1559,7 @@ declare module.exports: getData;
exports[`public API should not change unintentionally Libraries/Components/AccessibilityInfo/AccessibilityInfo.js 1`] = `
"type AccessibilityEventDefinitionsAndroid = {
accessibilityServiceChanged: [boolean],
highTextContrastChanged: [boolean],
};
type AccessibilityEventDefinitionsIOS = {
announcementFinished: [{ announcement: string, success: boolean }],
Expand All @@ -1580,6 +1581,7 @@ declare const AccessibilityInfo: {
isGrayscaleEnabled(): Promise<boolean>,
isInvertColorsEnabled(): Promise<boolean>,
isReduceMotionEnabled(): Promise<boolean>,
isHighTextContrastEnabled(): Promise<boolean>,
prefersCrossFadeTransitions(): Promise<boolean>,
isReduceTransparencyEnabled(): Promise<boolean>,
isScreenReaderEnabled(): Promise<boolean>,
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -3021,6 +3021,7 @@ public final class com/facebook/react/modules/accessibilityinfo/AccessibilityInf
public fun initialize ()V
public fun invalidate ()V
public fun isAccessibilityServiceEnabled (Lcom/facebook/react/bridge/Callback;)V
public fun isHighTextContrastEnabled (Lcom/facebook/react/bridge/Callback;)V
public fun isReduceMotionEnabled (Lcom/facebook/react/bridge/Callback;)V
public fun isTouchExplorationEnabled (Lcom/facebook/react/bridge/Callback;)V
public fun onHostDestroy ()V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,27 @@ public class AccessibilityInfoModule(context: ReactApplicationContext) :
}
}
}
// Listener that is notified when the ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED changes.
private val highTextContrastObserver: ContentObserver =
object : ContentObserver(UiThreadUtil.getUiThreadHandler()) {
override fun onChange(selfChange: Boolean) {
this.onChange(selfChange, null)
}

override fun onChange(selfChange: Boolean, uri: Uri?) {
if (getReactApplicationContext().hasActiveReactInstance()) {
updateAndSendHighTextContrastChangeEvent()
}
}
}
private val accessibilityManager: AccessibilityManager?
private val touchExplorationStateChangeListener: ReactTouchExplorationStateChangeListener =
ReactTouchExplorationStateChangeListener()
private val accessibilityServiceChangeListener: ReactAccessibilityServiceChangeListener =
ReactAccessibilityServiceChangeListener()
private val contentResolver: ContentResolver
private var reduceMotionEnabled = false
private var highTextContrastEnabled = false
private var touchExplorationEnabled = false
private var accessibilityServiceEnabled = false
private var recommendedTimeout = 0
Expand All @@ -81,6 +95,7 @@ public class AccessibilityInfoModule(context: ReactApplicationContext) :
touchExplorationEnabled = accessibilityManager.isTouchExplorationEnabled
accessibilityServiceEnabled = accessibilityManager.isEnabled
reduceMotionEnabled = isReduceMotionEnabledValue
highTextContrastEnabled = isHighTextContrastEnabledValue
}

@get:TargetApi(Build.VERSION_CODES.LOLLIPOP)
Expand All @@ -96,10 +111,24 @@ public class AccessibilityInfoModule(context: ReactApplicationContext) :
return parsedValue == 0f
}

@get:TargetApi(Build.VERSION_CODES.LOLLIPOP)
private val isHighTextContrastEnabledValue: Boolean
get() {
return Settings.Secure.getInt(
contentResolver,
ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED_CONSTANT,
0,
) != 0
}

override fun isReduceMotionEnabled(successCallback: Callback) {
successCallback.invoke(reduceMotionEnabled)
}

override fun isHighTextContrastEnabled(successCallback: Callback) {
successCallback.invoke(highTextContrastEnabled)
}

override fun isTouchExplorationEnabled(successCallback: Callback) {
successCallback.invoke(touchExplorationEnabled)
}
Expand All @@ -119,6 +148,20 @@ public class AccessibilityInfoModule(context: ReactApplicationContext) :
}
}

private fun updateAndSendHighTextContrastChangeEvent() {
val isHighTextContrastEnabled = isHighTextContrastEnabledValue
if (highTextContrastEnabled != isHighTextContrastEnabled) {
highTextContrastEnabled = isHighTextContrastEnabled
val reactApplicationContext = getReactApplicationContextIfActiveOrWarn()
if (reactApplicationContext != null) {
reactApplicationContext.emitDeviceEvent(
HIGH_TEXT_CONTRAST_EVENT_NAME,
highTextContrastEnabled,
)
}
}
}

private fun updateAndSendTouchExplorationChangeEvent(enabled: Boolean) {
if (touchExplorationEnabled != enabled) {
touchExplorationEnabled = enabled
Expand Down Expand Up @@ -148,10 +191,14 @@ public class AccessibilityInfoModule(context: ReactApplicationContext) :
accessibilityManager?.addAccessibilityStateChangeListener(accessibilityServiceChangeListener)
val transitionUri = Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE)
contentResolver.registerContentObserver(transitionUri, false, animationScaleObserver)
val highTextContrastUri =
Settings.Global.getUriFor(ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED_CONSTANT)
contentResolver.registerContentObserver(highTextContrastUri, false, highTextContrastObserver)
updateAndSendTouchExplorationChangeEvent(
accessibilityManager?.isTouchExplorationEnabled == true)
updateAndSendAccessibilityServiceChangeEvent(accessibilityManager?.isEnabled == true)
updateAndSendReduceMotionChangeEvent()
updateAndSendHighTextContrastChangeEvent()
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
Expand All @@ -160,6 +207,7 @@ public class AccessibilityInfoModule(context: ReactApplicationContext) :
touchExplorationStateChangeListener)
accessibilityManager?.removeAccessibilityStateChangeListener(accessibilityServiceChangeListener)
contentResolver.unregisterContentObserver(animationScaleObserver)
contentResolver.unregisterContentObserver(highTextContrastObserver)
}

override fun initialize() {
Expand All @@ -168,6 +216,7 @@ public class AccessibilityInfoModule(context: ReactApplicationContext) :
accessibilityManager?.isTouchExplorationEnabled == true)
updateAndSendAccessibilityServiceChangeEvent(accessibilityManager?.isEnabled == true)
updateAndSendReduceMotionChangeEvent()
updateAndSendHighTextContrastChangeEvent()
}

override fun invalidate() {
Expand Down Expand Up @@ -200,13 +249,18 @@ public class AccessibilityInfoModule(context: ReactApplicationContext) :
}
recommendedTimeout =
accessibilityManager?.getRecommendedTimeoutMillis(
originalTimeout.toInt(), AccessibilityManager.FLAG_CONTENT_CONTROLS) ?: 0
originalTimeout.toInt(),
AccessibilityManager.FLAG_CONTENT_CONTROLS,
) ?: 0
successCallback.invoke(recommendedTimeout)
}

private companion object {
private const val REDUCE_MOTION_EVENT_NAME = "reduceMotionDidChange"
private const val HIGH_TEXT_CONTRAST_EVENT_NAME = "highTextContrastDidChange"
private const val TOUCH_EXPLORATION_EVENT_NAME = "touchExplorationDidChange"
private const val ACCESSIBILITY_SERVICE_EVENT_NAME = "accessibilityServiceDidChange"
private const val ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED_CONSTANT =
"high_text_contrast_enabled" // constant is marked with @hide
}
}
1 change: 1 addition & 0 deletions packages/react-native/jest/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ jest
isGrayscaleEnabled: jest.fn(() => Promise.resolve(false)),
isInvertColorsEnabled: jest.fn(() => Promise.resolve(false)),
isReduceMotionEnabled: jest.fn(() => Promise.resolve(false)),
isHighTextContrastEnabled: jest.fn(() => Promise.resolve(false)),
prefersCrossFadeTransitions: jest.fn(() => Promise.resolve(false)),
isReduceTransparencyEnabled: jest.fn(() => Promise.resolve(false)),
isScreenReaderEnabled: jest.fn(() => Promise.resolve(false)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export interface Spec extends TurboModule {
+isReduceMotionEnabled: (
onSuccess: (isReduceMotionEnabled: boolean) => void,
) => void;
+isHighTextContrastEnabled?: (
onSuccess: (isHighTextContrastEnabled: boolean) => void,
) => void;
+isTouchExplorationEnabled: (
onSuccess: (isScreenReaderEnabled: boolean) => void,
) => void;
Expand Down

0 comments on commit d4ea147

Please sign in to comment.