Bug 1535701 - Focus GeckoView when interacting with TalkBack. r=geckoview-reviewers,snorp a=pascalc
authorEitan Isaacson <eitan@monotonous.org>
Wed, 20 Mar 2019 16:48:40 +0000
changeset 525742 b5979c2fa68fefe151bf3ea1b8d2190beb245d7f
parent 525741 c6e144b70ff5fb7b8d4a13052dc243670267036d
child 525743 0ea6b4ddf9c6920e0e88fabadc749f82504a1bbe
push id2032
push userffxbld-merge
push dateMon, 13 May 2019 09:36:57 +0000
treeherdermozilla-release@455c1065dcbe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgeckoview-reviewers, snorp, pascalc
bugs1535701
milestone67.0
Bug 1535701 - Focus GeckoView when interacting with TalkBack. r=geckoview-reviewers,snorp a=pascalc When TalkBack receives a focus event, it redirects the accessibility focus (the green cursor) to the focused element. This is an important driver for the screen reader experience. Since the focus mode of the GeckoView is "focusable in touch", the focused state of the view is very arbitrary when using TalkBack since the user never directly touches the view. The only way for the view to regain focus is if a control or link in the content is interacted with. TalkBack user, who is explicitly interacting with the webview/geckoview would expect it to have focus, and to have the accessibility focus redirected in the page in the case of script-driven focus events. Differential Revision: https://phabricator.services.mozilla.com/D23747
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
@@ -173,16 +173,17 @@ public class SessionAccessibility {
                 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
                     mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityScrollBackward", null);
                     return true;
                 case AccessibilityNodeInfo.ACTION_SELECT:
                     nativeProvider.click(virtualViewId);
                     return true;
                 case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT:
                 case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT:
+                    requestViewFocus();
                     if (arguments != null) {
                         data = new GeckoBundle(1);
                         data.putString("rule", arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING));
                     } else {
                         data = null;
                     }
                     mSession.getEventDispatcher().dispatch(action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT ?
                                                            "GeckoView:AccessibilityNext" : "GeckoView:AccessibilityPrevious", data);
@@ -486,16 +487,17 @@ public class SessionAccessibility {
     // The current node with focus
     private int mFocusedNode = 0;
     // Viewport cache
     final SparseArray<GeckoBundle> mViewportCache = new SparseArray<>();
     // Focus cache
     final SparseArray<GeckoBundle> mFocusPathCache = new SparseArray<>();
     // List of caches in descending order from last updated.
     LinkedList<SparseArray<GeckoBundle>> mCaches = new LinkedList<>();
+    private boolean mViewFocusRequested = false;
 
     /* package */ SessionAccessibility(final GeckoSession session) {
         mSession = session;
         Settings.updateAccessibilitySettings();
     }
 
     /**
       * Get the View instance that delegates accessibility to this session.
@@ -535,19 +537,40 @@ public class SessionAccessibility {
                 if (hostView != mView) {
                     return null;
                 }
                 if (mProvider == null) {
                     mProvider = new NodeProvider();
                 }
                 return mProvider;
             }
+
+            @Override
+            public void sendAccessibilityEvent(View host, int eventType) {
+                if (eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
+                    // We rely on the focus events sent from Gecko.
+                    return;
+                }
+
+                super.sendAccessibilityEvent(host, eventType);
+            }
         });
     }
 
+    private boolean isInTest() {
+        return Build.VERSION.SDK_INT >= 17 && mView != null && mView.getDisplay() == null;
+    }
+
+    private void requestViewFocus() {
+        if (!mView.isFocused() && !isInTest()) {
+            mViewFocusRequested = true;
+            mView.requestFocus();
+        }
+    }
+
     private static class Settings {
         private static final String FORCE_ACCESSIBILITY_PREF = "accessibility.force_disabled";
 
         private static volatile boolean sEnabled;
         private static volatile boolean sTouchExplorationEnabled;
         /* package */ static volatile boolean sForceEnabled;
 
         static {
@@ -630,31 +653,38 @@ public class SessionAccessibility {
 
         final int action = event.getActionMasked();
         if ((action != MotionEvent.ACTION_HOVER_MOVE) &&
                 (action != MotionEvent.ACTION_HOVER_ENTER) &&
                 (action != MotionEvent.ACTION_HOVER_EXIT)) {
             return false;
         }
 
-        mView.requestFocus();
+        requestViewFocus();
 
         final GeckoBundle data = new GeckoBundle(2);
         data.putDoubleArray("coordinates", new double[] {event.getRawX(), event.getRawY()});
         mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityExploreByTouch", data);
         return true;
     }
 
     /* package */ void sendEvent(final int eventType, final int sourceId, final int className, final GeckoBundle eventData) {
         ThreadUtils.assertOnUiThread();
         if (mView == null) {
             return;
         }
 
-        if (!Settings.isPlatformEnabled() && (Build.VERSION.SDK_INT < 17 || mView.getDisplay() != null)) {
+        if (mViewFocusRequested && className == CLASSNAME_WEBVIEW) {
+            // If the view was focused from an accessiblity action or
+            // explore-by-touch, we supress this focus event to avoid noise.
+            mViewFocusRequested = false;
+            return;
+        }
+
+        if (!Settings.isPlatformEnabled() && !isInTest()) {
             // Accessibility could be activated in Gecko via xpcom, for example when using a11y
             // devtools. Here we assure that either Android a11y is *really* enabled, or no
             // display is attached and we must be in a junit test.
             return;
         }
 
         GeckoBundle cachedBundle = getMostRecentBundle(sourceId);
         if (cachedBundle == null && sourceId != View.NO_ID) {
@@ -716,16 +746,20 @@ public class SessionAccessibility {
                     mAccessibilityFocusedNode = 0;
                 }
                 break;
             case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
                 mAccessibilityFocusedNode = sourceId;
                 break;
             case AccessibilityEvent.TYPE_VIEW_FOCUSED:
                 mFocusedNode = sourceId;
+                if (!mView.isFocused() && !isInTest()) {
+                    // Don't dispatch a focus event if the parent view is not focused
+                    return;
+                }
                 break;
         }
 
         ((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
     }
 
     private synchronized GeckoBundle getMostRecentBundle(final int virtualViewId) {
         Iterator<SparseArray<GeckoBundle>> iter = mCaches.descendingIterator();