Skip to content

Commit d4ea147

Browse files
Ariel Linfacebook-github-bot
Ariel Lin
authored andcommitted
add support for Android's "High Text Contrast" setting into AccessibilityInfo (#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
1 parent d19a217 commit d4ea147

File tree

7 files changed

+99
-1
lines changed

7 files changed

+99
-1
lines changed

packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type AccessibilityChangeEventName =
1717
| 'grayscaleChanged' // iOS-only Event
1818
| 'invertColorsChanged' // iOS-only Event
1919
| 'reduceMotionChanged'
20+
| 'highTextContrastChanged' // Android-only Event
2021
| 'screenReaderChanged'
2122
| 'reduceTransparencyChanged'; // iOS-only Event
2223

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

73+
/**
74+
*
75+
* Query whether high text contrast is currently enabled.
76+
*
77+
* @platform android
78+
*/
79+
isHighTextContrastEnabled: () => Promise<boolean>;
80+
7281
/**
7382
* Query whether reduce motion and prefer cross-fade transitions settings are currently enabled.
7483
*

packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js

+28
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import NativeAccessibilityManagerIOS from './NativeAccessibilityManager';
2222
// Events that are only supported on Android.
2323
type AccessibilityEventDefinitionsAndroid = {
2424
accessibilityServiceChanged: [boolean],
25+
highTextContrastChanged: [boolean],
2526
};
2627

2728
// Events that are only supported on iOS.
@@ -51,6 +52,7 @@ const EventNames: Map<
5152
? new Map([
5253
['change', 'touchExplorationDidChange'],
5354
['reduceMotionChanged', 'reduceMotionDidChange'],
55+
['highTextContrastChanged', 'highTextContrastDidChange'],
5456
['screenReaderChanged', 'touchExplorationDidChange'],
5557
['accessibilityServiceChanged', 'accessibilityServiceDidChange'],
5658
])
@@ -179,6 +181,26 @@ const AccessibilityInfo = {
179181
});
180182
},
181183

184+
/**
185+
* Query whether high text contrast is currently enabled. Android only.
186+
*
187+
* Returns a promise which resolves to a boolean.
188+
* The result is `true` when high text contrast is enabled and `false` otherwise.
189+
*/
190+
isHighTextContrastEnabled(): Promise<boolean> {
191+
return new Promise((resolve, reject) => {
192+
if (Platform.OS === 'android') {
193+
if (NativeAccessibilityInfoAndroid?.isHighTextContrastEnabled != null) {
194+
NativeAccessibilityInfoAndroid.isHighTextContrastEnabled(resolve);
195+
} else {
196+
reject(null);
197+
}
198+
} else {
199+
return Promise.resolve(false);
200+
}
201+
});
202+
},
203+
182204
/**
183205
* Query whether reduce motion and prefer cross-fade transitions settings are currently enabled.
184206
*
@@ -320,6 +342,12 @@ const AccessibilityInfo = {
320342
* - `success`: A boolean indicating whether the announcement was
321343
* successfully made.
322344
*
345+
* These events are only supported on Android:
346+
*
347+
* - `highTextContrastChanged`: Android-only event. Fires when the state of the high text contrast
348+
* toggle changes. The argument to the event handler is a boolean. The boolean is `true` when
349+
* high text contrast is enabled and `false` otherwise.
350+
*
323351
* See https://reactnative.dev/docs/accessibilityinfo#addeventlistener
324352
*/
325353
addEventListener<K: $Keys<AccessibilityEventDefinitions>>(

packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap

+2
Original file line numberDiff line numberDiff line change
@@ -1559,6 +1559,7 @@ declare module.exports: getData;
15591559
exports[`public API should not change unintentionally Libraries/Components/AccessibilityInfo/AccessibilityInfo.js 1`] = `
15601560
"type AccessibilityEventDefinitionsAndroid = {
15611561
accessibilityServiceChanged: [boolean],
1562+
highTextContrastChanged: [boolean],
15621563
};
15631564
type AccessibilityEventDefinitionsIOS = {
15641565
announcementFinished: [{ announcement: string, success: boolean }],
@@ -1580,6 +1581,7 @@ declare const AccessibilityInfo: {
15801581
isGrayscaleEnabled(): Promise<boolean>,
15811582
isInvertColorsEnabled(): Promise<boolean>,
15821583
isReduceMotionEnabled(): Promise<boolean>,
1584+
isHighTextContrastEnabled(): Promise<boolean>,
15831585
prefersCrossFadeTransitions(): Promise<boolean>,
15841586
isReduceTransparencyEnabled(): Promise<boolean>,
15851587
isScreenReaderEnabled(): Promise<boolean>,

packages/react-native/ReactAndroid/api/ReactAndroid.api

+1
Original file line numberDiff line numberDiff line change
@@ -3021,6 +3021,7 @@ public final class com/facebook/react/modules/accessibilityinfo/AccessibilityInf
30213021
public fun initialize ()V
30223022
public fun invalidate ()V
30233023
public fun isAccessibilityServiceEnabled (Lcom/facebook/react/bridge/Callback;)V
3024+
public fun isHighTextContrastEnabled (Lcom/facebook/react/bridge/Callback;)V
30243025
public fun isReduceMotionEnabled (Lcom/facebook/react/bridge/Callback;)V
30253026
public fun isTouchExplorationEnabled (Lcom/facebook/react/bridge/Callback;)V
30263027
public fun onHostDestroy ()V

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.kt

+55-1
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,27 @@ public class AccessibilityInfoModule(context: ReactApplicationContext) :
6262
}
6363
}
6464
}
65+
// Listener that is notified when the ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED changes.
66+
private val highTextContrastObserver: ContentObserver =
67+
object : ContentObserver(UiThreadUtil.getUiThreadHandler()) {
68+
override fun onChange(selfChange: Boolean) {
69+
this.onChange(selfChange, null)
70+
}
71+
72+
override fun onChange(selfChange: Boolean, uri: Uri?) {
73+
if (getReactApplicationContext().hasActiveReactInstance()) {
74+
updateAndSendHighTextContrastChangeEvent()
75+
}
76+
}
77+
}
6578
private val accessibilityManager: AccessibilityManager?
6679
private val touchExplorationStateChangeListener: ReactTouchExplorationStateChangeListener =
6780
ReactTouchExplorationStateChangeListener()
6881
private val accessibilityServiceChangeListener: ReactAccessibilityServiceChangeListener =
6982
ReactAccessibilityServiceChangeListener()
7083
private val contentResolver: ContentResolver
7184
private var reduceMotionEnabled = false
85+
private var highTextContrastEnabled = false
7286
private var touchExplorationEnabled = false
7387
private var accessibilityServiceEnabled = false
7488
private var recommendedTimeout = 0
@@ -81,6 +95,7 @@ public class AccessibilityInfoModule(context: ReactApplicationContext) :
8195
touchExplorationEnabled = accessibilityManager.isTouchExplorationEnabled
8296
accessibilityServiceEnabled = accessibilityManager.isEnabled
8397
reduceMotionEnabled = isReduceMotionEnabledValue
98+
highTextContrastEnabled = isHighTextContrastEnabledValue
8499
}
85100

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

114+
@get:TargetApi(Build.VERSION_CODES.LOLLIPOP)
115+
private val isHighTextContrastEnabledValue: Boolean
116+
get() {
117+
return Settings.Secure.getInt(
118+
contentResolver,
119+
ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED_CONSTANT,
120+
0,
121+
) != 0
122+
}
123+
99124
override fun isReduceMotionEnabled(successCallback: Callback) {
100125
successCallback.invoke(reduceMotionEnabled)
101126
}
102127

128+
override fun isHighTextContrastEnabled(successCallback: Callback) {
129+
successCallback.invoke(highTextContrastEnabled)
130+
}
131+
103132
override fun isTouchExplorationEnabled(successCallback: Callback) {
104133
successCallback.invoke(touchExplorationEnabled)
105134
}
@@ -119,6 +148,20 @@ public class AccessibilityInfoModule(context: ReactApplicationContext) :
119148
}
120149
}
121150

151+
private fun updateAndSendHighTextContrastChangeEvent() {
152+
val isHighTextContrastEnabled = isHighTextContrastEnabledValue
153+
if (highTextContrastEnabled != isHighTextContrastEnabled) {
154+
highTextContrastEnabled = isHighTextContrastEnabled
155+
val reactApplicationContext = getReactApplicationContextIfActiveOrWarn()
156+
if (reactApplicationContext != null) {
157+
reactApplicationContext.emitDeviceEvent(
158+
HIGH_TEXT_CONTRAST_EVENT_NAME,
159+
highTextContrastEnabled,
160+
)
161+
}
162+
}
163+
}
164+
122165
private fun updateAndSendTouchExplorationChangeEvent(enabled: Boolean) {
123166
if (touchExplorationEnabled != enabled) {
124167
touchExplorationEnabled = enabled
@@ -148,10 +191,14 @@ public class AccessibilityInfoModule(context: ReactApplicationContext) :
148191
accessibilityManager?.addAccessibilityStateChangeListener(accessibilityServiceChangeListener)
149192
val transitionUri = Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE)
150193
contentResolver.registerContentObserver(transitionUri, false, animationScaleObserver)
194+
val highTextContrastUri =
195+
Settings.Global.getUriFor(ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED_CONSTANT)
196+
contentResolver.registerContentObserver(highTextContrastUri, false, highTextContrastObserver)
151197
updateAndSendTouchExplorationChangeEvent(
152198
accessibilityManager?.isTouchExplorationEnabled == true)
153199
updateAndSendAccessibilityServiceChangeEvent(accessibilityManager?.isEnabled == true)
154200
updateAndSendReduceMotionChangeEvent()
201+
updateAndSendHighTextContrastChangeEvent()
155202
}
156203

157204
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@@ -160,6 +207,7 @@ public class AccessibilityInfoModule(context: ReactApplicationContext) :
160207
touchExplorationStateChangeListener)
161208
accessibilityManager?.removeAccessibilityStateChangeListener(accessibilityServiceChangeListener)
162209
contentResolver.unregisterContentObserver(animationScaleObserver)
210+
contentResolver.unregisterContentObserver(highTextContrastObserver)
163211
}
164212

165213
override fun initialize() {
@@ -168,6 +216,7 @@ public class AccessibilityInfoModule(context: ReactApplicationContext) :
168216
accessibilityManager?.isTouchExplorationEnabled == true)
169217
updateAndSendAccessibilityServiceChangeEvent(accessibilityManager?.isEnabled == true)
170218
updateAndSendReduceMotionChangeEvent()
219+
updateAndSendHighTextContrastChangeEvent()
171220
}
172221

173222
override fun invalidate() {
@@ -200,13 +249,18 @@ public class AccessibilityInfoModule(context: ReactApplicationContext) :
200249
}
201250
recommendedTimeout =
202251
accessibilityManager?.getRecommendedTimeoutMillis(
203-
originalTimeout.toInt(), AccessibilityManager.FLAG_CONTENT_CONTROLS) ?: 0
252+
originalTimeout.toInt(),
253+
AccessibilityManager.FLAG_CONTENT_CONTROLS,
254+
) ?: 0
204255
successCallback.invoke(recommendedTimeout)
205256
}
206257

207258
private companion object {
208259
private const val REDUCE_MOTION_EVENT_NAME = "reduceMotionDidChange"
260+
private const val HIGH_TEXT_CONTRAST_EVENT_NAME = "highTextContrastDidChange"
209261
private const val TOUCH_EXPLORATION_EVENT_NAME = "touchExplorationDidChange"
210262
private const val ACCESSIBILITY_SERVICE_EVENT_NAME = "accessibilityServiceDidChange"
263+
private const val ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED_CONSTANT =
264+
"high_text_contrast_enabled" // constant is marked with @hide
211265
}
212266
}

packages/react-native/jest/setup.js

+1
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ jest
151151
isGrayscaleEnabled: jest.fn(() => Promise.resolve(false)),
152152
isInvertColorsEnabled: jest.fn(() => Promise.resolve(false)),
153153
isReduceMotionEnabled: jest.fn(() => Promise.resolve(false)),
154+
isHighTextContrastEnabled: jest.fn(() => Promise.resolve(false)),
154155
prefersCrossFadeTransitions: jest.fn(() => Promise.resolve(false)),
155156
isReduceTransparencyEnabled: jest.fn(() => Promise.resolve(false)),
156157
isScreenReaderEnabled: jest.fn(() => Promise.resolve(false)),

packages/react-native/src/private/specs/modules/NativeAccessibilityInfo.js

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export interface Spec extends TurboModule {
1616
+isReduceMotionEnabled: (
1717
onSuccess: (isReduceMotionEnabled: boolean) => void,
1818
) => void;
19+
+isHighTextContrastEnabled?: (
20+
onSuccess: (isHighTextContrastEnabled: boolean) => void,
21+
) => void;
1922
+isTouchExplorationEnabled: (
2023
onSuccess: (isScreenReaderEnabled: boolean) => void,
2124
) => void;

0 commit comments

Comments
 (0)