From bc2c99aa8baa85d89611076e0df26139eeb7fff7 Mon Sep 17 00:00:00 2001 From: Hugo Holgersson Date: Thu, 19 Sep 2019 14:37:26 +0200 Subject: [PATCH] Support role=application web widgets After: The green rect follows web focus. Before: TalkBack tries to read the TV apps as text documents. Background: Within ..., screen readers should not consume DPAD (arrow key) events. Web apps or widgets with role=application have, per the WAI-ARIA spec's contract, their own JavaScript logic for moving focus [1]. [1] w3c/aria#1049, where we discussed this. Problem: TalkBack does not handle role=application so such web apps lose their 4-way (up/down/left/right) navigation. TalkBack only moves forward/backward which breaks authors' pre-defined TV UX. Solution: Whenever accessibility focus (the green rect) goes to some web content with or anywhere within a role=application widget, we don't consume the DPAD events; we let them through. Testing done: Open a simple TV web app that has . Notice: Once the web view gets accessibilty focus, TalkBack won't eat (consume) DPAD key events and the the key events reach the web page's key handler in JavaScript. --- .../TelevisionNavigationController.java | 7 ++++ .../utils/AccessibilityNodeInfoUtils.java | 33 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/talkback/src/main/java/com/google/android/accessibility/talkback/controller/TelevisionNavigationController.java b/talkback/src/main/java/com/google/android/accessibility/talkback/controller/TelevisionNavigationController.java index 51c981344..ab89fa740 100644 --- a/talkback/src/main/java/com/google/android/accessibility/talkback/controller/TelevisionNavigationController.java +++ b/talkback/src/main/java/com/google/android/accessibility/talkback/controller/TelevisionNavigationController.java @@ -361,6 +361,13 @@ private boolean shouldHandleEvent(AccessibilityNodeInfoCompat cursor, KeyEvent e return false; } } + + // Web applications and web widgets with role=application have, per the + // WAI-ARIA spec's contract, their own JavaScript logic for moving focus. + // TalkBack should not consume key events when such an app has accessibility focus. + boolean shouldProcessDPadKeyEvent = this.shouldProcessDPadKeyEvent && + !AccessibilityNodeInfoUtils.isWebApplication(cursor); + // TalkBack should always consume up/down/left/right on the d-pad, unless // shouldProcessDPadKeyEvent is false. Otherwise, strange things will happen when TalkBack // cannot navigate further. diff --git a/utils/src/main/java/com/google/android/accessibility/utils/AccessibilityNodeInfoUtils.java b/utils/src/main/java/com/google/android/accessibility/utils/AccessibilityNodeInfoUtils.java index 1e118cb97..548c641a3 100644 --- a/utils/src/main/java/com/google/android/accessibility/utils/AccessibilityNodeInfoUtils.java +++ b/utils/src/main/java/com/google/android/accessibility/utils/AccessibilityNodeInfoUtils.java @@ -381,6 +381,39 @@ public boolean accept(AccessibilityNodeInfoCompat node) { || (node != null && node.getCollectionInfo() != null); }); + public static boolean hasApplicationWebRole(AccessibilityNodeInfoCompat node) { + return node != null && node.getExtras() != null + && node.getExtras().containsKey("AccessibilityNodeInfo.chromeRole") + && node.getExtras().get("AccessibilityNodeInfo.chromeRole").equals("application"); + } + + private static final Filter FILTER_IN_WEB_APPLICATION = + new Filter() { + @Override + public boolean accept(AccessibilityNodeInfoCompat node) { + return hasApplicationWebRole(node); + } + }; + + /** + * Returns true if |node| has role=application, i.e. |node| has JavaScript + * that handles key events. + */ + public static boolean isWebApplication(AccessibilityNodeInfoCompat node) { + // When a WebView-like view (an actual WebView or a browser) has focus: + // Check the web content's accessibility tree's first node. + // If that node wants raw key event, instead of first "tabbing" the green + // rect to it, skip ahead and let the web app directly decide where to go. + boolean firstWebNode = WebInterfaceUtils.supportsWebActions(node) + && !WebInterfaceUtils.supportsWebActions(node.getParent()); + boolean firstWebNodeWantsKeyEvents = firstWebNode + && node.getChildCount() > 0 + && hasApplicationWebRole(node.getChild(0)); + + return firstWebNodeWantsKeyEvents + || getSelfOrMatchingAncestor(node, FILTER_IN_WEB_APPLICATION) != null; + } + private AccessibilityNodeInfoUtils() { // This class is not instantiable. }