Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9bc82d8
remove usages of semantic announcement in favor of liveRegion due to …
ash2moon Mar 19, 2025
8c566b0
remove usages of semantic announcement in favor of liveRegion due to …
ash2moon Mar 21, 2025
5b87306
Update semantic_utils.dart to have licence header
reidbaker Apr 17, 2025
a715fa2
refactor `SemanticsService.isAnnounceSupported` into DefaultSemantics…
ash2moon Apr 21, 2025
2c1d0d0
revert adding `SemanticsService.isAnnounceSupported`
ash2moon Apr 25, 2025
883834b
use noAnnounce for checking if announce is supported
ash2moon Apr 26, 2025
b375895
use noAnnounce for checking if announce is supported
ash2moon May 2, 2025
3d7a68e
flip noAnnounce to announce
ash2moon May 7, 2025
ac23161
flip noAnnounce to announce
ash2moon May 7, 2025
ea9d431
Merge branch 'master' of https://github.com/flutter/flutter into feat…
ash2moon May 7, 2025
20e7816
Merge branch 'master' of https://github.com/flutter/flutter into feat…
ash2moon May 8, 2025
93aada2
doc update
ash2moon May 8, 2025
53edff6
doc update
ash2moon May 9, 2025
87ca472
doc update
ash2moon May 9, 2025
49c5f65
Merge branch 'master' of https://github.com/flutter/flutter into feat…
ash2moon May 9, 2025
734324d
fix flaky test (MediaQueryData.fromView is sane) announce feature cha…
ash2moon May 9, 2025
7ad19b3
fix flaky test (MediaQueryData.fromView is sane) announce feature cha…
ash2moon May 9, 2025
fcb88cf
fix flaky test (MediaQueryData.fromView is sane) announce feature cha…
ash2moon May 9, 2025
698d7cc
fix flaky test (MediaQueryData.fromView is sane) announce feature cha…
ash2moon May 9, 2025
3b33025
fix flaky test (MediaQueryData.fromView is sane) announce feature cha…
ash2moon May 9, 2025
f430ced
fix flaky test (MediaQueryData.fromView is sane) announce feature cha…
ash2moon May 9, 2025
ddb43f3
Merge branch 'master' into feat/use-liveregion-form
ash2moon May 10, 2025
2db8719
fix flaky test (MediaQueryData.fromView is sane) announce feature cha…
ash2moon May 10, 2025
743c55d
fix flaky test (MediaQueryData.fromView is sane) announce feature cha…
ash2moon May 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions engine/src/flutter/lib/ui/window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,7 @@ class AccessibilityFeatures {
static const int _kReduceMotionIndex = 1 << 4;
static const int _kHighContrastIndex = 1 << 5;
static const int _kOnOffSwitchLabelsIndex = 1 << 6;
static const int _kNoAnnounceIndex = 1 << 7;

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

/// Whether accessibility announcements (like [SemanticsService.announce])
/// are allowed on the current platform.
///
/// Returns `false` on Android, where platform announcements are deprecated
/// by the underlying platform.
///
/// Returns `true` on all other platforms (iOS, web, desktop) where such
/// announcements are generally supported without discouragement.
///
/// Use this flag to conditionally avoid making announcements on Android.
// This is an inverted check on _index since there are many more platforms
// that support announce whereas don't.
bool get announce => _kNoAnnounceIndex & _index == 0;

@override
String toString() {
final List<String> features = <String>[];
Expand All @@ -992,6 +1007,9 @@ class AccessibilityFeatures {
if (onOffSwitchLabels) {
features.add('onOffSwitchLabels');
}
if (announce) {
features.add('announce');
}
return 'AccessibilityFeatures$features';
}

Expand Down
1 change: 1 addition & 0 deletions engine/src/flutter/lib/ui/window/platform_configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ enum class AccessibilityFeatureFlag : int32_t {
kReduceMotion = 1 << 4,
kHighContrast = 1 << 5,
kOnOffSwitchLabels = 1 << 6,
kNoAnnounce = 1 << 7,
};

//--------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
static const int _kReduceMotionIndex = 1 << 4;
static const int _kHighContrastIndex = 1 << 5;
static const int _kOnOffSwitchLabelsIndex = 1 << 6;
static const int _kNoAnnounceIndex = 1 << 7;

// A bitfield which represents each enabled feature.
final int _index;
Expand All @@ -72,6 +73,10 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
bool get highContrast => _kHighContrastIndex & _index != 0;
@override
bool get onOffSwitchLabels => _kOnOffSwitchLabelsIndex & _index != 0;
// This is an inverted check on _index since there are many more platforms
// that support announce whereas don't.
@override
bool get announce => _kNoAnnounceIndex & _index == 0;

@override
String toString() {
Expand All @@ -97,6 +102,9 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
if (onOffSwitchLabels) {
features.add('onOffSwitchLabels');
}
if (announce) {
features.add('announce');
}
return 'AccessibilityFeatures$features';
}

Expand All @@ -119,6 +127,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
bool? reduceMotion,
bool? highContrast,
bool? onOffSwitchLabels,
bool? announce,
}) {
final EngineAccessibilityFeaturesBuilder builder = EngineAccessibilityFeaturesBuilder(0);

Expand All @@ -129,6 +138,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
builder.reduceMotion = reduceMotion ?? this.reduceMotion;
builder.highContrast = highContrast ?? this.highContrast;
builder.onOffSwitchLabels = onOffSwitchLabels ?? this.onOffSwitchLabels;
builder.announce = announce ?? this.announce;

return builder.build();
}
Expand All @@ -146,6 +156,9 @@ class EngineAccessibilityFeaturesBuilder {
bool get reduceMotion => EngineAccessibilityFeatures._kReduceMotionIndex & _index != 0;
bool get highContrast => EngineAccessibilityFeatures._kHighContrastIndex & _index != 0;
bool get onOffSwitchLabels => EngineAccessibilityFeatures._kOnOffSwitchLabelsIndex & _index != 0;
// This is an inverted check on _index since there are many more platforms
// that support announce whereas don't.
bool get announce => EngineAccessibilityFeatures._kNoAnnounceIndex & _index == 0;

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

set announce(bool value) {
const int noAnnounce = EngineAccessibilityFeatures._kNoAnnounceIndex;
// Since we are using noAnnounce for the embedder, we need to flip the value.
_index = !value ? _index | noAnnounce : _index & ~noAnnounce;
}

/// Creates and returns an instance of EngineAccessibilityFeatures based on the value of _index
EngineAccessibilityFeatures build() {
return EngineAccessibilityFeatures(_index);
Expand Down
1 change: 1 addition & 0 deletions engine/src/flutter/lib/web_ui/lib/window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ abstract class AccessibilityFeatures {
bool get reduceMotion;
bool get highContrast;
bool get onOffSwitchLabels;
bool get announce;
}

enum Brightness { dark, light }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,14 @@ void _testEngineAccessibilityBuilder() {
expect(features.onOffSwitchLabels, isTrue);
});

test('announce', () {
// By default this starts off true, see EngineAccessibilityFeatures.announce
expect(features.announce, isTrue);
builder.announce = false;
features = builder.build();
expect(features.announce, isFalse);
});

test('reduce motion', () {
expect(features.reduceMotion, isFalse);
builder.reduceMotion = true;
Expand Down Expand Up @@ -391,14 +399,18 @@ void _testEngineSemanticsOwner() {
});

test('accessibilityFeatures copyWith function works', () {
const EngineAccessibilityFeatures original = EngineAccessibilityFeatures(0);
// Announce is an inverted check, see EngineAccessibilityFeatures.announce.
// Therefore, we need to ensure that the original copy starts with false (1 << 7).
const EngineAccessibilityFeatures original = EngineAccessibilityFeatures(0 | 1 << 7);

EngineAccessibilityFeatures copy = original.copyWith(accessibleNavigation: true);
expect(copy.accessibleNavigation, true);
expect(copy.boldText, false);
expect(copy.disableAnimations, false);
expect(copy.highContrast, false);
expect(copy.invertColors, false);
expect(copy.onOffSwitchLabels, false);
expect(copy.announce, false);
expect(copy.reduceMotion, false);

copy = original.copyWith(boldText: true);
Expand All @@ -417,6 +429,7 @@ void _testEngineSemanticsOwner() {
expect(copy.highContrast, false);
expect(copy.invertColors, false);
expect(copy.onOffSwitchLabels, false);
expect(copy.announce, false);
expect(copy.reduceMotion, false);

copy = original.copyWith(highContrast: true);
Expand All @@ -426,6 +439,7 @@ void _testEngineSemanticsOwner() {
expect(copy.highContrast, true);
expect(copy.invertColors, false);
expect(copy.onOffSwitchLabels, false);
expect(copy.announce, false);
expect(copy.reduceMotion, false);

copy = original.copyWith(invertColors: true);
Expand All @@ -435,6 +449,7 @@ void _testEngineSemanticsOwner() {
expect(copy.highContrast, false);
expect(copy.invertColors, true);
expect(copy.onOffSwitchLabels, false);
expect(copy.announce, false);
expect(copy.reduceMotion, false);

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

copy = original.copyWith(announce: true);
expect(copy.accessibleNavigation, false);
expect(copy.boldText, false);
expect(copy.disableAnimations, false);
expect(copy.highContrast, false);
expect(copy.invertColors, false);
expect(copy.onOffSwitchLabels, false);
expect(copy.announce, true);
expect(copy.reduceMotion, false);

copy = original.copyWith(reduceMotion: true);
expect(copy.accessibleNavigation, false);
expect(copy.boldText, false);
expect(copy.disableAnimations, false);
expect(copy.highContrast, false);
expect(copy.invertColors, false);
expect(copy.onOffSwitchLabels, false);
expect(copy.announce, false);
expect(copy.reduceMotion, true);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ public void onTouchExplorationStateChanged(boolean isTouchExplorationEnabled) {
this.accessibilityManager.addTouchExplorationStateChangeListener(
touchExplorationStateChangeListener);

accessibilityFeatureFlags |= AccessibilityFeature.NO_ANNOUNCE.value;
// Tell Flutter whether animations should initially be enabled or disabled. Then register a
// listener to be notified of changes in the future.
animationScaleObserver.onChange(false);
Expand Down Expand Up @@ -2170,7 +2171,8 @@ private enum AccessibilityFeature {
BOLD_TEXT(1 << 3), // NOT SUPPORTED
REDUCE_MOTION(1 << 4), // NOT SUPPORTED
HIGH_CONTRAST(1 << 5), // NOT SUPPORTED
ON_OFF_SWITCH_LABELS(1 << 6); // NOT SUPPORTED
ON_OFF_SWITCH_LABELS(1 << 6), // NOT SUPPORTED
NO_ANNOUNCE(1 << 7);

final int value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@
@RunWith(AndroidJUnit4.class)
public class AccessibilityBridgeTest {

private static final int ACCESSIBILITY_FEATURE_NAVIGATION = 1 << 0;
private static final int ACCESSIBILITY_FEATURE_DISABLE_ANIMATIONS = 1 << 2;
private static final int ACCESSIBILITY_FEATURE_BOLD_TEXT = 1 << 3;
private static final int ACCESSIBILITY_FEATURE_NO_ANNOUNCE = 1 << 7;

@Test
public void itDescribesNonTextFieldsWithAContentDescription() {
AccessibilityBridge accessibilityBridge = setUpBridge();
Expand Down Expand Up @@ -135,6 +140,26 @@ public void itTakesGlobalCoordinatesOfFlutterViewIntoAccount() {
assertEquals(position, outBoundsInScreen.top);
}

@Test
public void itSetsNoAnnounceAccessibleFlagByDefault() {
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
AccessibilityManager mockManager = mock(AccessibilityManager.class);
View mockRootView = mock(View.class);
Context context = mock(Context.class);
when(mockRootView.getContext()).thenReturn(context);
when(context.getPackageName()).thenReturn("test");
when(mockManager.isTouchExplorationEnabled()).thenReturn(false);
setUpBridge(
/*rootAccessibilityView=*/ mockRootView,
/*accessibilityChannel=*/ mockChannel,
/*accessibilityManager=*/ mockManager,
/*contentResolver=*/ null,
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
/*platformViewsAccessibilityDelegate=*/ null);
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
}

@Test
public void itSetsAccessibleNavigation() {
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
Expand All @@ -158,18 +183,20 @@ public void itSetsAccessibleNavigation() {
verify(mockManager).addTouchExplorationStateChangeListener(listenerCaptor.capture());

assertEquals(accessibilityBridge.getAccessibleNavigation(), false);
verify(mockChannel).setAccessibilityFeatures(0);
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
reset(mockChannel);

// Simulate assistive technology accessing accessibility tree.
accessibilityBridge.createAccessibilityNodeInfo(0);
verify(mockChannel).setAccessibilityFeatures(1);
verify(mockChannel)
.setAccessibilityFeatures(
ACCESSIBILITY_FEATURE_NAVIGATION | ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
assertEquals(accessibilityBridge.getAccessibleNavigation(), true);

// Simulate turning off TalkBack.
reset(mockChannel);
listenerCaptor.getValue().onTouchExplorationStateChanged(false);
verify(mockChannel).setAccessibilityFeatures(0);
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
assertEquals(accessibilityBridge.getAccessibleNavigation(), false);
}

Expand Down Expand Up @@ -1157,7 +1184,9 @@ public void itSetsBoldTextFlagCorrectly() {
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
/*platformViewsAccessibilityDelegate=*/ null);

verify(mockChannel).setAccessibilityFeatures(1 << 3);
verify(mockChannel)
.setAccessibilityFeatures(
ACCESSIBILITY_FEATURE_BOLD_TEXT | ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
reset(mockChannel);

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

// Set back to default
Settings.Global.putFloat(null, "transition_animation_scale", 1.0f);
Expand Down Expand Up @@ -1873,19 +1904,21 @@ public void testItSetsDisableAnimationsFlagBasedOnTransitionAnimationScale() {
ContentObserver observer = observerCaptor.getValue();

// Initial state
verify(mockChannel).setAccessibilityFeatures(0);
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
reset(mockChannel);

// Animations are disabled
Settings.Global.putFloat(mockContentResolver, "transition_animation_scale", 0.0f);
observer.onChange(false);
verify(mockChannel).setAccessibilityFeatures(1 << 2);
verify(mockChannel)
.setAccessibilityFeatures(
ACCESSIBILITY_FEATURE_DISABLE_ANIMATIONS | ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
reset(mockChannel);

// Animations are enabled
Settings.Global.putFloat(mockContentResolver, "transition_animation_scale", 1.0f);
observer.onChange(false);
verify(mockChannel).setAccessibilityFeatures(0);
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
}

@Test
Expand Down
2 changes: 2 additions & 0 deletions engine/src/flutter/shell/platform/embedder/embedder.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ typedef enum {
kFlutterAccessibilityFeatureHighContrast = 1 << 5,
/// Request to show on/off labels inside switches.
kFlutterAccessibilityFeatureOnOffSwitchLabels = 1 << 6,
/// Indicate the platform does not support announcements.
kFlutterAccessibilityFeatureNoAnnounce = 1 << 7,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

embedder API codeowners review: this is safe and will not break ABI compatibility.

} FlutterAccessibilityFeature;

/// The set of possible actions that can be conveyed to a semantics node.
Expand Down
Loading