Skip to content

Commit 694600a

Browse files
authored
Add announce support to the engine (flutter#169685)
Partly of flutter#165510 ⤵️ Child PR: flutter#168992 Partly re-lands flutter#165531 The PR was originally reverted due to an issue with an internal Google test. I split re-land PR into two separate ones so that we can individually revert in case it fails again. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent 5c4edc2 commit 694600a

File tree

9 files changed

+120
-11
lines changed

9 files changed

+120
-11
lines changed

engine/src/flutter/lib/ui/window.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,7 @@ class AccessibilityFeatures {
931931
static const int _kReduceMotionIndex = 1 << 4;
932932
static const int _kHighContrastIndex = 1 << 5;
933933
static const int _kOnOffSwitchLabelsIndex = 1 << 6;
934+
static const int _kNoAnnounceIndex = 1 << 7;
934935

935936
// A bitfield which represents each enabled feature.
936937
final int _index;
@@ -968,6 +969,20 @@ class AccessibilityFeatures {
968969
/// Only supported on iOS.
969970
bool get onOffSwitchLabels => _kOnOffSwitchLabelsIndex & _index != 0;
970971

972+
/// Whether accessibility announcements (like [SemanticsService.announce])
973+
/// are supported on the current platform.
974+
///
975+
/// Returns `false` on platforms where announcements are deprecated or
976+
/// unsupported by the underlying platform.
977+
///
978+
/// Returns `true` on platforms where such announcements are
979+
/// generally supported without discouragement. (iOS, web etc)
980+
///
981+
/// Use this flag to conditionally avoid making announcements on Android.
982+
// This index check is inverted (== 0 vs != 0); far more platforms support
983+
// "announce" than discourage it.
984+
bool get announce => _kNoAnnounceIndex & _index == 0;
985+
971986
@override
972987
String toString() {
973988
final List<String> features = <String>[];
@@ -992,6 +1007,9 @@ class AccessibilityFeatures {
9921007
if (onOffSwitchLabels) {
9931008
features.add('onOffSwitchLabels');
9941009
}
1010+
if (announce) {
1011+
features.add('announce');
1012+
}
9951013
return 'AccessibilityFeatures$features';
9961014
}
9971015

engine/src/flutter/lib/ui/window/platform_configuration.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ enum class AccessibilityFeatureFlag : int32_t {
4848
kReduceMotion = 1 << 4,
4949
kHighContrast = 1 << 5,
5050
kOnOffSwitchLabels = 1 << 6,
51+
kNoAnnounce = 1 << 7,
5152
};
5253

5354
//--------------------------------------------------------------------------

engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
5454
static const int _kReduceMotionIndex = 1 << 4;
5555
static const int _kHighContrastIndex = 1 << 5;
5656
static const int _kOnOffSwitchLabelsIndex = 1 << 6;
57+
static const int _kNoAnnounceIndex = 1 << 7;
5758

5859
// A bitfield which represents each enabled feature.
5960
final int _index;
@@ -72,6 +73,10 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
7273
bool get highContrast => _kHighContrastIndex & _index != 0;
7374
@override
7475
bool get onOffSwitchLabels => _kOnOffSwitchLabelsIndex & _index != 0;
76+
// This index check is inverted (== 0 vs != 0); far more platforms support
77+
// "announce" than discourage it.
78+
@override
79+
bool get announce => _kNoAnnounceIndex & _index == 0;
7580

7681
@override
7782
String toString() {
@@ -97,6 +102,9 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
97102
if (onOffSwitchLabels) {
98103
features.add('onOffSwitchLabels');
99104
}
105+
if (announce) {
106+
features.add('announce');
107+
}
100108
return 'AccessibilityFeatures$features';
101109
}
102110

@@ -119,6 +127,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
119127
bool? reduceMotion,
120128
bool? highContrast,
121129
bool? onOffSwitchLabels,
130+
bool? announce,
122131
}) {
123132
final EngineAccessibilityFeaturesBuilder builder = EngineAccessibilityFeaturesBuilder(0);
124133

@@ -129,6 +138,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
129138
builder.reduceMotion = reduceMotion ?? this.reduceMotion;
130139
builder.highContrast = highContrast ?? this.highContrast;
131140
builder.onOffSwitchLabels = onOffSwitchLabels ?? this.onOffSwitchLabels;
141+
builder.announce = announce ?? this.announce;
132142

133143
return builder.build();
134144
}
@@ -146,6 +156,9 @@ class EngineAccessibilityFeaturesBuilder {
146156
bool get reduceMotion => EngineAccessibilityFeatures._kReduceMotionIndex & _index != 0;
147157
bool get highContrast => EngineAccessibilityFeatures._kHighContrastIndex & _index != 0;
148158
bool get onOffSwitchLabels => EngineAccessibilityFeatures._kOnOffSwitchLabelsIndex & _index != 0;
159+
// This index check is inverted (== 0 vs != 0); far more platforms support
160+
// "announce" than discourage it.
161+
bool get announce => EngineAccessibilityFeatures._kNoAnnounceIndex & _index == 0;
149162

150163
set accessibleNavigation(bool value) {
151164
const int accessibleNavigation = EngineAccessibilityFeatures._kAccessibleNavigation;
@@ -182,6 +195,12 @@ class EngineAccessibilityFeaturesBuilder {
182195
_index = value ? _index | onOffSwitchLabels : _index & ~onOffSwitchLabels;
183196
}
184197

198+
set announce(bool value) {
199+
const int noAnnounce = EngineAccessibilityFeatures._kNoAnnounceIndex;
200+
// Since we are using noAnnounce for the embedder, we need to flip the value.
201+
_index = !value ? _index | noAnnounce : _index & ~noAnnounce;
202+
}
203+
185204
/// Creates and returns an instance of EngineAccessibilityFeatures based on the value of _index
186205
EngineAccessibilityFeatures build() {
187206
return EngineAccessibilityFeatures(_index);

engine/src/flutter/lib/web_ui/lib/window.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ abstract class AccessibilityFeatures {
115115
bool get reduceMotion;
116116
bool get highContrast;
117117
bool get onOffSwitchLabels;
118+
bool get announce;
118119
}
119120

120121
enum Brightness { dark, light }

engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,14 @@ void _testEngineAccessibilityBuilder() {
299299
expect(features.onOffSwitchLabels, isTrue);
300300
});
301301

302+
test('announce', () {
303+
// By default this starts off true, see EngineAccessibilityFeatures.announce
304+
expect(features.announce, isTrue);
305+
builder.announce = false;
306+
features = builder.build();
307+
expect(features.announce, isFalse);
308+
});
309+
302310
test('reduce motion', () {
303311
expect(features.reduceMotion, isFalse);
304312
builder.reduceMotion = true;
@@ -391,14 +399,18 @@ void _testEngineSemanticsOwner() {
391399
});
392400

393401
test('accessibilityFeatures copyWith function works', () {
394-
const EngineAccessibilityFeatures original = EngineAccessibilityFeatures(0);
402+
// Announce is an inverted check, see EngineAccessibilityFeatures.announce.
403+
// Therefore, we need to ensure that the original copy starts with false (1 << 7).
404+
const EngineAccessibilityFeatures original = EngineAccessibilityFeatures(0 | 1 << 7);
405+
395406
EngineAccessibilityFeatures copy = original.copyWith(accessibleNavigation: true);
396407
expect(copy.accessibleNavigation, true);
397408
expect(copy.boldText, false);
398409
expect(copy.disableAnimations, false);
399410
expect(copy.highContrast, false);
400411
expect(copy.invertColors, false);
401412
expect(copy.onOffSwitchLabels, false);
413+
expect(copy.announce, false);
402414
expect(copy.reduceMotion, false);
403415

404416
copy = original.copyWith(boldText: true);
@@ -417,6 +429,7 @@ void _testEngineSemanticsOwner() {
417429
expect(copy.highContrast, false);
418430
expect(copy.invertColors, false);
419431
expect(copy.onOffSwitchLabels, false);
432+
expect(copy.announce, false);
420433
expect(copy.reduceMotion, false);
421434

422435
copy = original.copyWith(highContrast: true);
@@ -426,6 +439,7 @@ void _testEngineSemanticsOwner() {
426439
expect(copy.highContrast, true);
427440
expect(copy.invertColors, false);
428441
expect(copy.onOffSwitchLabels, false);
442+
expect(copy.announce, false);
429443
expect(copy.reduceMotion, false);
430444

431445
copy = original.copyWith(invertColors: true);
@@ -435,6 +449,7 @@ void _testEngineSemanticsOwner() {
435449
expect(copy.highContrast, false);
436450
expect(copy.invertColors, true);
437451
expect(copy.onOffSwitchLabels, false);
452+
expect(copy.announce, false);
438453
expect(copy.reduceMotion, false);
439454

440455
copy = original.copyWith(onOffSwitchLabels: true);
@@ -446,13 +461,24 @@ void _testEngineSemanticsOwner() {
446461
expect(copy.onOffSwitchLabels, true);
447462
expect(copy.reduceMotion, false);
448463

464+
copy = original.copyWith(announce: true);
465+
expect(copy.accessibleNavigation, false);
466+
expect(copy.boldText, false);
467+
expect(copy.disableAnimations, false);
468+
expect(copy.highContrast, false);
469+
expect(copy.invertColors, false);
470+
expect(copy.onOffSwitchLabels, false);
471+
expect(copy.announce, true);
472+
expect(copy.reduceMotion, false);
473+
449474
copy = original.copyWith(reduceMotion: true);
450475
expect(copy.accessibleNavigation, false);
451476
expect(copy.boldText, false);
452477
expect(copy.disableAnimations, false);
453478
expect(copy.highContrast, false);
454479
expect(copy.invertColors, false);
455480
expect(copy.onOffSwitchLabels, false);
481+
expect(copy.announce, false);
456482
expect(copy.reduceMotion, true);
457483
});
458484

engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,7 @@ public void onTouchExplorationStateChanged(boolean isTouchExplorationEnabled) {
490490
this.accessibilityManager.addTouchExplorationStateChangeListener(
491491
touchExplorationStateChangeListener);
492492

493+
accessibilityFeatureFlags |= AccessibilityFeature.NO_ANNOUNCE.value;
493494
// Tell Flutter whether animations should initially be enabled or disabled. Then register a
494495
// listener to be notified of changes in the future.
495496
animationScaleObserver.onChange(false);
@@ -2174,7 +2175,8 @@ private enum AccessibilityFeature {
21742175
BOLD_TEXT(1 << 3), // NOT SUPPORTED
21752176
REDUCE_MOTION(1 << 4), // NOT SUPPORTED
21762177
HIGH_CONTRAST(1 << 5), // NOT SUPPORTED
2177-
ON_OFF_SWITCH_LABELS(1 << 6); // NOT SUPPORTED
2178+
ON_OFF_SWITCH_LABELS(1 << 6), // NOT SUPPORTED
2179+
NO_ANNOUNCE(1 << 7);
21782180

21792181
final int value;
21802182

engine/src/flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@
6666
@RunWith(AndroidJUnit4.class)
6767
public class AccessibilityBridgeTest {
6868

69+
private static final int ACCESSIBILITY_FEATURE_NAVIGATION = 1 << 0;
70+
private static final int ACCESSIBILITY_FEATURE_DISABLE_ANIMATIONS = 1 << 2;
71+
private static final int ACCESSIBILITY_FEATURE_BOLD_TEXT = 1 << 3;
72+
private static final int ACCESSIBILITY_FEATURE_NO_ANNOUNCE = 1 << 7;
73+
6974
@Test
7075
public void itDescribesNonTextFieldsWithAContentDescription() {
7176
AccessibilityBridge accessibilityBridge = setUpBridge();
@@ -135,6 +140,26 @@ public void itTakesGlobalCoordinatesOfFlutterViewIntoAccount() {
135140
assertEquals(position, outBoundsInScreen.top);
136141
}
137142

143+
@Test
144+
public void itSetsNoAnnounceAccessibleFlagByDefault() {
145+
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
146+
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
147+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
148+
View mockRootView = mock(View.class);
149+
Context context = mock(Context.class);
150+
when(mockRootView.getContext()).thenReturn(context);
151+
when(context.getPackageName()).thenReturn("test");
152+
when(mockManager.isTouchExplorationEnabled()).thenReturn(false);
153+
setUpBridge(
154+
/*rootAccessibilityView=*/ mockRootView,
155+
/*accessibilityChannel=*/ mockChannel,
156+
/*accessibilityManager=*/ mockManager,
157+
/*contentResolver=*/ null,
158+
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
159+
/*platformViewsAccessibilityDelegate=*/ null);
160+
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
161+
}
162+
138163
@Test
139164
public void itSetsAccessibleNavigation() {
140165
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
@@ -158,18 +183,20 @@ public void itSetsAccessibleNavigation() {
158183
verify(mockManager).addTouchExplorationStateChangeListener(listenerCaptor.capture());
159184

160185
assertEquals(accessibilityBridge.getAccessibleNavigation(), false);
161-
verify(mockChannel).setAccessibilityFeatures(0);
186+
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
162187
reset(mockChannel);
163188

164189
// Simulate assistive technology accessing accessibility tree.
165190
accessibilityBridge.createAccessibilityNodeInfo(0);
166-
verify(mockChannel).setAccessibilityFeatures(1);
191+
verify(mockChannel)
192+
.setAccessibilityFeatures(
193+
ACCESSIBILITY_FEATURE_NAVIGATION | ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
167194
assertEquals(accessibilityBridge.getAccessibleNavigation(), true);
168195

169196
// Simulate turning off TalkBack.
170197
reset(mockChannel);
171198
listenerCaptor.getValue().onTouchExplorationStateChanged(false);
172-
verify(mockChannel).setAccessibilityFeatures(0);
199+
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
173200
assertEquals(accessibilityBridge.getAccessibleNavigation(), false);
174201
}
175202

@@ -1157,7 +1184,9 @@ public void itSetsBoldTextFlagCorrectly() {
11571184
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
11581185
/*platformViewsAccessibilityDelegate=*/ null);
11591186

1160-
verify(mockChannel).setAccessibilityFeatures(1 << 3);
1187+
verify(mockChannel)
1188+
.setAccessibilityFeatures(
1189+
ACCESSIBILITY_FEATURE_BOLD_TEXT | ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
11611190
reset(mockChannel);
11621191

11631192
// Now verify that clearing the BOLD_TEXT flag doesn't touch any of the other flags.
@@ -1179,7 +1208,9 @@ public void itSetsBoldTextFlagCorrectly() {
11791208
// constructor, verify that the latest argument is correct
11801209
ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);
11811210
verify(mockChannel, atLeastOnce()).setAccessibilityFeatures(captor.capture());
1182-
assertEquals(1 << 2 /* DISABLE_ANIMATION */, captor.getValue().intValue());
1211+
assertEquals(
1212+
ACCESSIBILITY_FEATURE_DISABLE_ANIMATIONS | ACCESSIBILITY_FEATURE_NO_ANNOUNCE,
1213+
captor.getValue().intValue());
11831214

11841215
// Set back to default
11851216
Settings.Global.putFloat(null, "transition_animation_scale", 1.0f);
@@ -1874,19 +1905,21 @@ public void testItSetsDisableAnimationsFlagBasedOnTransitionAnimationScale() {
18741905
ContentObserver observer = observerCaptor.getValue();
18751906

18761907
// Initial state
1877-
verify(mockChannel).setAccessibilityFeatures(0);
1908+
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
18781909
reset(mockChannel);
18791910

18801911
// Animations are disabled
18811912
Settings.Global.putFloat(mockContentResolver, "transition_animation_scale", 0.0f);
18821913
observer.onChange(false);
1883-
verify(mockChannel).setAccessibilityFeatures(1 << 2);
1914+
verify(mockChannel)
1915+
.setAccessibilityFeatures(
1916+
ACCESSIBILITY_FEATURE_DISABLE_ANIMATIONS | ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
18841917
reset(mockChannel);
18851918

18861919
// Animations are enabled
18871920
Settings.Global.putFloat(mockContentResolver, "transition_animation_scale", 1.0f);
18881921
observer.onChange(false);
1889-
verify(mockChannel).setAccessibilityFeatures(0);
1922+
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
18901923
}
18911924

18921925
@Test

engine/src/flutter/shell/platform/embedder/embedder.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ typedef enum {
105105
kFlutterAccessibilityFeatureHighContrast = 1 << 5,
106106
/// Request to show on/off labels inside switches.
107107
kFlutterAccessibilityFeatureOnOffSwitchLabels = 1 << 6,
108+
/// Indicate the platform does not support announcements.
109+
kFlutterAccessibilityFeatureNoAnnounce = 1 << 7,
108110
} FlutterAccessibilityFeature;
109111

110112
/// The set of possible actions that can be conveyed to a semantics node.

packages/flutter_test/lib/src/window.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures {
3232
this.reduceMotion = false,
3333
this.highContrast = false,
3434
this.onOffSwitchLabels = false,
35+
this.announce = false,
3536
});
3637

3738
/// An instance of [AccessibilityFeatures] where all the features are enabled.
@@ -43,6 +44,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures {
4344
reduceMotion: true,
4445
highContrast: true,
4546
onOffSwitchLabels: true,
47+
announce: true,
4648
);
4749

4850
@override
@@ -66,6 +68,9 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures {
6668
@override
6769
final bool onOffSwitchLabels;
6870

71+
@override
72+
final bool announce;
73+
6974
@override
7075
bool operator ==(Object other) {
7176
if (other.runtimeType != runtimeType) {
@@ -78,7 +83,8 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures {
7883
other.boldText == boldText &&
7984
other.reduceMotion == reduceMotion &&
8085
other.highContrast == highContrast &&
81-
other.onOffSwitchLabels == onOffSwitchLabels;
86+
other.onOffSwitchLabels == onOffSwitchLabels &&
87+
other.announce == announce;
8288
}
8389

8490
@override
@@ -91,6 +97,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures {
9197
reduceMotion,
9298
highContrast,
9399
onOffSwitchLabels,
100+
announce,
94101
);
95102
}
96103

0 commit comments

Comments
 (0)