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. }