From eb006ca71c3a73b5826659decb9fda4b47dcc7c1 Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Tue, 12 Jun 2018 12:21:22 -0700 Subject: [PATCH 01/34] initial implementation of a live region for Android and addition of semantic flag --- lib/ui/semantics.dart | 16 ++++++++ lib/ui/semantics/semantics_node.h | 1 + .../io/flutter/view/AccessibilityBridge.java | 37 +++++++++++++++++-- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 7d00f6ac8e5fc..f362899a303ee 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -228,6 +228,7 @@ class SemanticsFlag { static const int _kScopesRouteIndex= 1 << 11; static const int _kNamesRouteIndex = 1 << 12; static const int _kIsHiddenIndex = 1 << 13; + static const int _kIsLiveRegionIndex = 1 << 14; const SemanticsFlag._(this.index); @@ -366,6 +367,18 @@ class SemanticsFlag { /// used to implement accessibility scrolling on iOS. static const SemanticsFlag isHidden = const SemanticsFlag._(_kIsHiddenIndex); + /// Whether the semantics node represents a region with important screen updates + /// + /// A [SnackBar] is an example of usecase for a live region. Since they are usually + /// triggered by a separate widget, they do not have accessibility focus when first + /// created. However, we would like to make users aware of it without interrupting + /// their workflow. On Android, the live region triggers an interruptable announcement + /// of the snackbar's text contents. + /// + /// Only widgets with important semantic information should be annotated with a live + /// region flag. + static const SemanticsFlag isLiveRegion = const SemanticsFlag._(_kIsLiveRegionIndex); + /// The possible semantics flags. /// /// The map's key is the [index] of the flag and the value is the flag itself. @@ -384,6 +397,7 @@ class SemanticsFlag { _kScopesRouteIndex: scopesRoute, _kNamesRouteIndex: namesRoute, _kIsHiddenIndex: isHidden, + _kIsLiveRegionIndex: isLiveRegion, }; @override @@ -417,6 +431,8 @@ class SemanticsFlag { return 'SemanticsFlag.namesRoute'; case _kIsHiddenIndex: return 'SemanticsFlag.isHidden'; + case _kIsLiveRegionIndex: + return 'SemanticsFlag.isLiveRegion'; } return null; } diff --git a/lib/ui/semantics/semantics_node.h b/lib/ui/semantics/semantics_node.h index 3ddb431e90dc8..328a621fe88be 100644 --- a/lib/ui/semantics/semantics_node.h +++ b/lib/ui/semantics/semantics_node.h @@ -59,6 +59,7 @@ enum class SemanticsFlags : int32_t { kScopesRoute = 1 << 11, kNamesRoute = 1 << 12, kIsHidden = 1 << 13, + kIsLiveRegion = 1 << 14, }; struct SemanticsNode { diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 322e113f628e0..54040852b079e 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -41,6 +41,7 @@ class AccessibilityBridge extends AccessibilityNodeProvider implements BasicMess private static final int ROOT_NODE_ID = 0; private Map mObjects; + private Map mLiveRegions; private final FlutterView mOwner; private boolean mAccessibilityEnabled = false; private SemanticsObject mA11yFocusedObject; @@ -91,7 +92,8 @@ enum Flag { IS_OBSCURED(1 << 10), SCOPES_ROUTE(1 << 11), NAMES_ROUTE(1 << 12), - IS_HIDDEN(1 << 13); + IS_HIDDEN(1 << 13), + IS_LIVE_REGION(1 << 14); Flag(int value) { this.value = value; @@ -104,6 +106,7 @@ enum Flag { assert owner != null; mOwner = owner; mObjects = new HashMap(); + mLiveRegions = new HashMap(); previousRoutes = new ArrayList<>(); mFlutterAccessibilityChannel = new BasicMessageChannel<>(owner, "flutter/accessibility", StandardMessageCodec.INSTANCE); @@ -238,7 +241,20 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); } } - + if (object.hasFlag(Flag.IS_LIVE_REGION)) { + result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); + String priorLabelHint = mLiveRegions.get(object.id); + String labelHint = object.getLabelHint(); + // It is very important that this is added to mLiveRegions before sending the accessibility event + // below. + mLiveRegions.put(object.id, labelHint); + if (priorLabelHint == null || !priorLabelHint.equals(labelHint)) { + sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } + } else if (object.hadFlag(Flag.IS_LIVE_REGION)) { + mLiveRegions.remove(object.id); + } + boolean hasCheckedState = object.hasFlag(Flag.HAS_CHECKED_STATE); result.setCheckable(hasCheckedState); if (hasCheckedState) { @@ -499,7 +515,7 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { rootObject.updateRecursively(identity, visitedObjects, false); rootObject.collectRoutes(newRoutes); } - + // Dispatch a TYPE_WINDOW_STATE_CHANGED event if the most recent route id changed from the // previously cached route id. SemanticsObject lastAdded = null; @@ -705,6 +721,7 @@ private void createWindowChangeEvent(SemanticsObject route) { private void willRemoveSemanticsObject(SemanticsObject object) { assert mObjects.containsKey(object.id); assert mObjects.get(object.id) == object; + mLiveRegions.remove(object.id); object.parent = null; if (mA11yFocusedObject == object) { sendAccessibilityEvent(mA11yFocusedObject.id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); @@ -1061,6 +1078,20 @@ private float max(float a, float b, float c, float d) { return Math.max(a, Math.max(b, Math.max(c, d))); } + private String getLabelHint() { + StringBuilder sb = new StringBuilder(); + if (label != null) { + sb.append(label); + } + if (sb.length() > 0) { + sb.append(", "); + } + if (hint != null) { + sb.append(hint); + } + return sb.length() > 0 ? sb.toString() : null; + } + private String getValueLabelHint() { StringBuilder sb = new StringBuilder(); String[] array = { value, label, hint }; From 5329a886c3618c6e6f7b20828c39ecf3ef6eec67 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Wed, 13 Jun 2018 12:51:40 -0700 Subject: [PATCH 02/34] Update semantics.dart --- lib/ui/semantics.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index f362899a303ee..15bc0e81d200e 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -367,9 +367,9 @@ class SemanticsFlag { /// used to implement accessibility scrolling on iOS. static const SemanticsFlag isHidden = const SemanticsFlag._(_kIsHiddenIndex); - /// Whether the semantics node represents a region with important screen updates + /// Whether the semantics node represents a region with important screen updates. /// - /// A [SnackBar] is an example of usecase for a live region. Since they are usually + /// A [SnackBar] is an example usecase for a live region. Since they are usually /// triggered by a separate widget, they do not have accessibility focus when first /// created. However, we would like to make users aware of it without interrupting /// their workflow. On Android, the live region triggers an interruptable announcement From 446621b7bbc6854dde577585a9c9f91d93ecad7e Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Sat, 23 Jun 2018 12:29:25 -0700 Subject: [PATCH 03/34] Add image flag for semantics --- lib/ui/semantics.dart | 7 +++++++ lib/ui/semantics/semantics_node.h | 1 + .../android/io/flutter/view/AccessibilityBridge.java | 6 +++++- .../darwin/ios/framework/Source/accessibility_bridge.mm | 3 +++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 7d00f6ac8e5fc..9c4772a73ecfe 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -228,6 +228,7 @@ class SemanticsFlag { static const int _kScopesRouteIndex= 1 << 11; static const int _kNamesRouteIndex = 1 << 12; static const int _kIsHiddenIndex = 1 << 13; + static const int _kIsImageIndex = 1 << 14; const SemanticsFlag._(this.index); @@ -366,6 +367,9 @@ class SemanticsFlag { /// used to implement accessibility scrolling on iOS. static const SemanticsFlag isHidden = const SemanticsFlag._(_kIsHiddenIndex); + /// Whether the semantics node represents an image. + static const SemanticsFlag isImage = const SemanticsFlag._(_kIsImageIndex); + /// The possible semantics flags. /// /// The map's key is the [index] of the flag and the value is the flag itself. @@ -384,6 +388,7 @@ class SemanticsFlag { _kScopesRouteIndex: scopesRoute, _kNamesRouteIndex: namesRoute, _kIsHiddenIndex: isHidden, + _kIsImageIndex: isImage, }; @override @@ -417,6 +422,8 @@ class SemanticsFlag { return 'SemanticsFlag.namesRoute'; case _kIsHiddenIndex: return 'SemanticsFlag.isHidden'; + case _kIsImageIndex: + return 'SemanticsFlag.isImage'; } return null; } diff --git a/lib/ui/semantics/semantics_node.h b/lib/ui/semantics/semantics_node.h index 3ddb431e90dc8..f80591d800f6c 100644 --- a/lib/ui/semantics/semantics_node.h +++ b/lib/ui/semantics/semantics_node.h @@ -59,6 +59,7 @@ enum class SemanticsFlags : int32_t { kScopesRoute = 1 << 11, kNamesRoute = 1 << 12, kIsHidden = 1 << 13, + kIsImage = 1 << 14, }; struct SemanticsNode { diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 322e113f628e0..21dc5c7454360 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -91,7 +91,8 @@ enum Flag { IS_OBSCURED(1 << 10), SCOPES_ROUTE(1 << 11), NAMES_ROUTE(1 << 12), - IS_HIDDEN(1 << 13); + IS_HIDDEN(1 << 13), + IS_IMAGE(1 << 14); Flag(int value) { this.value = value; @@ -182,6 +183,9 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { if (object.hasFlag(Flag.IS_BUTTON)) { result.setClassName("android.widget.Button"); } + if (object.hasFlag(Flag.IS_IMAGE)) { + result.setClassName("android.widget.ImageView"); + } if (object.parent != null) { assert object.id > ROOT_NODE_ID; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm index f923581e7f045..14a923c7e11fb 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm @@ -356,6 +356,9 @@ - (UIAccessibilityTraits)accessibilityTraits { if ([self node].HasFlag(blink::SemanticsFlags::kIsHeader)) { traits |= UIAccessibilityTraitHeader; } + if ([self node].HasFlag(blink::SemanticsFlags::kIsImage)) { + traits |= UIAccessibilityTraitImage; + } return traits; } From f53d5e3ecd492ea78aa98ba0f10daea398091a67 Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Wed, 27 Jun 2018 15:46:04 -0700 Subject: [PATCH 04/34] update doc comment --- lib/ui/semantics.dart | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index f362899a303ee..83030b1bef8d4 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -367,16 +367,19 @@ class SemanticsFlag { /// used to implement accessibility scrolling on iOS. static const SemanticsFlag isHidden = const SemanticsFlag._(_kIsHiddenIndex); - /// Whether the semantics node represents a region with important screen updates - /// - /// A [SnackBar] is an example of usecase for a live region. Since they are usually - /// triggered by a separate widget, they do not have accessibility focus when first - /// created. However, we would like to make users aware of it without interrupting - /// their workflow. On Android, the live region triggers an interruptable announcement - /// of the snackbar's text contents. - /// - /// Only widgets with important semantic information should be annotated with a live - /// region flag. + /// Whether the semantics node is a live region. + /// + /// A live region indicates that this semantics node will update its + /// semantics label. Platforms are free to use this information to + /// make polite updates to the user to inform them of this. + /// + /// On Android, TalkBack will make a polite announcement of the first and + /// subsequent updates to the label of this node. This flag is not currently + /// supported on iOS. + /// + /// An example of a live region is a [SnackBar] widget. When it appears + /// on the screen it may be difficult to focus to read the value. A live + /// region causes a polite announcement to be generated automatically. static const SemanticsFlag isLiveRegion = const SemanticsFlag._(_kIsLiveRegionIndex); /// The possible semantics flags. From fa7b778dd48a6c2a319d70763b1c792553f45835 Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Sun, 1 Jul 2018 11:26:30 -0700 Subject: [PATCH 05/34] add dismiss action --- lib/ui/semantics.dart | 16 ++++++++++++++++ lib/ui/semantics/semantics_node.h | 1 + .../io/flutter/view/AccessibilityBridge.java | 12 ++++++++++-- .../ios/framework/Source/accessibility_bridge.mm | 7 +++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 9c4772a73ecfe..44aacc1946f4e 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -26,6 +26,7 @@ class SemanticsAction { static const int _kPasteIndex = 1 << 14; static const int _kDidGainAccessibilityFocusIndex = 1 << 15; static const int _kDidLoseAccessibilityFocusIndex = 1 << 16; + static const int _kDismissIndex = 1 << 17; /// The numerical value for this action. /// @@ -146,6 +147,14 @@ class SemanticsAction { /// Accessibility focus and input focus can be held by two different nodes! static const SemanticsAction didLoseAccessibilityFocus = const SemanticsAction._(_kDidLoseAccessibilityFocusIndex); + /// A request that the node should be dismissed. + /// + /// A Snackbar, for example, may have a dismiss action to indicate to the user + /// that it can removed after it is no longer relevant. on Android, TalkBack + /// announces this after reading the label. On iOS, VoiceOver users can + /// perform a standard gesture to dismiss it. + static const SemanticsAction dismiss = const SemanticsAction._(_kDismissIndex); + /// The possible semantics actions. /// /// The map's key is the [index] of the action and the value is the action @@ -168,6 +177,7 @@ class SemanticsAction { _kPasteIndex: paste, _kDidGainAccessibilityFocusIndex: didGainAccessibilityFocus, _kDidLoseAccessibilityFocusIndex: didLoseAccessibilityFocus, + _kDismissIndex: dismiss, }; @override @@ -207,6 +217,8 @@ class SemanticsAction { return 'SemanticsAction.didGainAccessibilityFocus'; case _kDidLoseAccessibilityFocusIndex: return 'SemanticsAction.didLoseAccessibilityFocus'; + case _kDismissIndex: + return 'SemanticsAction.dismiss'; } return null; } @@ -368,6 +380,10 @@ class SemanticsFlag { static const SemanticsFlag isHidden = const SemanticsFlag._(_kIsHiddenIndex); /// Whether the semantics node represents an image. + /// + /// Platforms have special behavior for images. TalkBack will inform the user + /// the labeled node is an image. iOS may use the image flag to avoid + /// inverting their color when using smart invert. static const SemanticsFlag isImage = const SemanticsFlag._(_kIsImageIndex); /// The possible semantics flags. diff --git a/lib/ui/semantics/semantics_node.h b/lib/ui/semantics/semantics_node.h index f80591d800f6c..d8fd2986798e3 100644 --- a/lib/ui/semantics/semantics_node.h +++ b/lib/ui/semantics/semantics_node.h @@ -35,6 +35,7 @@ enum class SemanticsAction : int32_t { kPaste = 1 << 14, kDidGainAccessibilityFocus = 1 << 15, kDidLoseAccessibilityFocus = 1 << 16, + kDismiss = 1 << 18, }; const int kScrollableSemanticsActions = diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 21dc5c7454360..63fc6b2fa173f 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -68,7 +68,8 @@ enum Action { CUT(1 << 13), PASTE(1 << 14), DID_GAIN_ACCESSIBILITY_FOCUS(1 << 15), - DID_LOSE_ACCESSIBILITY_FOCUS(1 << 16); + DID_LOSE_ACCESSIBILITY_FOCUS(1 << 16), + DISMISS(1 << 17); Action(int value) { this.value = value; @@ -184,7 +185,11 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { result.setClassName("android.widget.Button"); } if (object.hasFlag(Flag.IS_IMAGE)) { - result.setClassName("android.widget.ImageView"); + result.setClassName("android.widget.ImageView"); + } + if (object.hasAction(Action.DISMISS)) { + result.setDismissable(true); + result.addAction(AccessibilityNodeInfo.ACTION_DISMISS); } if (object.parent != null) { @@ -394,6 +399,9 @@ public boolean performAction(int virtualViewId, int action, Bundle arguments) { mOwner.dispatchSemanticsAction(virtualViewId, Action.PASTE); return true; } + case AccessibilityNodeInfo.ACTION_DISMISS: { + mOwner.dispatchSemanticsAction(virtualViewId, Action.DISMISS); + } } return false; } diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm index 14a923c7e11fb..d51a94b2297ea 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm @@ -294,6 +294,13 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { return YES; } +- (BOOL)accessibilityPerformEscape { + if (![self node].HasAction(blink::SemanticsAction::kDismiss)) + return NO; + [self bridge] -> DispatchSemanticsAction([self uid], blink::SemanticsAction::kDismiss); + return YES; +} + #pragma mark UIAccessibilityFocus overrides - (void)accessibilityElementDidBecomeFocused { From cd9fa47e143e217256ecd6cf3d79f31b4462625a Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Mon, 2 Jul 2018 10:17:37 -0700 Subject: [PATCH 06/34] wip --- .../io/flutter/view/AccessibilityBridge.java | 27 ++++++++++++------- .../framework/Source/accessibility_bridge.h | 1 + .../framework/Source/accessibility_bridge.mm | 5 ++++ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 1fbff757a0054..d1b857dcf0b29 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -8,6 +8,7 @@ import android.opengl.Matrix; import android.os.Build; import android.os.Bundle; +import android.content.Context; import android.util.Log; import android.view.View; import android.view.accessibility.AccessibilityEvent; @@ -49,6 +50,7 @@ class AccessibilityBridge extends AccessibilityNodeProvider implements BasicMess private SemanticsObject mHoveredObject; private int previousRouteId = ROOT_NODE_ID; private List previousRoutes; + private String mPackageName; private final BasicMessageChannel mFlutterAccessibilityChannel; @@ -95,7 +97,7 @@ enum Flag { NAMES_ROUTE(1 << 12), IS_HIDDEN(1 << 13), IS_IMAGE(1 << 14), - IS_LIVE_REGION(1 << 115); + IS_LIVE_REGION(1 << 15); Flag(int value) { this.value = value; @@ -112,6 +114,7 @@ enum Flag { previousRoutes = new ArrayList<>(); mFlutterAccessibilityChannel = new BasicMessageChannel<>(owner, "flutter/accessibility", StandardMessageCodec.INSTANCE); + mPackageName = owner.getContext().getPackageName(); } void setAccessibilityEnabled(boolean accessibilityEnabled) { @@ -189,6 +192,9 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { } if (object.hasFlag(Flag.IS_IMAGE)) { result.setClassName("android.widget.ImageView"); + // conform to the expected id from TalkBack's CustomLabelManager. + // talkback/src/main/java/labeling/CustomLabelManager.java#L525 + result.setViewIdResourceName(mPackageName + ":id/" + Integer.toString(virtualViewId)); } if (object.hasAction(Action.DISMISS)) { result.setDismissable(true); @@ -242,6 +248,7 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { } } if (object.hasAction(Action.INCREASE) || object.hasAction(Action.DECREASE)) { + // TODO(jonahwilliams): support AccessibilityAction.ACTION_SET_PROGRESS once SDK is updated. result.setClassName("android.widget.SeekBar"); if (object.hasAction(Action.INCREASE)) { result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); @@ -252,14 +259,6 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { } if (object.hasFlag(Flag.IS_LIVE_REGION)) { result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); - String priorLabelHint = mLiveRegions.get(object.id); - String labelHint = object.getLabelHint(); - // It is very important that this is added to mLiveRegions before sending the accessibility event - // below. - mLiveRegions.put(object.id, labelHint); - if (priorLabelHint == null || !priorLabelHint.equals(labelHint)) { - sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - } } else if (object.hadFlag(Flag.IS_LIVE_REGION)) { mLiveRegions.remove(object.id); } @@ -527,7 +526,7 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { rootObject.updateRecursively(identity, visitedObjects, false); rootObject.collectRoutes(newRoutes); } - + // Dispatch a TYPE_WINDOW_STATE_CHANGED event if the most recent route id changed from the // previously cached route id. SemanticsObject lastAdded = null; @@ -626,6 +625,14 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { sendAccessibilityEvent(selectionEvent); } } + if (object.hasFlag(Flag.IS_LIVE_REGION)) { + String priorLabelHint = mLiveRegions.get(object.id); + String labelHint = object.getLabelHint(); + mLiveRegions.put(object.id, labelHint); + if (priorLabelHint == null || !priorLabelHint.equals(labelHint)) { + sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } + } } } diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h index ffe99c080324d..32f211cca7be9 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h @@ -120,6 +120,7 @@ class AccessibilityBridge final { UIView* view_; PlatformViewIOS* platform_view_; fml::scoped_nsobject> objects_; + fml::scoped_nsobject> live_regions_; fml::scoped_nsprotocol accessibility_channel_; fml::WeakPtrFactory weak_factory_; int32_t previous_route_id_; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm index d51a94b2297ea..83a034a2bab58 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm @@ -366,6 +366,9 @@ - (UIAccessibilityTraits)accessibilityTraits { if ([self node].HasFlag(blink::SemanticsFlags::kIsImage)) { traits |= UIAccessibilityTraitImage; } + if ([self node].HasFlag(blink::SemanticsFlags::kIsLiveRegion)) { + traits |= UIAccessibilityTraitUpdatesFrequently; + } return traits; } @@ -470,6 +473,7 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { : view_(view), platform_view_(platform_view), objects_([[NSMutableDictionary alloc] init]), + live_regions_([[NSMutableDictionary alloc] init]), weak_factory_(this), previous_route_id_(0), previous_routes_({}) { @@ -549,6 +553,7 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { if (root) VisitObjectsRecursivelyAndRemove(root, doomed_uids); [objects_ removeObjectsForKeys:doomed_uids]; + [live_regions_ removeObjectsForKeys:doomed_uids]; layoutChanged = layoutChanged || [doomed_uids count] > 0; From a82dd2f46eb275472da323d641283071c3521e9d Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Mon, 2 Jul 2018 10:19:52 -0700 Subject: [PATCH 07/34] Address comments --- lib/ui/semantics.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index da30ec9c12ef8..9c5f6dbb44d12 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -389,9 +389,9 @@ class SemanticsFlag { /// Whether the semantics node is a live region. /// - /// A live region indicates that this semantics node will update its - /// semantics label. Platforms are free to use this information to - /// make polite updates to the user to inform them of this. + /// A live region indicates that updates to semantics node are important. Platforms + /// are free to use this information to make polite updates to the user to inform + /// them of this. /// /// On Android, TalkBack will make a polite announcement of the first and /// subsequent updates to the label of this node. This flag is not currently From 38c62aedd5e07d4e6ff4f3a6455c117d70526ee0 Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Mon, 2 Jul 2018 10:27:51 -0700 Subject: [PATCH 08/34] remove unused live region code from ios --- .../platform/darwin/ios/framework/Source/accessibility_bridge.h | 1 - .../platform/darwin/ios/framework/Source/accessibility_bridge.mm | 1 - 2 files changed, 2 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h index 32f211cca7be9..ffe99c080324d 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h @@ -120,7 +120,6 @@ class AccessibilityBridge final { UIView* view_; PlatformViewIOS* platform_view_; fml::scoped_nsobject> objects_; - fml::scoped_nsobject> live_regions_; fml::scoped_nsprotocol accessibility_channel_; fml::WeakPtrFactory weak_factory_; int32_t previous_route_id_; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm index 83a034a2bab58..f47ded2b08661 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm @@ -473,7 +473,6 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { : view_(view), platform_view_(platform_view), objects_([[NSMutableDictionary alloc] init]), - live_regions_([[NSMutableDictionary alloc] init]), weak_factory_(this), previous_route_id_(0), previous_routes_({}) { From 91ea4f37764426147c6daed51c5de12b5ee9a10e Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Mon, 2 Jul 2018 10:32:36 -0700 Subject: [PATCH 09/34] remove for real --- .../platform/darwin/ios/framework/Source/accessibility_bridge.mm | 1 - 1 file changed, 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm index f47ded2b08661..be87d997f0ec0 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm @@ -552,7 +552,6 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { if (root) VisitObjectsRecursivelyAndRemove(root, doomed_uids); [objects_ removeObjectsForKeys:doomed_uids]; - [live_regions_ removeObjectsForKeys:doomed_uids]; layoutChanged = layoutChanged || [doomed_uids count] > 0; From fca700a644d8305df3ac5f7b70ce767d1ac8bbf0 Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Wed, 4 Jul 2018 15:46:54 -0700 Subject: [PATCH 10/34] Address review comments and fix compile error in xcode beta --- lib/ui/semantics.dart | 10 +++++----- lib/ui/semantics/semantics_node.h | 2 +- .../darwin/ios/framework/Source/FlutterAppDelegate.mm | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 9c5f6dbb44d12..23e0c9e809bf2 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -149,8 +149,8 @@ class SemanticsAction { /// A request that the node should be dismissed. /// - /// A Snackbar, for example, may have a dismiss action to indicate to the user - /// that it can removed after it is no longer relevant. on Android, TalkBack + /// A [Snackbar], for example, may have a dismiss action to indicate to the + /// user that it can removed after it is no longer relevant. On Android, TalkBack /// announces this after reading the label. On iOS, VoiceOver users can /// perform a standard gesture to dismiss it. static const SemanticsAction dismiss = const SemanticsAction._(_kDismissIndex); @@ -389,9 +389,9 @@ class SemanticsFlag { /// Whether the semantics node is a live region. /// - /// A live region indicates that updates to semantics node are important. Platforms - /// are free to use this information to make polite updates to the user to inform - /// them of this. + /// A live region indicates that updates to semantics node are important. + /// Platforms may use this information to make polite updates to the user to + /// inform them of this. /// /// On Android, TalkBack will make a polite announcement of the first and /// subsequent updates to the label of this node. This flag is not currently diff --git a/lib/ui/semantics/semantics_node.h b/lib/ui/semantics/semantics_node.h index d8fedf4f8c592..467fb726e946a 100644 --- a/lib/ui/semantics/semantics_node.h +++ b/lib/ui/semantics/semantics_node.h @@ -35,7 +35,7 @@ enum class SemanticsAction : int32_t { kPaste = 1 << 14, kDidGainAccessibilityFocus = 1 << 15, kDidLoseAccessibilityFocus = 1 << 16, - kDismiss = 1 << 18, + kDismiss = 1 << 17, }; const int kScrollableSemanticsActions = diff --git a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm index a544d516c8157..efdabb7f6b2d4 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm @@ -129,7 +129,7 @@ - (void)application:(UIApplication*)application - (BOOL)application:(UIApplication*)application continueUserActivity:(NSUserActivity*)userActivity - restorationHandler:(void (^)(NSArray*))restorationHandler { + restorationHandler:(void (^)(NSArray>*))restorationHandler { return [_lifeCycleDelegate application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; From e5cec1ce26d50ef88546d610044132307d0e6c6f Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Tue, 10 Jul 2018 11:01:09 -0700 Subject: [PATCH 11/34] Update FlutterAppDelegate.mm --- .../platform/darwin/ios/framework/Source/FlutterAppDelegate.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm index efdabb7f6b2d4..a544d516c8157 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm @@ -129,7 +129,7 @@ - (void)application:(UIApplication*)application - (BOOL)application:(UIApplication*)application continueUserActivity:(NSUserActivity*)userActivity - restorationHandler:(void (^)(NSArray>*))restorationHandler { + restorationHandler:(void (^)(NSArray*))restorationHandler { return [_lifeCycleDelegate application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; From 61e4617747239ef0a3d08a08dd92130096b29032 Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Tue, 10 Jul 2018 13:32:08 -0700 Subject: [PATCH 12/34] add toggleable flags --- lib/ui/semantics.dart | 19 +++++++++++++++++++ lib/ui/semantics/semantics_node.h | 2 ++ .../io/flutter/view/AccessibilityBridge.java | 11 ++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 23e0c9e809bf2..0fc411644f3ae 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -242,6 +242,8 @@ class SemanticsFlag { static const int _kIsHiddenIndex = 1 << 13; static const int _kIsImageIndex = 1 << 14; static const int _kIsLiveRegionIndex = 1 << 15; + static const int _kHasToggledStateIndex = 1 << 16; + static const int _kIsToggled = 1 << 17; const SemanticsFlag._(this.index); @@ -402,6 +404,17 @@ class SemanticsFlag { /// region causes a polite announcement to be generated automatically. static const SemanticsFlag isLiveRegion = const SemanticsFlag._(_kIsLiveRegionIndex); + /// The semantics node has the quality of either being "on" or "off". + /// + /// For example, a switch has toggled state. + static const SemanticsFlag hasToggledState = const SemanticsFlag._(_kHasToggledStateIndex); + + /// If true, the semantics node is "on". If false, the semantics node is + /// "off". + /// + /// For example, if a switch is in the on position, [isToggled] is true. + static const SemanticsFlag isToggled = const SemanticsFlag._(_kIsToggled); + /// The possible semantics flags. /// /// The map's key is the [index] of the flag and the value is the flag itself. @@ -422,6 +435,8 @@ class SemanticsFlag { _kIsHiddenIndex: isHidden, _kIsImageIndex: isImage, _kIsLiveRegionIndex: isLiveRegion, + _kHasToggledStateIndex: isToggled, + _kIsToggled: isToggled, }; @override @@ -459,6 +474,10 @@ class SemanticsFlag { return 'SemanticsFlag.isImage'; case _kIsLiveRegionIndex: return 'SemanticsFlag.isLiveRegion'; + case _kHasToggledStateIndex: + return 'SemanticsFlag.hasToggledState'; + case _kIsToggled: + return 'SemanticsFlag.isToggled'; } return null; } diff --git a/lib/ui/semantics/semantics_node.h b/lib/ui/semantics/semantics_node.h index 467fb726e946a..ba1c111cbe477 100644 --- a/lib/ui/semantics/semantics_node.h +++ b/lib/ui/semantics/semantics_node.h @@ -62,6 +62,8 @@ enum class SemanticsFlags : int32_t { kIsHidden = 1 << 13, kIsImage = 1 << 14, kIsLiveRegion = 1 << 15, + kHasToggledState = 1 << 16, + kIsToggled = 1 << 17, }; struct SemanticsNode { diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 04758779ec007..cd43f0eea5b5f 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -88,7 +88,9 @@ enum Flag { NAMES_ROUTE(1 << 12), IS_HIDDEN(1 << 13), IS_IMAGE(1 << 14), - IS_LIVE_REGION(1 << 15); + IS_LIVE_REGION(1 << 15), + HAS_TOGGLED_STATE(1 << 16), + IS_TOGGLED(1 << 17); Flag(int value) { this.value = value; @@ -264,6 +266,13 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { result.setClassName("android.widget.CheckBox"); } + // boolean hasToggledState = object.hasFlag(Flag.HAS_TOGGLED_STATE); + // result.setCheckable(hasToggledState); + // if (hasToggledState) { + // result.setChecked(object.hasFlag(Flag.IS_TOGGLED)); + // result.setClassName("android.widget.Switch"); + // } + result.setSelected(object.hasFlag(Flag.IS_SELECTED)); result.setText(object.getValueLabelHint()); From 7e060d24056be455dbe97a63d2f4ad24219a6a17 Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Tue, 10 Jul 2018 15:41:06 -0700 Subject: [PATCH 13/34] Add toggle semantics flags and support in android, mapping to checked for iOS --- .../io/flutter/view/AccessibilityBridge.java | 14 ++++++-------- .../ios/framework/Source/accessibility_bridge.mm | 1 + 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index cd43f0eea5b5f..200f8d9dffd9e 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -257,22 +257,20 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { } boolean hasCheckedState = object.hasFlag(Flag.HAS_CHECKED_STATE); - result.setCheckable(hasCheckedState); + boolean hasToggledState = object.hasFlag(Flag.HAS_TOGGLED_STATE); + assert !(hasCheckedState && hasToggledState); + result.setCheckable(hasCheckedState || hasToggledState); if (hasCheckedState) { result.setChecked(object.hasFlag(Flag.IS_CHECKED)); if (object.hasFlag(Flag.IS_IN_MUTUALLY_EXCLUSIVE_GROUP)) result.setClassName("android.widget.RadioButton"); else result.setClassName("android.widget.CheckBox"); + } else if (hasToggledState) { + result.setChecked(object.hasFlag(Flag.IS_TOGGLED)); + result.setClassName("android.widget.Switch"); } - // boolean hasToggledState = object.hasFlag(Flag.HAS_TOGGLED_STATE); - // result.setCheckable(hasToggledState); - // if (hasToggledState) { - // result.setChecked(object.hasFlag(Flag.IS_TOGGLED)); - // result.setClassName("android.widget.Switch"); - // } - result.setSelected(object.hasFlag(Flag.IS_SELECTED)); result.setText(object.getValueLabelHint()); diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm index be87d997f0ec0..39b2f26d48415 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm @@ -350,6 +350,7 @@ - (UIAccessibilityTraits)accessibilityTraits { traits |= UIAccessibilityTraitAdjustable; } if ([self node].HasFlag(blink::SemanticsFlags::kIsSelected) || + [self node].HasFlag(blink::SemanticsFlags::kIsToggled) || [self node].HasFlag(blink::SemanticsFlags::kIsChecked)) { traits |= UIAccessibilityTraitSelected; } From 37380f73d8d48b93ca6ecabcfd63e91dd5ade21b Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Tue, 10 Jul 2018 15:42:43 -0700 Subject: [PATCH 14/34] fix typos in semantics --- lib/ui/semantics.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 0fc411644f3ae..0d1f7b7c3b225 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -243,7 +243,7 @@ class SemanticsFlag { static const int _kIsImageIndex = 1 << 14; static const int _kIsLiveRegionIndex = 1 << 15; static const int _kHasToggledStateIndex = 1 << 16; - static const int _kIsToggled = 1 << 17; + static const int _kIsToggledIndex = 1 << 17; const SemanticsFlag._(this.index); @@ -413,7 +413,7 @@ class SemanticsFlag { /// "off". /// /// For example, if a switch is in the on position, [isToggled] is true. - static const SemanticsFlag isToggled = const SemanticsFlag._(_kIsToggled); + static const SemanticsFlag isToggled = const SemanticsFlag._(_kIsToggledIndex); /// The possible semantics flags. /// @@ -435,8 +435,8 @@ class SemanticsFlag { _kIsHiddenIndex: isHidden, _kIsImageIndex: isImage, _kIsLiveRegionIndex: isLiveRegion, - _kHasToggledStateIndex: isToggled, - _kIsToggled: isToggled, + _kHasToggledStateIndex: hasToggledState, + _kIsToggledIndex: isToggled, }; @override @@ -476,7 +476,7 @@ class SemanticsFlag { return 'SemanticsFlag.isLiveRegion'; case _kHasToggledStateIndex: return 'SemanticsFlag.hasToggledState'; - case _kIsToggled: + case _kIsToggledIndex: return 'SemanticsFlag.isToggled'; } return null; From e21a2aec2b1f3fd78c0fa2ec5a0e70ca94963260 Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Tue, 10 Jul 2018 17:29:46 -0700 Subject: [PATCH 15/34] move live region experiments --- .../android/io/flutter/view/AccessibilityBridge.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 200f8d9dffd9e..f960f9b5dfbd1 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -627,8 +627,12 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { String priorLabelHint = mLiveRegions.get(object.id); String labelHint = object.getLabelHint(); mLiveRegions.put(object.id, labelHint); - if (priorLabelHint == null || !priorLabelHint.equals(labelHint)) { - sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + // Always send initial content changed event, but only notify about subsequent events + // if the node is not focused. This allows to release any framework side throttling on + // semantic node updates so that the user can always read the most up to date value + // without being overwhelmed. + if (priorLabelHint == null || (mA11yFocusedObject != object && !priorLabelHint.equals(labelHint))) { + sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } } } From d7bc1f6b40d6f1fd29fcdb3f30f5c6452b489ba7 Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Tue, 10 Jul 2018 19:04:13 -0700 Subject: [PATCH 16/34] remove automatic live region update --- .../io/flutter/view/AccessibilityBridge.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index f960f9b5dfbd1..2b1eb92182fdd 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -627,11 +627,7 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { String priorLabelHint = mLiveRegions.get(object.id); String labelHint = object.getLabelHint(); mLiveRegions.put(object.id, labelHint); - // Always send initial content changed event, but only notify about subsequent events - // if the node is not focused. This allows to release any framework side throttling on - // semantic node updates so that the user can always read the most up to date value - // without being overwhelmed. - if (priorLabelHint == null || (mA11yFocusedObject != object && !priorLabelHint.equals(labelHint))) { + if (priorLabelHint == null) { sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } } @@ -729,6 +725,16 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { e.getText().add((String) data.get("message")); sendAccessibilityEvent(e); } + case "liveRegionUpdate": { + Integer nodeId = (Integer) annotatedEvent.get("nodeId"); + if (nodeId == null) { + return; + } + if (!mLiveRegions.containsKey(nodeId)) { + return; + } + sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } } } From 4cc928a5ada09a4a87f771258b07b099d55d0732 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Wed, 11 Jul 2018 10:36:13 -0700 Subject: [PATCH 17/34] clean up comments --- lib/ui/semantics.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 740aad4f574ee..6d779ca1b8bc6 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -150,9 +150,9 @@ class SemanticsAction { /// A request that the node should be dismissed. /// /// A [Snackbar], for example, may have a dismiss action to indicate to the - /// user that it can removed after it is no longer relevant. On Android, TalkBack - /// announces this after reading the label. On iOS, VoiceOver users can - /// perform a standard gesture to dismiss it. + /// user that it can be removed after it is no longer relevant. On Android, + /// TalkBack announces this after reading the label. On iOS, VoiceOver users + /// can perform a standard gesture to dismiss it. static const SemanticsAction dismiss = const SemanticsAction._(_kDismissIndex); /// The possible semantics actions. @@ -384,9 +384,10 @@ class SemanticsFlag { /// Whether the semantics node represents an image. /// - /// Platforms have special behavior for images. TalkBack will inform the user - /// the labeled node is an image. iOS may use the image flag to avoid - /// inverting their color when using smart invert. + /// Platforms have special behavior for images. Both TalkBack and VoiceOver + /// will inform the user the the semantics node represents an image. Talkback + /// also allows users to provide their own labels to images that have not + /// been labeled. static const SemanticsFlag isImage = const SemanticsFlag._(_kIsImageIndex); /// Whether the semantics node is a live region. From b75b257ebd9fc33983f7ecef76b5cb722a52f7e3 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Wed, 11 Jul 2018 10:53:03 -0700 Subject: [PATCH 18/34] address some comments --- lib/ui/semantics.dart | 6 ++---- .../android/io/flutter/view/AccessibilityBridge.java | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 3a72e90de34af..ab844bd7b0be5 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -394,10 +394,8 @@ class SemanticsFlag { /// Whether the semantics node represents an image. /// - /// Platforms have special behavior for images. Both TalkBack and VoiceOver - /// will inform the user the the semantics node represents an image. Talkback - /// also allows users to provide their own labels to images that have not - /// been labeled. + /// Both TalkBack and VoiceOver will inform the user the the semantics node + /// represents an image. static const SemanticsFlag isImage = const SemanticsFlag._(_kIsImageIndex); /// Whether the semantics node is a live region. diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 529292e668793..5c6bf025f1504 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -188,9 +188,8 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { } if (object.hasFlag(Flag.IS_IMAGE)) { result.setClassName("android.widget.ImageView"); - // conform to the expected id from TalkBack's CustomLabelManager. + // TODO(jonahwilliams): Figure out a way conform to the expected id from TalkBack's CustomLabelManager. // talkback/src/main/java/labeling/CustomLabelManager.java#L525 - result.setViewIdResourceName(mPackageName + ":id/" + Integer.toString(virtualViewId)); } if (object.hasAction(Action.DISMISS)) { result.setDismissable(true); From 8114b031f526a693d68b49109346b4ef3fd9b7de Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Wed, 11 Jul 2018 14:00:25 -0700 Subject: [PATCH 19/34] remove initial live region update, all updates handled by framework --- lib/ui/semantics.dart | 8 ++--- .../io/flutter/view/AccessibilityBridge.java | 31 ++----------------- 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index ab844bd7b0be5..f715a17ddd09a 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -401,12 +401,8 @@ class SemanticsFlag { /// Whether the semantics node is a live region. /// /// A live region indicates that updates to semantics node are important. - /// Platforms may use this information to make polite updates to the user to - /// inform them of this. - /// - /// On Android, TalkBack will make a polite announcement of the first and - /// subsequent updates to the label of this node. This flag is not currently - /// supported on iOS. + /// Platforms may use this information to make polite announcements to the + /// user to inform them of updates to this node. /// /// An example of a live region is a [SnackBar] widget. When it appears /// on the screen it may be difficult to focus to read the value. A live diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 5c6bf025f1504..a2c0fc41d01f7 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -33,7 +33,6 @@ class AccessibilityBridge extends AccessibilityNodeProvider implements BasicMess private static final int ROOT_NODE_ID = 0; private Map mObjects; - private Map mLiveRegions; private Map mCustomAccessibilityActions; private final FlutterView mOwner; private boolean mAccessibilityEnabled = false; @@ -105,7 +104,6 @@ enum Flag { assert owner != null; mOwner = owner; mObjects = new HashMap(); - mLiveRegions = new HashMap(); mCustomAccessibilityActions = new HashMap(); previousRoutes = new ArrayList<>(); mFlutterAccessibilityChannel = new BasicMessageChannel<>(owner, "flutter/accessibility", @@ -254,8 +252,6 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { } if (object.hasFlag(Flag.IS_LIVE_REGION)) { result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); - } else if (object.hadFlag(Flag.IS_LIVE_REGION)) { - mLiveRegions.remove(object.id); } boolean hasCheckedState = object.hasFlag(Flag.HAS_CHECKED_STATE); @@ -665,12 +661,7 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { } } if (object.hasFlag(Flag.IS_LIVE_REGION)) { - String priorLabelHint = mLiveRegions.get(object.id); - String labelHint = object.getLabelHint(); - mLiveRegions.put(object.id, labelHint); - if (priorLabelHint == null) { - sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - } + sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } } } @@ -771,10 +762,7 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { if (nodeId == null) { return; } - if (!mLiveRegions.containsKey(nodeId)) { - return; - } - sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } } } @@ -789,7 +777,6 @@ private void createWindowChangeEvent(SemanticsObject route) { private void willRemoveSemanticsObject(SemanticsObject object) { assert mObjects.containsKey(object.id); assert mObjects.get(object.id) == object; - mLiveRegions.remove(object.id); object.parent = null; if (mA11yFocusedObject == object) { sendAccessibilityEvent(mA11yFocusedObject.id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); @@ -1175,20 +1162,6 @@ private float max(float a, float b, float c, float d) { return Math.max(a, Math.max(b, Math.max(c, d))); } - private String getLabelHint() { - StringBuilder sb = new StringBuilder(); - if (label != null) { - sb.append(label); - } - if (sb.length() > 0) { - sb.append(", "); - } - if (hint != null) { - sb.append(hint); - } - return sb.length() > 0 ? sb.toString() : null; - } - private String getValueLabelHint() { StringBuilder sb = new StringBuilder(); String[] array = { value, label, hint }; From 9169bf205428d503490dbca00a122c1ee562a363 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Wed, 11 Jul 2018 15:16:49 -0700 Subject: [PATCH 20/34] WIP --- .../android/io/flutter/view/AccessibilityBridge.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index a2c0fc41d01f7..4de04e85fe005 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -34,6 +34,8 @@ class AccessibilityBridge extends AccessibilityNodeProvider implements BasicMess private Map mObjects; private Map mCustomAccessibilityActions; + + private Set mLiveRegions; private final FlutterView mOwner; private boolean mAccessibilityEnabled = false; private SemanticsObject mA11yFocusedObject; @@ -104,6 +106,7 @@ enum Flag { assert owner != null; mOwner = owner; mObjects = new HashMap(); + mLiveRegions = new HashSet<>(); mCustomAccessibilityActions = new HashMap(); previousRoutes = new ArrayList<>(); mFlutterAccessibilityChannel = new BasicMessageChannel<>(owner, "flutter/accessibility", @@ -661,6 +664,7 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { } } if (object.hasFlag(Flag.IS_LIVE_REGION)) { + mLiveRegions.add(object.id); sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } } @@ -757,11 +761,14 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { e.getText().add((String) data.get("message")); sendAccessibilityEvent(e); } - case "liveRegionUpdate": { + case "updateLiveRegion": { Integer nodeId = (Integer) annotatedEvent.get("nodeId"); if (nodeId == null) { return; } + if (!mLiveRegions.contains(nodeId)) { + return; + } sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } } @@ -777,6 +784,7 @@ private void createWindowChangeEvent(SemanticsObject route) { private void willRemoveSemanticsObject(SemanticsObject object) { assert mObjects.containsKey(object.id); assert mObjects.get(object.id) == object; + mLiveRegions.remove(object.id); object.parent = null; if (mA11yFocusedObject == object) { sendAccessibilityEvent(mA11yFocusedObject.id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); From 2ed58bdce80cc786389916a6c0fd6e796d6e9cc6 Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Thu, 12 Jul 2018 13:47:38 -0700 Subject: [PATCH 21/34] never mind they work for real --- .../io/flutter/view/AccessibilityBridge.java | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index a2c0fc41d01f7..d1ac57784b069 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -34,6 +34,9 @@ class AccessibilityBridge extends AccessibilityNodeProvider implements BasicMess private Map mObjects; private Map mCustomAccessibilityActions; + /// Creating a live region through the async semantics bridge requires some compromises in the design. + /// For starters, Talkback will not know about the + private Map mLiveRegions; private final FlutterView mOwner; private boolean mAccessibilityEnabled = false; private SemanticsObject mA11yFocusedObject; @@ -100,10 +103,27 @@ enum Flag { final int value; } + enum LiveRegionState { + // A live region has been created but the correspond AccessibilityNodeInfo has not + // been created yet. + CREATED(1 << 0), + // A live region has been marked dirty and an update should be emitted. + DIRTY(1 << 1), + // A live region has no updates. + CLEAN(1 << 2); + + LiveRegionState(int value) { + this.value = value; + } + + final int value; + } + AccessibilityBridge(FlutterView owner) { assert owner != null; mOwner = owner; mObjects = new HashMap(); + mLiveRegions = new HashMap<>(); mCustomAccessibilityActions = new HashMap(); previousRoutes = new ArrayList<>(); mFlutterAccessibilityChannel = new BasicMessageChannel<>(owner, "flutter/accessibility", @@ -252,6 +272,10 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { } if (object.hasFlag(Flag.IS_LIVE_REGION)) { result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); + LiveRegionState state = mLiveRegions.get(object.id); + if (state == LiveRegionState.CREATED) { + mLiveRegions.put(object.id, LiveRegionState.DIRTY); + } } boolean hasCheckedState = object.hasFlag(Flag.HAS_CHECKED_STATE); @@ -632,6 +656,26 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { } sendAccessibilityEvent(event); } + if (object.hasFlag(Flag.IS_LIVE_REGION)) { + if (!mLiveRegions.containsKey(object.id)) { + mLiveRegions.put(object.id, LiveRegionState.CREATED); + } + LiveRegionState state = mLiveRegions.get(object.id); + switch (state) { + case CREATED: + sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + break; + case CLEAN: + break; + case DIRTY: + sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + if (object.getValueLabelHint() != null) + mLiveRegions.put(object.id, LiveRegionState.CLEAN); + break; + } + } else if (object.hadFlag(Flag.IS_LIVE_REGION)) { + mLiveRegions.remove(object.id); + } if (mA11yFocusedObject != null && mA11yFocusedObject.id == object.id && !object.hadFlag(Flag.IS_SELECTED) && object.hasFlag(Flag.IS_SELECTED)) { AccessibilityEvent event = @@ -660,9 +704,6 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { sendAccessibilityEvent(selectionEvent); } } - if (object.hasFlag(Flag.IS_LIVE_REGION)) { - sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - } } } @@ -762,7 +803,11 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { if (nodeId == null) { return; } - sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + if (!mLiveRegions.containsKey(nodeId)) { + return; + } else if (mLiveRegions.get(nodeId) != LiveRegionState.CREATED) { + mLiveRegions.put(nodeId, LiveRegionState.DIRTY); + } } } } @@ -778,6 +823,7 @@ private void willRemoveSemanticsObject(SemanticsObject object) { assert mObjects.containsKey(object.id); assert mObjects.get(object.id) == object; object.parent = null; + mLiveRegions.remove(object.id); if (mA11yFocusedObject == object) { sendAccessibilityEvent(mA11yFocusedObject.id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); mA11yFocusedObject = null; From 4dbca6c2604771a90e94f10ebc05247fbb6c0f7c Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Thu, 12 Jul 2018 14:06:23 -0700 Subject: [PATCH 22/34] add comments --- .../io/flutter/view/AccessibilityBridge.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index d1ac57784b069..dd0825df2d366 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -34,8 +34,9 @@ class AccessibilityBridge extends AccessibilityNodeProvider implements BasicMess private Map mObjects; private Map mCustomAccessibilityActions; - /// Creating a live region through the async semantics bridge requires some compromises in the design. - /// For starters, Talkback will not know about the + // Track whether the corresponding accessibility node has been created yet, and + // if an announcement needs to be made. TalkBack will not read the contents of + // the node until after the first TYPE_WINDOW_STATE_CHANGED event is dispatched. private Map mLiveRegions; private final FlutterView mOwner; private boolean mAccessibilityEnabled = false; @@ -273,6 +274,9 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { if (object.hasFlag(Flag.IS_LIVE_REGION)) { result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); LiveRegionState state = mLiveRegions.get(object.id); + // If the live region is newly created, mark the node as dirty + // for future updates. Sending an event here won't do anything + // since Talkback doesn't know it is a live region yet. if (state == LiveRegionState.CREATED) { mLiveRegions.put(object.id, LiveRegionState.DIRTY); } @@ -663,14 +667,16 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { LiveRegionState state = mLiveRegions.get(object.id); switch (state) { case CREATED: + // Send an initial event to force TalkBack to create the accessibility node info + // which informs it that the node is a live region. sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); break; case CLEAN: break; case DIRTY: + // Send an update to read out the contents of the semantics object. sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - if (object.getValueLabelHint() != null) - mLiveRegions.put(object.id, LiveRegionState.CLEAN); + mLiveRegions.put(object.id, LiveRegionState.CLEAN); break; } } else if (object.hadFlag(Flag.IS_LIVE_REGION)) { From fb13c0fa95030b4e7a10a4bec660e6909d248c3d Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Thu, 12 Jul 2018 14:14:55 -0700 Subject: [PATCH 23/34] clean up comments --- lib/ui/semantics.dart | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index f715a17ddd09a..b943165000e36 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -263,8 +263,14 @@ class SemanticsFlag { final int index; /// The semantics node has the quality of either being "checked" or "unchecked". + /// + /// This flag is mutually exclusive with [hasToggledState]. /// /// For example, a checkbox or a radio button widget has checked state. + /// + /// See also: + /// + /// * [SemanticsFlag.isChecked], which controls whether the node is "checked" or "unchecked". static const SemanticsFlag hasCheckedState = const SemanticsFlag._(_kHasCheckedStateIndex); /// Whether a semantics node that [hasCheckedState] is checked. @@ -273,6 +279,10 @@ class SemanticsFlag { /// "unchecked". /// /// For example, if a checkbox has a visible checkmark, [isChecked] is true. + /// + /// See also: + /// + /// * [SemanticsFlag.hasCheckedState], which enables a checked state. static const SemanticsFlag isChecked = const SemanticsFlag._(_kIsCheckedIndex); @@ -404,20 +414,30 @@ class SemanticsFlag { /// Platforms may use this information to make polite announcements to the /// user to inform them of updates to this node. /// - /// An example of a live region is a [SnackBar] widget. When it appears - /// on the screen it may be difficult to focus to read the value. A live - /// region causes a polite announcement to be generated automatically. + /// An example of a live region is a [SnackBar] widget. On Android, A live + /// region causes a polite announcement to be generated automatically, even + /// if the user does not have focus of the widget. static const SemanticsFlag isLiveRegion = const SemanticsFlag._(_kIsLiveRegionIndex); /// The semantics node has the quality of either being "on" or "off". + /// + /// This flag is mutually exclusive with [hasCheckedState]. /// /// For example, a switch has toggled state. + /// + /// See also: + /// + /// * [SemanticsFlag.isToggled], which controls whether the node is "on" or "off". static const SemanticsFlag hasToggledState = const SemanticsFlag._(_kHasToggledStateIndex); /// If true, the semantics node is "on". If false, the semantics node is /// "off". /// /// For example, if a switch is in the on position, [isToggled] is true. + /// + /// See also: + /// + /// * [SemanticsFlag.hasToggledState], which enables a toggled state. static const SemanticsFlag isToggled = const SemanticsFlag._(_kIsToggledIndex); /// The possible semantics flags. From 1778dde558398ce7a707e27308584d5f783c83c0 Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Thu, 12 Jul 2018 14:18:09 -0700 Subject: [PATCH 24/34] doc comment clean up --- lib/ui/semantics.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index b943165000e36..9678b1cc16e5b 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -157,9 +157,10 @@ class SemanticsAction { /// A request that the node should be dismissed. /// /// A [Snackbar], for example, may have a dismiss action to indicate to the - /// user that it can be removed after it is no longer relevant. On Android, - /// TalkBack announces this after reading the label. On iOS, VoiceOver users - /// can perform a standard gesture to dismiss it. + /// user that it can be removed after it is no longer relevant. On Android, + /// (with TalkBack) special hint text is spoken when focusing the node and + /// a custom action is availible in the local context menu. On iOS, + /// (with VoiceOver) users can perform a standard gesture to dismiss it. static const SemanticsAction dismiss = const SemanticsAction._(_kDismissIndex); /// The possible semantics actions. From 99fef792a5fb72b3bedbfb38b07f2c7fc3e8c0d4 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Thu, 12 Jul 2018 14:19:18 -0700 Subject: [PATCH 25/34] Update semantics.dart --- lib/ui/semantics.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 9678b1cc16e5b..d021c134ac15e 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -147,7 +147,7 @@ class SemanticsAction { /// is usually held by the element that currently responds to keyboard inputs. /// Accessibility focus and input focus can be held by two different nodes! static const SemanticsAction didLoseAccessibilityFocus = const SemanticsAction._(_kDidLoseAccessibilityFocusIndex); - + /// Indicates that the user has invoked a custom accessibility action. /// /// This handler is added automatically whenever a custom accessibility From c0d6d305d0291cf0e521b4607e3de7f57140fe03 Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Fri, 13 Jul 2018 20:08:49 -0700 Subject: [PATCH 26/34] clang format --- .../io/flutter/view/AccessibilityBridge.java | 238 +++++++++--------- .../framework/Source/accessibility_bridge.mm | 23 +- 2 files changed, 135 insertions(+), 126 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 9e2df52b26919..ca5d59c399ada 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -20,7 +20,8 @@ import java.nio.ByteBuffer; import java.util.*; -class AccessibilityBridge extends AccessibilityNodeProvider implements BasicMessageChannel.MessageHandler { +class AccessibilityBridge + extends AccessibilityNodeProvider implements BasicMessageChannel.MessageHandler { private static final String TAG = "FlutterView"; // Constants from higher API levels. @@ -110,8 +111,8 @@ enum Flag { mLiveRegions = new HashMap<>(); mCustomAccessibilityActions = new HashMap(); previousRoutes = new ArrayList<>(); - mFlutterAccessibilityChannel = new BasicMessageChannel<>(owner, "flutter/accessibility", - StandardMessageCodec.INSTANCE); + mFlutterAccessibilityChannel = new BasicMessageChannel<>( + owner, "flutter/accessibility", StandardMessageCodec.INSTANCE); mPackageName = owner.getContext().getPackageName(); } @@ -130,22 +131,19 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { if (virtualViewId == View.NO_ID) { AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(mOwner); mOwner.onInitializeAccessibilityNodeInfo(result); - if (mObjects.containsKey(ROOT_NODE_ID)) - result.addChild(mOwner, ROOT_NODE_ID); + if (mObjects.containsKey(ROOT_NODE_ID)) result.addChild(mOwner, ROOT_NODE_ID); return result; } SemanticsObject object = mObjects.get(virtualViewId); - if (object == null) - return null; + if (object == null) return null; AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(mOwner, virtualViewId); result.setPackageName(mOwner.getContext().getPackageName()); result.setClassName("android.view.View"); result.setSource(mOwner, virtualViewId); result.setFocusable(object.isFocusable()); - if (mInputFocusedObject != null) - result.setFocused(mInputFocusedObject.id == virtualViewId); + if (mInputFocusedObject != null) result.setFocused(mInputFocusedObject.id == virtualViewId); if (mA11yFocusedObject != null) result.setAccessibilityFocused(mA11yFocusedObject.id == virtualViewId); @@ -186,16 +184,16 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { } if (object.hasFlag(Flag.IS_BUTTON)) { - result.setClassName("android.widget.Button"); + result.setClassName("android.widget.Button"); } if (object.hasFlag(Flag.IS_IMAGE)) { - result.setClassName("android.widget.ImageView"); - // TODO(jonahwilliams): Figure out a way conform to the expected id from TalkBack's CustomLabelManager. - // talkback/src/main/java/labeling/CustomLabelManager.java#L525 + result.setClassName("android.widget.ImageView"); + // TODO(jonahwilliams): Figure out a way conform to the expected id from TalkBack's + // CustomLabelManager. talkback/src/main/java/labeling/CustomLabelManager.java#L525 } if (object.hasAction(Action.DISMISS)) { - result.setDismissable(true); - result.addAction(AccessibilityNodeInfo.ACTION_DISMISS); + result.setDismissable(true); + result.addAction(AccessibilityNodeInfo.ACTION_DISMISS); } if (object.parent != null) { @@ -217,8 +215,8 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { } result.setBoundsInScreen(bounds); result.setVisibleToUser(true); - result.setEnabled(!object.hasFlag(Flag.HAS_ENABLED_STATE) || - object.hasFlag(Flag.IS_ENABLED)); + result.setEnabled( + !object.hasFlag(Flag.HAS_ENABLED_STATE) || object.hasFlag(Flag.IS_ENABLED)); if (object.hasAction(Action.TAP)) { result.addAction(AccessibilityNodeInfo.ACTION_CLICK); @@ -245,7 +243,8 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { } } if (object.hasAction(Action.INCREASE) || object.hasAction(Action.DECREASE)) { - // TODO(jonahwilliams): support AccessibilityAction.ACTION_SET_PROGRESS once SDK is updated. + // TODO(jonahwilliams): support AccessibilityAction.ACTION_SET_PROGRESS once SDK is + // updated. result.setClassName("android.widget.SeekBar"); if (object.hasAction(Action.INCREASE)) { result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); @@ -257,7 +256,7 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { if (object.hasFlag(Flag.IS_LIVE_REGION)) { result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); } - + boolean hasCheckedState = object.hasFlag(Flag.HAS_CHECKED_STATE); boolean hasToggledState = object.hasFlag(Flag.HAS_TOGGLED_STATE); assert !(hasCheckedState && hasToggledState); @@ -287,7 +286,8 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { if (Build.VERSION.SDK_INT >= 21) { if (object.customAccessibilityAction != null) { for (CustomAccessibilityAction action : object.customAccessibilityAction) { - result.addAction(new AccessibilityNodeInfo.AccessibilityAction(action.resourceId, action.label)); + result.addAction(new AccessibilityNodeInfo.AccessibilityAction( + action.resourceId, action.label)); } } } @@ -364,13 +364,15 @@ public boolean performAction(int virtualViewId, int action, Bundle arguments) { } case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { mOwner.dispatchSemanticsAction(virtualViewId, Action.DID_LOSE_ACCESSIBILITY_FOCUS); - sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + sendAccessibilityEvent( + virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); mA11yFocusedObject = null; return true; } case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { mOwner.dispatchSemanticsAction(virtualViewId, Action.DID_GAIN_ACCESSIBILITY_FOCUS); - sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + sendAccessibilityEvent( + virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); if (mA11yFocusedObject == null) { // When Android focuses a node, it doesn't invalidate the view. @@ -394,15 +396,17 @@ public boolean performAction(int virtualViewId, int action, Bundle arguments) { case AccessibilityNodeInfo.ACTION_SET_SELECTION: { final Map selection = new HashMap(); final boolean hasSelection = arguments != null - && arguments.containsKey( - AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT) - && arguments.containsKey( - AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT); + && arguments.containsKey( + AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT) + && arguments.containsKey( + AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT); if (hasSelection) { - selection.put("base", arguments.getInt( - AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT)); - selection.put("extent", arguments.getInt( - AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT)); + selection.put("base", + arguments.getInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT)); + selection.put("extent", + arguments.getInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT)); } else { // Clear the selection selection.put("base", object.textSelectionExtent); @@ -430,32 +434,37 @@ public boolean performAction(int virtualViewId, int action, Bundle arguments) { default: // might be a custom accessibility action. final int flutterId = action - firstResourceId; - CustomAccessibilityAction contextAction = mCustomAccessibilityActions.get(flutterId); + CustomAccessibilityAction contextAction = + mCustomAccessibilityActions.get(flutterId); if (contextAction != null) { - mOwner.dispatchSemanticsAction(virtualViewId, Action.CUSTOM_ACTION, contextAction.id); + mOwner.dispatchSemanticsAction( + virtualViewId, Action.CUSTOM_ACTION, contextAction.id); return true; } } return false; } - boolean performCursorMoveAction(SemanticsObject object, int virtualViewId, Bundle arguments, boolean forward) { - final int granularity = arguments.getInt( - AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT); + boolean performCursorMoveAction( + SemanticsObject object, int virtualViewId, Bundle arguments, boolean forward) { + final int granularity = + arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT); final boolean extendSelection = arguments.getBoolean( - AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN); + AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN); switch (granularity) { case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER: { if (forward && object.hasAction(Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) { - mOwner.dispatchSemanticsAction(virtualViewId, Action.MOVE_CURSOR_FORWARD_BY_CHARACTER, extendSelection); + mOwner.dispatchSemanticsAction(virtualViewId, + Action.MOVE_CURSOR_FORWARD_BY_CHARACTER, extendSelection); return true; } if (!forward && object.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) { - mOwner.dispatchSemanticsAction(virtualViewId, Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER, extendSelection); + mOwner.dispatchSemanticsAction(virtualViewId, + Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER, extendSelection); return true; } } - // TODO(goderbauer): support other granularities. + // TODO(goderbauer): support other granularities. } return false; } @@ -478,20 +487,19 @@ public AccessibilityNodeInfo findFocus(int focus) { return null; } - private SemanticsObject getRootObject() { - assert mObjects.containsKey(0); - return mObjects.get(0); + assert mObjects.containsKey(0); + return mObjects.get(0); } private SemanticsObject getOrCreateObject(int id) { - SemanticsObject object = mObjects.get(id); - if (object == null) { - object = new SemanticsObject(); - object.id = id; - mObjects.put(id, object); - } - return object; + SemanticsObject object = mObjects.get(id); + if (object == null) { + object = new SemanticsObject(); + object.id = id; + mObjects.put(id, object); + } + return object; } private CustomAccessibilityAction getOrCreateAction(int id) { @@ -516,7 +524,7 @@ void handleTouchExploration(float x, float y) { if (mObjects.isEmpty()) { return; } - SemanticsObject newObject = getRootObject().hitTest(new float[]{ x, y, 0, 1 }); + SemanticsObject newObject = getRootObject().hitTest(new float[] {x, y, 0, 1}); if (newObject != mHoveredObject) { // sending ENTER before EXIT is how Android wants it if (newObject != null) { @@ -530,7 +538,8 @@ void handleTouchExploration(float x, float y) { } void updateCustomAccessibilityActions(ByteBuffer buffer, String[] strings) { - ArrayList updatedActions = new ArrayList(); + ArrayList updatedActions = + new ArrayList(); while (buffer.hasRemaining()) { int id = buffer.getInt(); CustomAccessibilityAction action = getOrCreateAction(id); @@ -560,10 +569,10 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { SemanticsObject rootObject = getRootObject(); List newRoutes = new ArrayList<>(); if (rootObject != null) { - final float[] identity = new float[16]; - Matrix.setIdentityM(identity, 0); - rootObject.updateRecursively(identity, visitedObjects, false); - rootObject.collectRoutes(newRoutes); + final float[] identity = new float[16]; + Matrix.setIdentityM(identity, 0); + rootObject.updateRecursively(identity, visitedObjects, false); + rootObject.collectRoutes(newRoutes); } // Dispatch a TYPE_WINDOW_STATE_CHANGED event if the most recent route id changed from the @@ -638,7 +647,8 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { } if (object.hasFlag(Flag.IS_LIVE_REGION)) { if (!mLiveRegions.containsKey(object.id) || mLiveRegions.get(object.id)) { - sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + sendAccessibilityEvent( + object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); mLiveRegions.put(object.id, false); } } else if (object.hadFlag(Flag.IS_LIVE_REGION)) { @@ -652,8 +662,7 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { sendAccessibilityEvent(event); } if (mInputFocusedObject != null && mInputFocusedObject.id == object.id - && object.hadFlag(Flag.IS_TEXT_FIELD) - && object.hasFlag(Flag.IS_TEXT_FIELD)) { + && object.hadFlag(Flag.IS_TEXT_FIELD) && object.hasFlag(Flag.IS_TEXT_FIELD)) { String oldValue = object.previousValue != null ? object.previousValue : ""; String newValue = object.value != null ? object.value : ""; AccessibilityEvent event = createTextChangedEvent(object.id, oldValue, newValue); @@ -664,7 +673,7 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { if (object.previousTextSelectionBase != object.textSelectionBase || object.previousTextSelectionExtent != object.textSelectionExtent) { AccessibilityEvent selectionEvent = obtainAccessibilityEvent( - object.id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED); + object.id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED); selectionEvent.getText().add(newValue); selectionEvent.setFromIndex(object.textSelectionBase); selectionEvent.setToIndex(object.textSelectionExtent); @@ -676,7 +685,8 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { } private AccessibilityEvent createTextChangedEvent(int id, String oldValue, String newValue) { - AccessibilityEvent e = obtainAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); + AccessibilityEvent e = + obtainAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); e.setBeforeText(oldValue); e.getText().add(newValue); @@ -687,7 +697,7 @@ private AccessibilityEvent createTextChangedEvent(int id, String oldValue, Strin } } if (i >= oldValue.length() && i >= newValue.length()) { - return null; // Text did not change + return null; // Text did not change } int firstDifference = i; e.setFromIndex(firstDifference); @@ -736,10 +746,10 @@ private void sendAccessibilityEvent(AccessibilityEvent event) { // Message Handler for [mFlutterAccessibilityChannel]. public void onMessage(Object message, BasicMessageChannel.Reply reply) { @SuppressWarnings("unchecked") - final HashMap annotatedEvent = (HashMap)message; - final String type = (String)annotatedEvent.get("type"); + final HashMap annotatedEvent = (HashMap) message; + final String type = (String) annotatedEvent.get("type"); @SuppressWarnings("unchecked") - final HashMap data = (HashMap)annotatedEvent.get("data"); + final HashMap data = (HashMap) annotatedEvent.get("data"); switch (type) { case "announce": @@ -762,7 +772,8 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { break; } case "tooltip": { - AccessibilityEvent e = obtainAccessibilityEvent(ROOT_NODE_ID, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + AccessibilityEvent e = obtainAccessibilityEvent( + ROOT_NODE_ID, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); e.getText().add((String) data.get("message")); sendAccessibilityEvent(e); } @@ -781,7 +792,8 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } private void createWindowChangeEvent(SemanticsObject route) { - AccessibilityEvent e = obtainAccessibilityEvent(route.id, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + AccessibilityEvent e = + obtainAccessibilityEvent(route.id, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); String routeName = route.getRouteName(); e.getText().add(routeName); sendAccessibilityEvent(e); @@ -793,7 +805,8 @@ private void willRemoveSemanticsObject(SemanticsObject object) { object.parent = null; mLiveRegions.remove(object.id); if (mA11yFocusedObject == object) { - sendAccessibilityEvent(mA11yFocusedObject.id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + sendAccessibilityEvent(mA11yFocusedObject.id, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); mA11yFocusedObject = null; } if (mInputFocusedObject == object) { @@ -807,14 +820,17 @@ private void willRemoveSemanticsObject(SemanticsObject object) { void reset() { mObjects.clear(); if (mA11yFocusedObject != null) - sendAccessibilityEvent(mA11yFocusedObject.id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + sendAccessibilityEvent(mA11yFocusedObject.id, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); mA11yFocusedObject = null; mHoveredObject = null; sendAccessibilityEvent(0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } private enum TextDirection { - UNKNOWN, LTR, RTL; + UNKNOWN, + LTR, + RTL; public static TextDirection fromInt(int value) { switch (value) { @@ -829,7 +845,7 @@ public static TextDirection fromInt(int value) { private class CustomAccessibilityAction { CustomAccessibilityAction() {} - + /// Resource id is the id of the custom action plus a minimum value so that the identifier /// does not collide with existing Android accessibility actions. int resourceId = -1; @@ -842,7 +858,7 @@ private class CustomAccessibilityAction { static int firstResourceId = 267386881; private class SemanticsObject { - SemanticsObject() { } + SemanticsObject() {} int id = -1; @@ -911,16 +927,18 @@ boolean didScroll() { } void log(String indent, boolean recursive) { - Log.i(TAG, indent + "SemanticsObject id=" + id + " label=" + label + " actions=" + actions + " flags=" + flags + "\n" + - indent + " +-- textDirection=" + textDirection + "\n"+ - indent + " +-- rect.ltrb=(" + left + ", " + top + ", " + right + ", " + bottom + ")\n" + - indent + " +-- transform=" + Arrays.toString(transform) + "\n"); - if (childrenInTraversalOrder != null && recursive) { - String childIndent = indent + " "; - for (SemanticsObject child : childrenInTraversalOrder) { - child.log(childIndent, recursive); - } - } + Log.i(TAG, + indent + "SemanticsObject id=" + id + " label=" + label + " actions=" + actions + + " flags=" + flags + "\n" + indent + " +-- textDirection=" + + textDirection + "\n" + indent + " +-- rect.ltrb=(" + left + ", " + + top + ", " + right + ", " + bottom + ")\n" + indent + + " +-- transform=" + Arrays.toString(transform) + "\n"); + if (childrenInTraversalOrder != null && recursive) { + String childIndent = indent + " "; + for (SemanticsObject child : childrenInTraversalOrder) { + child.log(childIndent, recursive); + } + } } void updateWith(ByteBuffer buffer, String[] strings) { @@ -964,10 +982,8 @@ void updateWith(ByteBuffer buffer, String[] strings) { right = buffer.getFloat(); bottom = buffer.getFloat(); - if (transform == null) - transform = new float[16]; - for (int i = 0; i < 16; ++i) - transform[i] = buffer.getFloat(); + if (transform == null) transform = new float[16]; + for (int i = 0; i < 16; ++i) transform[i] = buffer.getFloat(); inverseTransformDirty = true; globalGeometryDirty = true; @@ -1003,7 +1019,8 @@ void updateWith(ByteBuffer buffer, String[] strings) { customAccessibilityAction = null; } else { if (customAccessibilityAction == null) - customAccessibilityAction = new ArrayList(actionCount); + customAccessibilityAction = + new ArrayList(actionCount); else customAccessibilityAction.clear(); @@ -1015,11 +1032,9 @@ void updateWith(ByteBuffer buffer, String[] strings) { } private void ensureInverseTransform() { - if (!inverseTransformDirty) - return; + if (!inverseTransformDirty) return; inverseTransformDirty = false; - if (inverseTransform == null) - inverseTransform = new float[16]; + if (inverseTransform == null) inverseTransform = new float[16]; if (!Matrix.invertM(inverseTransform, 0, transform, 0)) Arrays.fill(inverseTransform, 0); } @@ -1033,8 +1048,7 @@ SemanticsObject hitTest(float[] point) { final float w = point[3]; final float x = point[0] / w; final float y = point[1] / w; - if (x < left || x >= right || y < top || y >= bottom) - return null; + if (x < left || x >= right || y < top || y >= bottom) return null; if (childrenInHitTestOrder != null) { final float[] transformedPoint = new float[4]; for (int i = 0; i < childrenInHitTestOrder.size(); i += 1) { @@ -1063,11 +1077,9 @@ boolean isFocusable() { } int scrollableActions = Action.SCROLL_RIGHT.value | Action.SCROLL_LEFT.value | Action.SCROLL_UP.value | Action.SCROLL_DOWN.value; - return (actions & ~scrollableActions) != 0 - || flags != 0 - || (label != null && !label.isEmpty()) - || (value != null && !value.isEmpty()) - || (hint != null && !hint.isEmpty()); + return (actions & ~scrollableActions) != 0 || flags != 0 + || (label != null && !label.isEmpty()) || (value != null && !value.isEmpty()) + || (hint != null && !hint.isEmpty()); } void collectRoutes(List edges) { @@ -1100,15 +1112,14 @@ String getRouteName() { return null; } - void updateRecursively(float[] ancestorTransform, Set visitedObjects, boolean forceUpdate) { + void updateRecursively(float[] ancestorTransform, Set visitedObjects, + boolean forceUpdate) { visitedObjects.add(this); - if (globalGeometryDirty) - forceUpdate = true; + if (globalGeometryDirty) forceUpdate = true; if (forceUpdate) { - if (globalTransform == null) - globalTransform = new float[16]; + if (globalTransform == null) globalTransform = new float[16]; Matrix.multiplyMM(globalTransform, 0, ancestorTransform, 0, transform, 0); final float[] sample = new float[4]; @@ -1136,15 +1147,12 @@ void updateRecursively(float[] ancestorTransform, Set visitedOb sample[1] = bottom; transformPoint(point4, globalTransform, sample); - if (globalRect == null) - globalRect = new Rect(); + if (globalRect == null) globalRect = new Rect(); - globalRect.set( - Math.round(min(point1[0], point2[0], point3[0], point4[0])), - Math.round(min(point1[1], point2[1], point3[1], point4[1])), - Math.round(max(point1[0], point2[0], point3[0], point4[0])), - Math.round(max(point1[1], point2[1], point3[1], point4[1])) - ); + globalRect.set(Math.round(min(point1[0], point2[0], point3[0], point4[0])), + Math.round(min(point1[1], point2[1], point3[1], point4[1])), + Math.round(max(point1[0], point2[0], point3[0], point4[0])), + Math.round(max(point1[1], point2[1], point3[1], point4[1]))); globalGeometryDirty = false; } @@ -1154,7 +1162,8 @@ void updateRecursively(float[] ancestorTransform, Set visitedOb if (childrenInTraversalOrder != null) { for (int i = 0; i < childrenInTraversalOrder.size(); ++i) { - childrenInTraversalOrder.get(i).updateRecursively(globalTransform, visitedObjects, forceUpdate); + childrenInTraversalOrder.get(i).updateRecursively( + globalTransform, visitedObjects, forceUpdate); } } } @@ -1178,11 +1187,10 @@ private float max(float a, float b, float c, float d) { private String getValueLabelHint() { StringBuilder sb = new StringBuilder(); - String[] array = { value, label, hint }; - for (String word: array) { + String[] array = {value, label, hint}; + for (String word : array) { if (word != null && word.length() > 0) { - if (sb.length() > 0) - sb.append(", "); + if (sb.length() > 0) sb.append(", "); sb.append(word); } } diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm index e1ae59290117d..3520363ea5708 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm @@ -42,8 +42,7 @@ } // namespace -@implementation FlutterCustomAccessibilityAction - { +@implementation FlutterCustomAccessibilityAction { } @end @@ -185,12 +184,12 @@ - (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action { return NO; int32_t action_id = action.uid; std::vector args; - args.push_back(3); // type=int32. + args.push_back(3); // type=int32. args.push_back(action_id); args.push_back(action_id >> 8); args.push_back(action_id >> 16); args.push_back(action_id >> 24); - [self bridge] ->DispatchSemanticsAction([self uid], blink::SemanticsAction::kCustomAction, args); + [self bridge] -> DispatchSemanticsAction([self uid], blink::SemanticsAction::kCustomAction, args); return YES; } @@ -518,7 +517,7 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { blink::CustomAccessibilityActionUpdates actions) { BOOL layoutChanged = NO; BOOL scrollOccured = NO; - for (const auto& entry: actions) { + for (const auto& entry : actions) { const blink::CustomAccessibilityAction& action = entry.second; actions_[action.id] = action; } @@ -538,14 +537,16 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { } object.children = newChildren; if (node.customAccessibilityActions.size() > 0) { - NSMutableArray* accessibilityCustomActions = + NSMutableArray* accessibilityCustomActions = [[[NSMutableArray alloc] init] autorelease]; for (int32_t action_id : node.customAccessibilityActions) { blink::CustomAccessibilityAction& action = actions_[action_id]; NSString* label = @(action.label.data()); SEL selector = @selector(onCustomAccessibilityAction:); - FlutterCustomAccessibilityAction* customAction = - [[FlutterCustomAccessibilityAction alloc] initWithName:label target:object selector:selector]; + FlutterCustomAccessibilityAction* customAction = + [[FlutterCustomAccessibilityAction alloc] initWithName:label + target:object + selector:selector]; customAction.uid = action_id; [accessibilityCustomActions addObject:customAction]; } @@ -611,10 +612,10 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { platform_view_->DispatchSemanticsAction(uid, action, args); } -void AccessibilityBridge::DispatchSemanticsAction(int32_t uid, - blink::SemanticsAction action, +void AccessibilityBridge::DispatchSemanticsAction(int32_t uid, + blink::SemanticsAction action, std::vector args) { - platform_view_->DispatchSemanticsAction(uid, action, args); + platform_view_->DispatchSemanticsAction(uid, action, args); } SemanticsObject* AccessibilityBridge::GetOrCreateObject(int32_t uid, From f25ae6509e13168d2b75e3b9333d6b268df88913 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Fri, 13 Jul 2018 23:10:47 -0700 Subject: [PATCH 27/34] Update semantics.dart --- lib/ui/semantics.dart | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index d021c134ac15e..07ac208deb91d 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -149,7 +149,7 @@ class SemanticsAction { static const SemanticsAction didLoseAccessibilityFocus = const SemanticsAction._(_kDidLoseAccessibilityFocusIndex); /// Indicates that the user has invoked a custom accessibility action. - /// + /// /// This handler is added automatically whenever a custom accessibility /// action is added to a semantics node. static const SemanticsAction customAction = const SemanticsAction._(_kCustomAction); @@ -157,7 +157,7 @@ class SemanticsAction { /// A request that the node should be dismissed. /// /// A [Snackbar], for example, may have a dismiss action to indicate to the - /// user that it can be removed after it is no longer relevant. On Android, + /// user that it can be removed after it is no longer relevant. On Android, /// (with TalkBack) special hint text is spoken when focusing the node and /// a custom action is availible in the local context menu. On iOS, /// (with VoiceOver) users can perform a standard gesture to dismiss it. @@ -264,13 +264,13 @@ class SemanticsFlag { final int index; /// The semantics node has the quality of either being "checked" or "unchecked". - /// + /// /// This flag is mutually exclusive with [hasToggledState]. /// /// For example, a checkbox or a radio button widget has checked state. - /// + /// /// See also: - /// + /// /// * [SemanticsFlag.isChecked], which controls whether the node is "checked" or "unchecked". static const SemanticsFlag hasCheckedState = const SemanticsFlag._(_kHasCheckedStateIndex); @@ -280,9 +280,9 @@ class SemanticsFlag { /// "unchecked". /// /// For example, if a checkbox has a visible checkmark, [isChecked] is true. - /// + /// /// See also: - /// + /// /// * [SemanticsFlag.hasCheckedState], which enables a checked state. static const SemanticsFlag isChecked = const SemanticsFlag._(_kIsCheckedIndex); @@ -421,23 +421,23 @@ class SemanticsFlag { static const SemanticsFlag isLiveRegion = const SemanticsFlag._(_kIsLiveRegionIndex); /// The semantics node has the quality of either being "on" or "off". - /// + /// /// This flag is mutually exclusive with [hasCheckedState]. /// /// For example, a switch has toggled state. - /// + /// /// See also: - /// - /// * [SemanticsFlag.isToggled], which controls whether the node is "on" or "off". + /// + /// * [SemanticsFlag.isToggled], which controls whether the node is "on" or "off". static const SemanticsFlag hasToggledState = const SemanticsFlag._(_kHasToggledStateIndex); /// If true, the semantics node is "on". If false, the semantics node is /// "off". /// /// For example, if a switch is in the on position, [isToggled] is true. - /// + /// /// See also: - /// + /// /// * [SemanticsFlag.hasToggledState], which enables a toggled state. static const SemanticsFlag isToggled = const SemanticsFlag._(_kIsToggledIndex); @@ -640,8 +640,8 @@ class SemanticsUpdateBuilder extends NativeFieldWrapperClass2 { ) native 'SemanticsUpdateBuilder_updateNode'; /// Update the custom accessibility action associated with the given `id`. - /// - /// The name of the action exposed to the user is the `label`. The text + /// + /// The name of the action exposed to the user is the `label`. The text /// direction of this label is the same as the global window. void updateCustomAction({int id, String label}) { assert(id != null); From 17e60b335e20bc8a20eb60ab9c90d448e64bd6bd Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Fri, 13 Jul 2018 23:52:28 -0700 Subject: [PATCH 28/34] Update semantics.dart --- lib/ui/semantics.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 07ac208deb91d..09a213aa1acac 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -410,12 +410,12 @@ class SemanticsFlag { static const SemanticsFlag isImage = const SemanticsFlag._(_kIsImageIndex); /// Whether the semantics node is a live region. - /// + /// /// A live region indicates that updates to semantics node are important. - /// Platforms may use this information to make polite announcements to the + /// Platforms may use this information to make polite announcements to the /// user to inform them of updates to this node. - /// - /// An example of a live region is a [SnackBar] widget. On Android, A live + /// + /// An example of a live region is a [SnackBar] widget. On Android, A live /// region causes a polite announcement to be generated automatically, even /// if the user does not have focus of the widget. static const SemanticsFlag isLiveRegion = const SemanticsFlag._(_kIsLiveRegionIndex); From 89bfcc1d53cf801f15125c4a2d12adbfcf91de75 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Tue, 17 Jul 2018 16:42:12 -0700 Subject: [PATCH 29/34] special case text field --- .../io/flutter/view/AccessibilityBridge.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index ca5d59c399ada..38f5c1692ab2c 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -38,6 +38,9 @@ class AccessibilityBridge // Track whether the corresponding accessibility node has been created yet, and // if an announcement needs to be made. private Map mLiveRegions; + // Track the labels of all text fields so that we can correctly send content changed + // events to trigger live region updates. + private Map mTextFields; private final FlutterView mOwner; private boolean mAccessibilityEnabled = false; private SemanticsObject mA11yFocusedObject; @@ -109,6 +112,7 @@ enum Flag { mOwner = owner; mObjects = new HashMap(); mLiveRegions = new HashMap<>(); + mTextFields = new HashMap<>(); mCustomAccessibilityActions = new HashMap(); previousRoutes = new ArrayList<>(); mFlutterAccessibilityChannel = new BasicMessageChannel<>( @@ -156,6 +160,7 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { if (object.textSelectionBase != -1 && object.textSelectionExtent != -1) { result.setTextSelection(object.textSelectionBase, object.textSelectionExtent); } + result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); } // Cursor movements @@ -653,6 +658,19 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { } } else if (object.hadFlag(Flag.IS_LIVE_REGION)) { mLiveRegions.remove(object.id); + } else if (object.hasFlag(Flag.IS_TEXT_FIELD)) { + if (!mTextFields.containsKey(object.id)) { + mTextFields.put(object.id, object.label); + } + if (mInputFocusedObject != null && mInputFocusedObject.id == object.id) { + if (!object.label.equals(mTextFields.get(object.id))) { + sendAccessibilityEvent( + object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + mTextFields.put(object.id, object.label); + } + } + } else if (object.hadFlag(Flag.IS_TEXT_FIELD)) { + mTextFields.remove(object.id); } if (mA11yFocusedObject != null && mA11yFocusedObject.id == object.id && !object.hadFlag(Flag.IS_SELECTED) && object.hasFlag(Flag.IS_SELECTED)) { @@ -804,6 +822,7 @@ private void willRemoveSemanticsObject(SemanticsObject object) { assert mObjects.get(object.id) == object; object.parent = null; mLiveRegions.remove(object.id); + mTextFields.remove(object.id); if (mA11yFocusedObject == object) { sendAccessibilityEvent(mA11yFocusedObject.id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); From 9efe1acfe12c0e53f9ffeb8c226955dedaa86cea Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Thu, 19 Jul 2018 16:40:46 -0700 Subject: [PATCH 30/34] address comments --- .../io/flutter/view/AccessibilityBridge.java | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 38f5c1692ab2c..3e12472235dec 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -48,7 +48,6 @@ class AccessibilityBridge private SemanticsObject mHoveredObject; private int previousRouteId = ROOT_NODE_ID; private List previousRoutes; - private String mPackageName; private final BasicMessageChannel mFlutterAccessibilityChannel; @@ -117,7 +116,6 @@ enum Flag { previousRoutes = new ArrayList<>(); mFlutterAccessibilityChannel = new BasicMessageChannel<>( owner, "flutter/accessibility", StandardMessageCodec.INSTANCE); - mPackageName = owner.getContext().getPackageName(); } void setAccessibilityEnabled(boolean accessibilityEnabled) { @@ -135,22 +133,29 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { if (virtualViewId == View.NO_ID) { AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(mOwner); mOwner.onInitializeAccessibilityNodeInfo(result); - if (mObjects.containsKey(ROOT_NODE_ID)) result.addChild(mOwner, ROOT_NODE_ID); + if (mObjects.containsKey(ROOT_NODE_ID)) { + result.addChild(mOwner, ROOT_NODE_ID); + } return result; } SemanticsObject object = mObjects.get(virtualViewId); - if (object == null) return null; + if (object == null) { + return null; + } AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(mOwner, virtualViewId); result.setPackageName(mOwner.getContext().getPackageName()); result.setClassName("android.view.View"); result.setSource(mOwner, virtualViewId); result.setFocusable(object.isFocusable()); - if (mInputFocusedObject != null) result.setFocused(mInputFocusedObject.id == virtualViewId); + if (mInputFocusedObject != null) { + result.setFocused(mInputFocusedObject.id == virtualViewId); + } - if (mA11yFocusedObject != null) + if (mA11yFocusedObject != null) { result.setAccessibilityFocused(mA11yFocusedObject.id == virtualViewId); + } if (object.hasFlag(Flag.IS_TEXT_FIELD)) { result.setPassword(object.hasFlag(Flag.IS_OBSCURED)); @@ -160,6 +165,9 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { if (object.textSelectionBase != -1 && object.textSelectionExtent != -1) { result.setTextSelection(object.textSelectionBase, object.textSelectionExtent); } + // Text fields will always be created as a live region, so that updates to + // the label trigger polite announcements. This makes it easy to follow a11y + // guidelines for text fields on Android. result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); } @@ -1001,8 +1009,12 @@ void updateWith(ByteBuffer buffer, String[] strings) { right = buffer.getFloat(); bottom = buffer.getFloat(); - if (transform == null) transform = new float[16]; - for (int i = 0; i < 16; ++i) transform[i] = buffer.getFloat(); + if (transform == null) { + transform = new float[16]; + } + for (int i = 0; i < 16; ++i) { + transform[i] = buffer.getFloat(); + } inverseTransformDirty = true; globalGeometryDirty = true; @@ -1051,11 +1063,16 @@ void updateWith(ByteBuffer buffer, String[] strings) { } private void ensureInverseTransform() { - if (!inverseTransformDirty) return; + if (!inverseTransformDirty) { + return; + } inverseTransformDirty = false; - if (inverseTransform == null) inverseTransform = new float[16]; - if (!Matrix.invertM(inverseTransform, 0, transform, 0)) + if (inverseTransform == null) { + inverseTransform = new float[16]; + } + if (!Matrix.invertM(inverseTransform, 0, transform, 0)) { Arrays.fill(inverseTransform, 0); + } } Rect getGlobalRect() { @@ -1135,10 +1152,14 @@ void updateRecursively(float[] ancestorTransform, Set visitedOb boolean forceUpdate) { visitedObjects.add(this); - if (globalGeometryDirty) forceUpdate = true; + if (globalGeometryDirty) { + forceUpdate = true; + } if (forceUpdate) { - if (globalTransform == null) globalTransform = new float[16]; + if (globalTransform == null) { + globalTransform = new float[16]; + } Matrix.multiplyMM(globalTransform, 0, ancestorTransform, 0, transform, 0); final float[] sample = new float[4]; From f32c555778c46a625be8cef99f38a234b1a111af Mon Sep 17 00:00:00 2001 From: jonahwilliams Date: Thu, 19 Jul 2018 16:42:25 -0700 Subject: [PATCH 31/34] remove unused import --- shell/platform/android/io/flutter/view/AccessibilityBridge.java | 1 - 1 file changed, 1 deletion(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 3e12472235dec..acc4b7d44eb3f 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -8,7 +8,6 @@ import android.opengl.Matrix; import android.os.Build; import android.os.Bundle; -import android.content.Context; import android.util.Log; import android.view.View; import android.view.accessibility.AccessibilityEvent; From 4249d62001b2e365c114a20d5abd4438a3cf92f3 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Fri, 20 Jul 2018 10:12:52 -0700 Subject: [PATCH 32/34] make adjusted changes --- .../io/flutter/view/AccessibilityBridge.java | 44 ++++--------------- 1 file changed, 8 insertions(+), 36 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 38f5c1692ab2c..ed3acf44d9185 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -35,12 +35,6 @@ class AccessibilityBridge private Map mObjects; private Map mCustomAccessibilityActions; - // Track whether the corresponding accessibility node has been created yet, and - // if an announcement needs to be made. - private Map mLiveRegions; - // Track the labels of all text fields so that we can correctly send content changed - // events to trigger live region updates. - private Map mTextFields; private final FlutterView mOwner; private boolean mAccessibilityEnabled = false; private SemanticsObject mA11yFocusedObject; @@ -111,8 +105,6 @@ enum Flag { assert owner != null; mOwner = owner; mObjects = new HashMap(); - mLiveRegions = new HashMap<>(); - mTextFields = new HashMap<>(); mCustomAccessibilityActions = new HashMap(); previousRoutes = new ArrayList<>(); mFlutterAccessibilityChannel = new BasicMessageChannel<>( @@ -650,27 +642,11 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { } sendAccessibilityEvent(event); } - if (object.hasFlag(Flag.IS_LIVE_REGION)) { - if (!mLiveRegions.containsKey(object.id) || mLiveRegions.get(object.id)) { - sendAccessibilityEvent( - object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - mLiveRegions.put(object.id, false); - } - } else if (object.hadFlag(Flag.IS_LIVE_REGION)) { - mLiveRegions.remove(object.id); - } else if (object.hasFlag(Flag.IS_TEXT_FIELD)) { - if (!mTextFields.containsKey(object.id)) { - mTextFields.put(object.id, object.label); - } - if (mInputFocusedObject != null && mInputFocusedObject.id == object.id) { - if (!object.label.equals(mTextFields.get(object.id))) { - sendAccessibilityEvent( - object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - mTextFields.put(object.id, object.label); - } - } - } else if (object.hadFlag(Flag.IS_TEXT_FIELD)) { - mTextFields.remove(object.id); + if (object.hasFlag(Flag.IS_LIVE_REGION) && !object.hadFlag(Flag.IS_LIVE_REGION)) { + sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } else if (object.hasFlag(Flag.IS_TEXT_FIELD) && object.label != object.previousLabel + && mInputFocusedObject != null && mInputFocusedObject.id == object.id) { + sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } if (mA11yFocusedObject != null && mA11yFocusedObject.id == object.id && !object.hadFlag(Flag.IS_SELECTED) && object.hasFlag(Flag.IS_SELECTED)) { @@ -800,11 +776,7 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { if (nodeId == null) { return; } - if (!mLiveRegions.containsKey(nodeId)) { - return; - } else { - mLiveRegions.put(nodeId, true); - } + sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } } } @@ -821,8 +793,6 @@ private void willRemoveSemanticsObject(SemanticsObject object) { assert mObjects.containsKey(object.id); assert mObjects.get(object.id) == object; object.parent = null; - mLiveRegions.remove(object.id); - mTextFields.remove(object.id); if (mA11yFocusedObject == object) { sendAccessibilityEvent(mA11yFocusedObject.id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); @@ -904,6 +874,7 @@ private class SemanticsObject { float previousScrollExtentMax; float previousScrollExtentMin; String previousValue; + String previousLabel; private float left; private float top; @@ -963,6 +934,7 @@ void log(String indent, boolean recursive) { void updateWith(ByteBuffer buffer, String[] strings) { hadPreviousConfig = true; previousValue = value; + previousLabel = label; previousFlags = flags; previousActions = actions; previousTextSelectionBase = textSelectionBase; From a1e088285a1413a92a6575b78f2c4499faf1542a Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Fri, 20 Jul 2018 10:36:59 -0700 Subject: [PATCH 33/34] remember that equals and == are different in Java --- .../android/io/flutter/view/AccessibilityBridge.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 67b75bfda7aa8..13041cc42808d 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -651,7 +651,7 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { } if (object.hasFlag(Flag.IS_LIVE_REGION) && !object.hadFlag(Flag.IS_LIVE_REGION)) { sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - } else if (object.hasFlag(Flag.IS_TEXT_FIELD) && object.label != object.previousLabel + } else if (object.hasFlag(Flag.IS_TEXT_FIELD) && object.didChangeLabel() && mInputFocusedObject != null && mInputFocusedObject.id == object.id) { sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } @@ -778,6 +778,8 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { e.getText().add((String) data.get("message")); sendAccessibilityEvent(e); } + // Requires that the node id provided corresponds to a live region, or TalkBack will + // ignore the event. case "updateLiveRegion": { Integer nodeId = (Integer) annotatedEvent.get("nodeId"); if (nodeId == null) { @@ -923,6 +925,13 @@ boolean didScroll() { && previousScrollPosition != scrollPosition; } + boolean didChangeLabel() { + if (label == null && previousLabel == null) { + return false; + } + return label == null || previousLabel == null || !label.equals(previousLabel); + } + void log(String indent, boolean recursive) { Log.i(TAG, indent + "SemanticsObject id=" + id + " label=" + label + " actions=" + actions From 11ea454d42106a3cab1ead744f8ef7441ce1c239 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Fri, 20 Jul 2018 11:03:54 -0700 Subject: [PATCH 34/34] Address comments --- .../android/io/flutter/view/AccessibilityBridge.java | 5 ++++- .../darwin/ios/framework/Source/accessibility_bridge.mm | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 13041cc42808d..ecd78af4bb01e 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -653,6 +653,8 @@ void updateSemantics(ByteBuffer buffer, String[] strings) { sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } else if (object.hasFlag(Flag.IS_TEXT_FIELD) && object.didChangeLabel() && mInputFocusedObject != null && mInputFocusedObject.id == object.id) { + // Text fields should announce when their label changes while focused. We use a live + // region tag to do so, and this event triggers that update. sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } if (mA11yFocusedObject != null && mA11yFocusedObject.id == object.id @@ -779,7 +781,8 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { sendAccessibilityEvent(e); } // Requires that the node id provided corresponds to a live region, or TalkBack will - // ignore the event. + // ignore the event. The event will cause talkback to read out the new label even + // if node is not focused. case "updateLiveRegion": { Integer nodeId = (Integer) annotatedEvent.get("nodeId"); if (nodeId == null) { diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm index 3520363ea5708..b619325a96888 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm @@ -367,6 +367,7 @@ - (UIAccessibilityTraits)accessibilityTraits { [self node].HasAction(blink::SemanticsAction::kDecrease)) { traits |= UIAccessibilityTraitAdjustable; } + // TODO(jonahwilliams): switches should have a value of "on" or "off" if ([self node].HasFlag(blink::SemanticsFlags::kIsSelected) || [self node].HasFlag(blink::SemanticsFlags::kIsToggled) || [self node].HasFlag(blink::SemanticsFlags::kIsChecked)) {