From 07212c5d2ea99b85d64b393ac6e62edad5a34a15 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 30 Sep 2019 13:54:38 -0700 Subject: [PATCH 1/6] Add FOCUS action to focusable nodes --- .../platform/android/io/flutter/view/AccessibilityBridge.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 8303a9e11ec3e..da00add0de9e2 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -519,6 +519,9 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { result.setClassName("android.view.View"); result.setSource(rootAccessibilityView, virtualViewId); result.setFocusable(semanticsNode.isFocusable()); + if (semanticsNode.isFocusable() && !semanticsNode.hasFlag(Flag.IS_FOCUSED)) { + result.addAction(AccessibilityNodeInfo.ACTION_FOCUS); + } if (inputFocusedSemanticsNode != null) { result.setFocused(inputFocusedSemanticsNode.id == virtualViewId); } From 1ade84ea8ba9f1f916adf8489a48b95e0dfc454b Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 30 Sep 2019 14:43:31 -0700 Subject: [PATCH 2/6] Add VIEW_FOCUSED event --- .../android/io/flutter/view/AccessibilityBridge.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index da00add0de9e2..a99b146bac683 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -1380,6 +1380,12 @@ void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) { event.getText().add(object.label); sendAccessibilityEvent(event); } + + // If the object is the input-focused node, then tell the reader about it. + if (inputFocusedSemanticsNode != null && inputFocusedSemanticsNode.id == object.id) { + sendAccessibilityEvent(obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_FOCUSED)); + } + if (inputFocusedSemanticsNode != null && inputFocusedSemanticsNode.id == object.id && object.hadFlag(Flag.IS_TEXT_FIELD) && object.hasFlag(Flag.IS_TEXT_FIELD) // If we have a TextField that has InputFocus, we should avoid announcing it if something From 3940396d2fbd3322e423eb876c4429b06e1237f6 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 30 Sep 2019 17:27:02 -0700 Subject: [PATCH 3/6] Sorta works. Checkpoint. --- .../io/flutter/view/AccessibilityBridge.java | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index a99b146bac683..74bc1bb118ab7 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -516,7 +516,11 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { result.setViewIdResourceName(""); } result.setPackageName(rootAccessibilityView.getContext().getPackageName()); - result.setClassName("android.view.View"); + if (semanticsNode.hasFlag(Flag.HAS_IMPLICIT_SCROLLING)) { + result.setClassName("android.view.ListView"); + } else { + result.setClassName("android.view.View"); + } result.setSource(rootAccessibilityView, virtualViewId); result.setFocusable(semanticsNode.isFocusable()); if (semanticsNode.isFocusable() && !semanticsNode.hasFlag(Flag.IS_FOCUSED)) { @@ -682,6 +686,13 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { semanticsNode.scrollChildren, // columns false // hierarchical )); + // result.setCollectionItemInfo(AccessibilityNodeInfo.CollectionItemInfo.obtain( + // 0, + // 0, + // semanticsNode.scrollIndex, + // 1, + // false + // )); } else { result.setClassName("android.widget.HorizontalScrollView"); } @@ -692,6 +703,13 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { 0, // columns false // hierarchical )); + // result.setCollectionItemInfo(AccessibilityNodeInfo.CollectionItemInfo.obtain( + // semanticsNode.scrollIndex, + // 1, + // 0, + // 0, + // false + // )); } else { result.setClassName("android.widget.ScrollView"); } @@ -730,11 +748,17 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { result.setCheckable(hasCheckedState || hasToggledState); if (hasCheckedState) { result.setChecked(semanticsNode.hasFlag(Flag.IS_CHECKED)); - result.setContentDescription(semanticsNode.getValueLabelHint()); if (semanticsNode.hasFlag(Flag.IS_IN_MUTUALLY_EXCLUSIVE_GROUP)) { + result.setContentDescription(semanticsNode.getValueLabelHint()); result.setClassName("android.widget.RadioButton"); } else { - result.setClassName("android.widget.CheckBox"); + if (semanticsNode.label != null && !semanticsNode.label.isEmpty()) { + result.setText(semanticsNode.getValueLabelHint()); + result.setClassName("android.widget.CheckedTextView"); + } else { + result.setContentDescription(semanticsNode.getValueLabelHint()); + result.setClassName("android.widget.CheckBox"); + } } } else if (hasToggledState) { result.setChecked(semanticsNode.hasFlag(Flag.IS_TOGGLED)); @@ -1383,6 +1407,7 @@ void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) { // If the object is the input-focused node, then tell the reader about it. if (inputFocusedSemanticsNode != null && inputFocusedSemanticsNode.id == object.id) { + Log.e(TAG, "Sending VIEW_FOCUSED for " + inputFocusedSemanticsNode.id); sendAccessibilityEvent(obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_FOCUSED)); } @@ -2029,7 +2054,7 @@ private boolean isFocusable() { // We enforce in the framework that no other useful semantics are merged with these // nodes. if (hasFlag(Flag.SCOPES_ROUTE)) { - return false; + return true; } if (hasFlag(Flag.IS_FOCUSABLE)) { return true; From 54b847fa16fa0e5b096da483dc7cb6d803c1123b Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 1 Oct 2019 13:07:36 -0700 Subject: [PATCH 4/6] Checkpoint. Works. --- .../io/flutter/view/AccessibilityBridge.java | 35 ++++--------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 74bc1bb118ab7..50b1a7c8830b2 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -686,13 +686,6 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { semanticsNode.scrollChildren, // columns false // hierarchical )); - // result.setCollectionItemInfo(AccessibilityNodeInfo.CollectionItemInfo.obtain( - // 0, - // 0, - // semanticsNode.scrollIndex, - // 1, - // false - // )); } else { result.setClassName("android.widget.HorizontalScrollView"); } @@ -703,13 +696,6 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { 0, // columns false // hierarchical )); - // result.setCollectionItemInfo(AccessibilityNodeInfo.CollectionItemInfo.obtain( - // semanticsNode.scrollIndex, - // 1, - // 0, - // 0, - // false - // )); } else { result.setClassName("android.widget.ScrollView"); } @@ -767,7 +753,9 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { } else { // Setting the text directly instead of the content description // will replace the "checked" or "not-checked" label. - result.setText(semanticsNode.getValueLabelHint()); + if (!semanticsNode.hasFlag(Flag.SCOPES_ROUTE)) { + result.setText(semanticsNode.getValueLabelHint()); + } } result.setSelected(semanticsNode.hasFlag(Flag.IS_SELECTED)); @@ -910,11 +898,6 @@ public boolean performAction(int virtualViewId, int accessibilityAction, @Nullab virtualViewId, Action.DID_GAIN_ACCESSIBILITY_FOCUS ); - sendAccessibilityEvent( - virtualViewId, - AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED - ); - if (accessibilityFocusedSemanticsNode == null) { // When Android focuses a node, it doesn't invalidate the view. // (It does when it sends ACTION_CLEAR_ACCESSIBILITY_FOCUS, so @@ -922,6 +905,10 @@ public boolean performAction(int virtualViewId, int accessibilityAction, @Nullab rootAccessibilityView.invalidate(); } accessibilityFocusedSemanticsNode = semanticsNode; + sendAccessibilityEvent( + virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED + ); if (semanticsNode.hasAction(Action.INCREASE) || semanticsNode.hasAction(Action.DECREASE)) { // SeekBars only announce themselves after this event. @@ -2051,17 +2038,9 @@ private SemanticsNode hitTest(float[] point) { // TODO(goderbauer): This should be decided by the framework once we have more information // about focusability there. private boolean isFocusable() { - // We enforce in the framework that no other useful semantics are merged with these - // nodes. - if (hasFlag(Flag.SCOPES_ROUTE)) { - return true; - } if (hasFlag(Flag.IS_FOCUSABLE)) { return true; } - // If not explicitly set as focusable, then use our legacy - // algorithm. Once all focusable widgets have a Focus widget, then - // this won't be needed. int scrollableActions = Action.SCROLL_RIGHT.value | Action.SCROLL_LEFT.value | Action.SCROLL_UP.value | Action.SCROLL_DOWN.value; return (actions & ~scrollableActions) != 0 || flags != 0 From bc84b6f50da44b09935b1c6025b0b6815226d7e3 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 1 Oct 2019 14:33:29 -0700 Subject: [PATCH 5/6] Reverting unnecessary changes --- .../io/flutter/view/AccessibilityBridge.java | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 50b1a7c8830b2..2155f21cdbb3e 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -516,16 +516,9 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { result.setViewIdResourceName(""); } result.setPackageName(rootAccessibilityView.getContext().getPackageName()); - if (semanticsNode.hasFlag(Flag.HAS_IMPLICIT_SCROLLING)) { - result.setClassName("android.view.ListView"); - } else { - result.setClassName("android.view.View"); - } + result.setClassName("android.view.View"); result.setSource(rootAccessibilityView, virtualViewId); result.setFocusable(semanticsNode.isFocusable()); - if (semanticsNode.isFocusable() && !semanticsNode.hasFlag(Flag.IS_FOCUSED)) { - result.addAction(AccessibilityNodeInfo.ACTION_FOCUS); - } if (inputFocusedSemanticsNode != null) { result.setFocused(inputFocusedSemanticsNode.id == virtualViewId); } @@ -734,17 +727,11 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { result.setCheckable(hasCheckedState || hasToggledState); if (hasCheckedState) { result.setChecked(semanticsNode.hasFlag(Flag.IS_CHECKED)); + result.setContentDescription(semanticsNode.getValueLabelHint()); if (semanticsNode.hasFlag(Flag.IS_IN_MUTUALLY_EXCLUSIVE_GROUP)) { - result.setContentDescription(semanticsNode.getValueLabelHint()); result.setClassName("android.widget.RadioButton"); } else { - if (semanticsNode.label != null && !semanticsNode.label.isEmpty()) { - result.setText(semanticsNode.getValueLabelHint()); - result.setClassName("android.widget.CheckedTextView"); - } else { - result.setContentDescription(semanticsNode.getValueLabelHint()); - result.setClassName("android.widget.CheckBox"); - } + result.setClassName("android.widget.CheckBox"); } } else if (hasToggledState) { result.setChecked(semanticsNode.hasFlag(Flag.IS_TOGGLED)); @@ -898,6 +885,11 @@ public boolean performAction(int virtualViewId, int accessibilityAction, @Nullab virtualViewId, Action.DID_GAIN_ACCESSIBILITY_FOCUS ); + sendAccessibilityEvent( + virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED + ); + if (accessibilityFocusedSemanticsNode == null) { // When Android focuses a node, it doesn't invalidate the view. // (It does when it sends ACTION_CLEAR_ACCESSIBILITY_FOCUS, so @@ -905,10 +897,6 @@ public boolean performAction(int virtualViewId, int accessibilityAction, @Nullab rootAccessibilityView.invalidate(); } accessibilityFocusedSemanticsNode = semanticsNode; - sendAccessibilityEvent( - virtualViewId, - AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED - ); if (semanticsNode.hasAction(Action.INCREASE) || semanticsNode.hasAction(Action.DECREASE)) { // SeekBars only announce themselves after this event. @@ -1394,7 +1382,6 @@ void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) { // If the object is the input-focused node, then tell the reader about it. if (inputFocusedSemanticsNode != null && inputFocusedSemanticsNode.id == object.id) { - Log.e(TAG, "Sending VIEW_FOCUSED for " + inputFocusedSemanticsNode.id); sendAccessibilityEvent(obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_FOCUSED)); } @@ -2038,9 +2025,17 @@ private SemanticsNode hitTest(float[] point) { // TODO(goderbauer): This should be decided by the framework once we have more information // about focusability there. private boolean isFocusable() { + // We enforce in the framework that no other useful semantics are merged with these + // nodes. + if (hasFlag(Flag.SCOPES_ROUTE)) { + return false; + } if (hasFlag(Flag.IS_FOCUSABLE)) { return true; } + // If not explicitly set as focusable, then use our legacy + // algorithm. Once all focusable widgets have a Focus widget, then + // this won't be needed. int scrollableActions = Action.SCROLL_RIGHT.value | Action.SCROLL_LEFT.value | Action.SCROLL_UP.value | Action.SCROLL_DOWN.value; return (actions & ~scrollableActions) != 0 || flags != 0 From 15a3e22f5525af68b740e15eab1ec36b57aaa486 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 1 Oct 2019 16:59:59 -0700 Subject: [PATCH 6/6] Removing sending of VIEW_FOCUSED --- .../android/io/flutter/view/AccessibilityBridge.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 2155f21cdbb3e..c542da80f1f8a 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -737,12 +737,10 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { result.setChecked(semanticsNode.hasFlag(Flag.IS_TOGGLED)); result.setClassName("android.widget.Switch"); result.setContentDescription(semanticsNode.getValueLabelHint()); - } else { + } else if (!semanticsNode.hasFlag(Flag.SCOPES_ROUTE)) { // Setting the text directly instead of the content description // will replace the "checked" or "not-checked" label. - if (!semanticsNode.hasFlag(Flag.SCOPES_ROUTE)) { - result.setText(semanticsNode.getValueLabelHint()); - } + result.setText(semanticsNode.getValueLabelHint()); } result.setSelected(semanticsNode.hasFlag(Flag.IS_SELECTED)); @@ -1379,12 +1377,6 @@ void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) { event.getText().add(object.label); sendAccessibilityEvent(event); } - - // If the object is the input-focused node, then tell the reader about it. - if (inputFocusedSemanticsNode != null && inputFocusedSemanticsNode.id == object.id) { - sendAccessibilityEvent(obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_FOCUSED)); - } - if (inputFocusedSemanticsNode != null && inputFocusedSemanticsNode.id == object.id && object.hadFlag(Flag.IS_TEXT_FIELD) && object.hasFlag(Flag.IS_TEXT_FIELD) // If we have a TextField that has InputFocus, we should avoid announcing it if something