Visitar URL original
[a11y][android] In Android 16, sendWindowContentChangeEvent when chec… · ahmedsameha1/flutter@90a237a · GitHub
Skip to content

Commit 90a237a

Browse files
hannah-hyjahmedsameha1
authored andcommitted
[a11y][android] In Android 16, sendWindowContentChangeEvent when check state changes (flutter#183606)
fix flutter#181321 this is a new AccessibilityEvent added in android 16 api 36: https://developer.android.com/reference/android/view/accessibility/AccessibilityEvent#CONTENT_CHANGE_TYPE_CHECKED So I tested this on checkbox and switch, it announces (checked, partially checked, not checked ) for status changes now, in android 15 it was announcing (checked, not checked ) for check box and announcing(on, off) for the switch widget. I think (checked, not checked ) is not ideal for switch widget but better than nothing. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [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 d462cc3 commit 90a237a

2 files changed

Lines changed: 155 additions & 8 deletions

File tree

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1914,7 +1914,7 @@ void updateSemantics(
19141914

19151915
// TODO(goderbauer): Send this event only once (!) for changed subtrees,
19161916
// see https://github.com/flutter/flutter/issues/14534
1917-
sendWindowContentChangeEvent(0);
1917+
sendWindowContentChangeEvent(0, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
19181918

19191919
for (SemanticsNode object : updated) {
19201920
if (object.didScroll()) {
@@ -1982,7 +1982,10 @@ void updateSemantics(
19821982
sendAccessibilityEvent(event);
19831983
}
19841984
if (object.hasFlag(Flag.IS_LIVE_REGION) && object.didChangeLabel()) {
1985-
sendWindowContentChangeEvent(object.id);
1985+
sendWindowContentChangeEvent(object.id, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
1986+
}
1987+
if (Build.VERSION.SDK_INT >= API_LEVELS.API_36 && object.didChangeCheckedState()) {
1988+
sendWindowContentChangeEvent(object.id, AccessibilityEvent.CONTENT_CHANGE_TYPE_CHECKED);
19861989
}
19871990
if (accessibilityFocusedSemanticsNode != null
19881991
&& accessibilityFocusedSemanticsNode.id == object.id
@@ -2150,16 +2153,13 @@ private void setAccessibilityPaneTitle(String title) {
21502153
* Creates a {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} and sends the event to
21512154
* Android's accessibility system.
21522155
*
2153-
* <p>It sets the content change types to {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE}
2154-
* when supported by the API level.
2155-
*
21562156
* <p>The given {@code virtualViewId} should be a {@link SemanticsNode} below which the content
21572157
* has changed.
21582158
*/
2159-
private void sendWindowContentChangeEvent(int virtualViewId) {
2159+
private void sendWindowContentChangeEvent(int virtualViewId, int changeType) {
21602160
AccessibilityEvent event =
21612161
obtainAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
2162-
event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
2162+
event.setContentChangeTypes(changeType);
21632163
sendAccessibilityEvent(event);
21642164
}
21652165

@@ -2283,7 +2283,7 @@ public void reset() {
22832283
}
22842284
accessibilityFocusedSemanticsNode = null;
22852285
hoveredObject = null;
2286-
sendWindowContentChangeEvent(0);
2286+
sendWindowContentChangeEvent(0, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
22872287
}
22882288

22892289
/**
@@ -2646,6 +2646,15 @@ private boolean didChangeLabel() {
26462646
return label == null || !label.equals(previousLabel);
26472647
}
26482648

2649+
private boolean didChangeCheckedState() {
2650+
if (!hadPreviousConfig) {
2651+
return false;
2652+
}
2653+
return (hasFlag(Flag.IS_CHECKED) != hadFlag(Flag.IS_CHECKED))
2654+
|| (hasFlag(Flag.IS_CHECK_STATE_MIXED) != hadFlag(Flag.IS_CHECK_STATE_MIXED))
2655+
|| (hasFlag(Flag.IS_TOGGLED) != hadFlag(Flag.IS_TOGGLED));
2656+
}
2657+
26492658
private void log(@NonNull String indent, boolean recursive) {
26502659
if (BuildConfig.DEBUG) {
26512660
Log.i(

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

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2626,6 +2626,144 @@ public void itCanPerformCollapse() {
26262626
verify(mockChannel).dispatchSemanticsAction(0, AccessibilityBridge.Action.COLLAPSE);
26272627
}
26282628

2629+
@Config(sdk = API_LEVELS.API_36)
2630+
@TargetApi(API_LEVELS.API_36)
2631+
@Test
2632+
public void itSendsContentChangeCheckedOnAPI36ForCheckbox() {
2633+
View mockRootView = mock(View.class);
2634+
Context mockContext = mock(Context.class);
2635+
when(mockRootView.getContext()).thenReturn(mockContext);
2636+
when(mockContext.getPackageName()).thenReturn("test");
2637+
ViewParent mockParent = mock(ViewParent.class);
2638+
when(mockRootView.getParent()).thenReturn(mockParent);
2639+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
2640+
when(mockManager.isEnabled()).thenReturn(true);
2641+
2642+
AccessibilityBridge accessibilityBridge = setUpBridge(mockRootView, mockManager, null);
2643+
2644+
TestSemanticsNode node = new TestSemanticsNode();
2645+
node.id = 0;
2646+
node.addFlag(AccessibilityBridge.Flag.HAS_CHECKED_STATE);
2647+
node.toUpdate().sendUpdateToBridge(accessibilityBridge);
2648+
2649+
// Change checked state: false -> true
2650+
node.addFlag(AccessibilityBridge.Flag.IS_CHECKED);
2651+
node.toUpdate().sendUpdateToBridge(accessibilityBridge);
2652+
2653+
// Change checked state: true -> false
2654+
node.flags &= ~AccessibilityBridge.Flag.IS_CHECKED.value;
2655+
node.toUpdate().sendUpdateToBridge(accessibilityBridge);
2656+
2657+
ArgumentCaptor<AccessibilityEvent> eventCaptor =
2658+
ArgumentCaptor.forClass(AccessibilityEvent.class);
2659+
verify(mockParent, atLeastOnce())
2660+
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
2661+
2662+
int foundCount = 0;
2663+
for (AccessibilityEvent event : eventCaptor.getAllValues()) {
2664+
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
2665+
&& event.getContentChangeTypes() == AccessibilityEvent.CONTENT_CHANGE_TYPE_CHECKED) {
2666+
foundCount++;
2667+
}
2668+
}
2669+
assertEquals(
2670+
"CONTENT_CHANGE_TYPE_CHECKED event should be sent twice for checkbox transitions",
2671+
2,
2672+
foundCount);
2673+
}
2674+
2675+
@Config(sdk = API_LEVELS.API_36)
2676+
@TargetApi(API_LEVELS.API_36)
2677+
@Test
2678+
public void itSendsContentChangeCheckedOnAPI36ForToggle() {
2679+
View mockRootView = mock(View.class);
2680+
Context mockContext = mock(Context.class);
2681+
when(mockRootView.getContext()).thenReturn(mockContext);
2682+
when(mockContext.getPackageName()).thenReturn("test");
2683+
ViewParent mockParent = mock(ViewParent.class);
2684+
when(mockRootView.getParent()).thenReturn(mockParent);
2685+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
2686+
when(mockManager.isEnabled()).thenReturn(true);
2687+
2688+
AccessibilityBridge accessibilityBridge = setUpBridge(mockRootView, mockManager, null);
2689+
2690+
TestSemanticsNode node = new TestSemanticsNode();
2691+
node.id = 0;
2692+
node.addFlag(AccessibilityBridge.Flag.HAS_TOGGLED_STATE);
2693+
node.toUpdate().sendUpdateToBridge(accessibilityBridge);
2694+
2695+
// Change toggle state: false -> true
2696+
node.addFlag(AccessibilityBridge.Flag.IS_TOGGLED);
2697+
node.toUpdate().sendUpdateToBridge(accessibilityBridge);
2698+
2699+
// Change toggle state: true -> false
2700+
node.flags &= ~AccessibilityBridge.Flag.IS_TOGGLED.value;
2701+
node.toUpdate().sendUpdateToBridge(accessibilityBridge);
2702+
2703+
ArgumentCaptor<AccessibilityEvent> eventCaptor =
2704+
ArgumentCaptor.forClass(AccessibilityEvent.class);
2705+
verify(mockParent, atLeastOnce())
2706+
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
2707+
2708+
int foundCount = 0;
2709+
for (AccessibilityEvent event : eventCaptor.getAllValues()) {
2710+
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
2711+
&& event.getContentChangeTypes() == AccessibilityEvent.CONTENT_CHANGE_TYPE_CHECKED) {
2712+
foundCount++;
2713+
}
2714+
}
2715+
assertEquals(
2716+
"CONTENT_CHANGE_TYPE_CHECKED event should be sent twice for toggle transitions",
2717+
2,
2718+
foundCount);
2719+
}
2720+
2721+
@Config(sdk = API_LEVELS.API_36)
2722+
@TargetApi(API_LEVELS.API_36)
2723+
@Test
2724+
public void itSendsContentChangeCheckedOnAPI36ForMixedState() {
2725+
View mockRootView = mock(View.class);
2726+
Context mockContext = mock(Context.class);
2727+
when(mockRootView.getContext()).thenReturn(mockContext);
2728+
when(mockContext.getPackageName()).thenReturn("test");
2729+
ViewParent mockParent = mock(ViewParent.class);
2730+
when(mockRootView.getParent()).thenReturn(mockParent);
2731+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
2732+
when(mockManager.isEnabled()).thenReturn(true);
2733+
2734+
AccessibilityBridge accessibilityBridge = setUpBridge(mockRootView, mockManager, null);
2735+
2736+
TestSemanticsNode node = new TestSemanticsNode();
2737+
node.id = 0;
2738+
node.addFlag(AccessibilityBridge.Flag.HAS_CHECKED_STATE);
2739+
node.toUpdate().sendUpdateToBridge(accessibilityBridge);
2740+
2741+
// Change to mixed state: false -> mixed
2742+
node.addFlag(AccessibilityBridge.Flag.IS_CHECK_STATE_MIXED);
2743+
node.toUpdate().sendUpdateToBridge(accessibilityBridge);
2744+
2745+
// Change back from mixed state: mixed -> false
2746+
node.flags &= ~AccessibilityBridge.Flag.IS_CHECK_STATE_MIXED.value;
2747+
node.toUpdate().sendUpdateToBridge(accessibilityBridge);
2748+
2749+
ArgumentCaptor<AccessibilityEvent> eventCaptor =
2750+
ArgumentCaptor.forClass(AccessibilityEvent.class);
2751+
verify(mockParent, atLeastOnce())
2752+
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
2753+
2754+
int foundCount = 0;
2755+
for (AccessibilityEvent event : eventCaptor.getAllValues()) {
2756+
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
2757+
&& event.getContentChangeTypes() == AccessibilityEvent.CONTENT_CHANGE_TYPE_CHECKED) {
2758+
foundCount++;
2759+
}
2760+
}
2761+
assertEquals(
2762+
"CONTENT_CHANGE_TYPE_CHECKED event should be sent twice for mixed state transitions",
2763+
2,
2764+
foundCount);
2765+
}
2766+
26292767
@Config(sdk = API_LEVELS.API_28)
26302768
@TargetApi(API_LEVELS.API_28)
26312769
@Test

0 commit comments

Comments
 (0)