Bug 1473998 - Use EditText entry in accessible's classNames and use the hint for the label. r=yzen r=jchen a=lizzard
authorEitan Isaacson <eitan@monotonous.org>
Tue, 10 Jul 2018 17:31:00 +0300
changeset 477950 36921dd17b6b
parent 477949 1654819afa70
child 477951 f31b2ce16942
push id9475
push userarchaeopteryx@coole-files.de
push date2018-07-13 21:33 +0000
treeherdermozilla-beta@d7ab2f3df084 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyzen, jchen, lizzard
bugs1473998
milestone62.0
Bug 1473998 - Use EditText entry in accessible's classNames and use the hint for the label. r=yzen r=jchen a=lizzard
accessible/jsat/Constants.jsm
accessible/jsat/Presentation.jsm
accessible/tests/mochitest/jsat/test_text_editable_navigation.html
accessible/tests/mochitest/jsat/test_text_editing.html
accessible/tests/mochitest/jsat/test_text_navigation_focus.html
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
--- a/accessible/jsat/Constants.jsm
+++ b/accessible/jsat/Constants.jsm
@@ -4,16 +4,17 @@ const AndroidEvents = {
   VIEW_CLICKED: 0x01,
   VIEW_LONG_CLICKED: 0x02,
   VIEW_SELECTED: 0x04,
   VIEW_FOCUSED: 0x08,
   VIEW_TEXT_CHANGED: 0x10,
   WINDOW_STATE_CHANGED: 0x20,
   VIEW_HOVER_ENTER: 0x80,
   VIEW_HOVER_EXIT: 0x100,
+  WINDOW_CONTENT_CHANGED: 0x800,
   VIEW_SCROLLED: 0x1000,
   VIEW_TEXT_SELECTION_CHANGED: 0x2000,
   ANNOUNCEMENT: 0x4000,
   VIEW_ACCESSIBILITY_FOCUSED: 0x8000,
   VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: 0x20000,
 };
 
 function ConstantsMap(aObject, aPrefix, aMap = {}, aModifier = null) {
--- a/accessible/jsat/Presentation.jsm
+++ b/accessible/jsat/Presentation.jsm
@@ -8,21 +8,27 @@
 
 ChromeUtils.import("resource://gre/modules/accessibility/Utils.jsm");
 ChromeUtils.defineModuleGetter(this, "PivotContext", // jshint ignore:line
   "resource://gre/modules/accessibility/Utils.jsm");
 ChromeUtils.defineModuleGetter(this, "UtteranceGenerator", // jshint ignore:line
   "resource://gre/modules/accessibility/OutputGenerator.jsm");
 ChromeUtils.defineModuleGetter(this, "States", // jshint ignore:line
   "resource://gre/modules/accessibility/Constants.jsm");
+ChromeUtils.defineModuleGetter(this, "Roles", // jshint ignore:line
+  "resource://gre/modules/accessibility/Constants.jsm");
 ChromeUtils.defineModuleGetter(this, "AndroidEvents", // jshint ignore:line
   "resource://gre/modules/accessibility/Constants.jsm");
 
 var EXPORTED_SYMBOLS = ["Presentation"]; // jshint ignore:line
 
+const EDIT_TEXT_ROLES = new Set([
+  Roles.SPINBUTTON, Roles.PASSWORD_TEXT,
+  Roles.AUTOCOMPLETE, Roles.ENTRY, Roles.EDITCOMBOBOX]);
+
 class AndroidPresentor {
   constructor() {
     this.type = "Android";
     this.displayedAccessibles = new WeakMap();
   }
 
   /**
    * The virtual cursor's position changed.
@@ -36,43 +42,38 @@ class AndroidPresentor {
     let context = new PivotContext(
       aPosition, aOldPosition, aStartOffset, aEndOffset);
     if (!context.accessible) {
       return null;
     }
 
     let androidEvents = [];
 
-    let isExploreByTouch = (aReason == Ci.nsIAccessiblePivot.REASON_POINT &&
-                            Utils.AndroidSdkVersion >= 14);
-    let focusEventType = (Utils.AndroidSdkVersion >= 16) ?
-      AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED :
-      AndroidEvents.VIEW_FOCUSED;
+    const isExploreByTouch = aReason == Ci.nsIAccessiblePivot.REASON_POINT;
 
     if (isExploreByTouch) {
       // This isn't really used by TalkBack so this is a half-hearted attempt
       // for now.
       androidEvents.push({eventType: AndroidEvents.VIEW_HOVER_EXIT, text: []});
     }
 
     if (aReason === Ci.nsIAccessiblePivot.REASON_TEXT) {
-      if (Utils.AndroidSdkVersion >= 16) {
-        let adjustedText = context.textAndAdjustedOffsets;
+      const adjustedText = context.textAndAdjustedOffsets;
 
-        androidEvents.push({
-          eventType: AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-          text: [adjustedText.text],
-          fromIndex: adjustedText.startOffset,
-          toIndex: adjustedText.endOffset
-        });
-      }
+      androidEvents.push({
+        eventType: AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+        text: [adjustedText.text],
+        fromIndex: adjustedText.startOffset,
+        toIndex: adjustedText.endOffset
+      });
     } else {
       let info = this._infoFromContext(context);
       let eventType = isExploreByTouch ?
-        AndroidEvents.VIEW_HOVER_ENTER : focusEventType;
+        AndroidEvents.VIEW_HOVER_ENTER :
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED;
       androidEvents.push({...info, eventType});
     }
 
     try {
       context.accessibleForBounds.scrollTo(
         Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
     } catch (e) {}
 
@@ -138,35 +139,33 @@ class AndroidPresentor {
   }
 
   /**
    * Text selection has changed. TODO.
    */
   textSelectionChanged(aText, aStart, aEnd, aOldStart, aOldEnd, aIsFromUserInput) {
     let androidEvents = [];
 
-    if (Utils.AndroidSdkVersion >= 14 && !aIsFromUserInput) {
-      androidEvents.push({
-        eventType: AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
-        text: [aText],
-        fromIndex: aStart,
-        toIndex: aEnd,
-        itemCount: aText.length
-      });
-    }
-
-    if (Utils.AndroidSdkVersion >= 16 && aIsFromUserInput) {
+    if (aIsFromUserInput) {
       let [from, to] = aOldStart < aStart ?
         [aOldStart, aStart] : [aStart, aOldStart];
       androidEvents.push({
         eventType: AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
         text: [aText],
         fromIndex: from,
         toIndex: to
       });
+    } else {
+      androidEvents.push({
+        eventType: AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
+        text: [aText],
+        fromIndex: aStart,
+        toIndex: aEnd,
+        itemCount: aText.length
+      });
     }
 
     return androidEvents;
   }
 
   /**
    * Selection has changed.
    * XXX: Implement android event?
@@ -220,51 +219,44 @@ class AndroidPresentor {
   /**
    * The viewport has changed, either a scroll, pan, zoom, or
    *    landscape/portrait toggle.
    * @param {Window} aWindow window of viewport that changed.
    */
   viewportChanged(aWindow) {
     let currentContext = this.displayedAccessibles.get(aWindow);
 
-    if (Utils.AndroidSdkVersion < 14) {
-      return null;
-    }
-
     let events = [{
       eventType: AndroidEvents.VIEW_SCROLLED,
-      text: [],
       scrollX: aWindow.scrollX,
       scrollY: aWindow.scrollY,
       maxScrollX: aWindow.scrollMaxX,
-      maxScrollY: aWindow.scrollMaxY
+      maxScrollY: aWindow.scrollMaxY,
     }];
 
-    if (Utils.AndroidSdkVersion >= 16 && currentContext) {
+    if (currentContext) {
       let currentAcc = currentContext.accessibleForBounds;
       if (Utils.isAliveAndVisible(currentAcc)) {
         events.push({
-          eventType: AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED,
+          eventType: AndroidEvents.WINDOW_CONTENT_CHANGED,
           bounds: Utils.getBounds(currentAcc)
         });
       }
     }
 
     return events;
   }
 
   /**
    * Announce something. Typically an app state change.
    */
   announce(aAnnouncement) {
     let localizedAnnouncement = Utils.localize(aAnnouncement).join(" ");
     return [{
-      eventType: (Utils.AndroidSdkVersion >= 16) ?
-        AndroidEvents.ANNOUNCEMENT :
-        AndroidEvents.VIEW_TEXT_CHANGED,
+      eventType: AndroidEvents.ANNOUNCEMENT,
       text: [localizedAnnouncement],
       addedCount: localizedAnnouncement.length,
       removedCount: 0,
       fromIndex: 0
     }];
   }
 
 
@@ -290,23 +282,38 @@ class AndroidPresentor {
   liveRegion(aAccessible, aIsPolite, aIsHide, aModifiedText) {
     let context = !aModifiedText ?
       new PivotContext(aAccessible, null, -1, -1, true, !!aIsHide) : null;
     return this.announce(
       UtteranceGenerator.genForLiveRegion(context, aIsHide, aModifiedText));
   }
 
   _infoFromContext(aContext) {
-    let state = Utils.getState(aContext.accessible);
-    return {
-      text: Utils.localize(UtteranceGenerator.genForContext(aContext)),
+    const state = Utils.getState(aContext.accessible);
+    const info = {
       bounds: aContext.bounds,
       focusable: state.contains(States.FOCUSABLE),
       focused: state.contains(States.FOCUSED),
       clickable: aContext.accessible.actionCount > 0,
       checkable: state.contains(States.CHECKABLE),
       checked: state.contains(States.CHECKED),
       editable: state.contains(States.EDITABLE),
     };
+
+    if (EDIT_TEXT_ROLES.has(aContext.accessible.role)) {
+      let textAcc = aContext.accessible.QueryInterface(Ci.nsIAccessibleText);
+      return {
+        ...info,
+        className: "android.widget.EditText",
+        hint: aContext.accessible.name,
+        text: [textAcc.getText(0, -1)]
+      };
+    }
+
+    return {
+      ...info,
+      className: "android.view.View",
+      text: Utils.localize(UtteranceGenerator.genForContext(aContext)),
+    };
   }
 }
 
 const Presentation = new AndroidPresentor();
--- a/accessible/tests/mochitest/jsat/test_text_editable_navigation.html
+++ b/accessible/tests/mochitest/jsat/test_text_editable_navigation.html
@@ -36,19 +36,18 @@
       let evt;
 
       evt = await runner.focusSelector("textarea",
         AndroidEvents.VIEW_FOCUSED,
         AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
       is(evt[0].editable, true, "focused item is editable");
       runner.eventTextMatches(evt[1],
-        ["Text content test document",
-         "Please refrain from Mayoneggs during this salmonella scare.",
-         "text area"]);
+        ["Please refrain from Mayoneggs during this salmonella scare."]);
+      is(evt[1].className, "android.widget.EditText", "editable class");
       is(evt[1].focused, true, "a11y focused item is focused");
       is(evt[2].fromIndex, 0, "Correct fromIndex");
       is(evt[2].toIndex, 0, "Correct toIndex");
 
       evt = await runner.activateCurrent(10,
         AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
       checkMoveCaret(...evt, 0, 10);
--- a/accessible/tests/mochitest/jsat/test_text_editing.html
+++ b/accessible/tests/mochitest/jsat/test_text_editing.html
@@ -36,17 +36,17 @@
 
       let evt;
 
       evt = await runner.focusSelector("input",
         AndroidEvents.VIEW_FOCUSED,
         AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
       is(evt[0].editable, true, "focused item is editable");
-      runner.eventTextMatches(evt[1], ["Text content test document", "entry"]);
+      is(evt[1].className, "android.widget.EditText", "editable class");
       is(evt[1].focused, true, "a11y focused item is focused");
       is(evt[2].fromIndex, 0, "Caret at start (fromIndex)");
       is(evt[2].toIndex, 0, "Caret at start (toIndex)");
 
       evt = await runner.typeKey("B",
         AndroidEvents.VIEW_TEXT_CHANGED,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
       checkInsert(evt[0], evt[1], ["B"], 0, 1);
--- a/accessible/tests/mochitest/jsat/test_text_navigation_focus.html
+++ b/accessible/tests/mochitest/jsat/test_text_navigation_focus.html
@@ -27,31 +27,30 @@
     async function testTextNavigationFocus(doc, runner) {
       let evt;
 
       evt = await runner.focusSelector("textarea",
         AndroidEvents.VIEW_FOCUSED,
         AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
       is(evt[0].editable, true, "focused item is editable");
       is(evt[1].focused, true, "a11y focused item is focused");
+      is(evt[1].className, "android.widget.EditText", "editable class");
       runner.eventTextMatches(evt[1],
-        ["Text content test document",
-         "Please refrain from Mayoneggs during this salmonella scare.",
-         "text area"]);
+        ["Please refrain from Mayoneggs during this salmonella scare."]);
 
       evt = await runner.moveNext("Simple",
         AndroidEvents.VIEW_FOCUSED,
         AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
       is(evt[0].editable, false, "focused out of editable");
       runner.eventTextMatches(evt[1], ["So we don't get dessert?", "label"]);
       runner.isFocused("html");
 
       evt = await runner.moveNext("Simple",
         AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
-      runner.eventTextMatches(evt, ["entry"]);
+      is(evt.className, "android.widget.EditText", "editable class");
       runner.isFocused("html");
 
       evt = await runner.activateCurrent(0,
         AndroidEvents.VIEW_CLICKED,
         AndroidEvents.VIEW_FOCUSED,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
       is(evt[1].editable, true, "focused item is editable");
       is(evt[2].fromIndex, 0, "Cursor at start");
@@ -61,17 +60,17 @@
         AndroidEvents.VIEW_FOCUSED,
         AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
       is(evt[0].editable, false, "focused out of editable");
       runner.eventTextMatches(evt[1], ["So we don't get dessert?", "label"]);
       runner.isFocused("html");
 
       evt = await runner.moveNext("Simple",
         AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
-      runner.eventTextMatches(evt, ["entry"]);
+      is(evt.className, "android.widget.EditText", "editable class");
       runner.isFocused("html");
 
       // XXX: TEXT_SELECTION_CHANGED should be fired here
       evt = await runner.activateCurrent(0,
         AndroidEvents.VIEW_CLICKED,
         AndroidEvents.VIEW_FOCUSED);
       is(evt[1].editable, true, "focused item is editable");
       runner.isFocused("input[type=text]");
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
@@ -3,16 +3,18 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.geckoview.test
 
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDevToolsAPI
 
+import android.os.Build
+
 import android.support.test.filters.MediumTest
 import android.support.test.InstrumentationRegistry
 import android.support.test.runner.AndroidJUnit4
 
 import android.view.accessibility.AccessibilityNodeInfo
 import android.view.accessibility.AccessibilityNodeProvider
 import android.view.accessibility.AccessibilityEvent
 import android.view.accessibility.AccessibilityRecord
@@ -28,83 +30,87 @@ import org.junit.After
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 @WithDisplay(width = 480, height = 640)
 @WithDevToolsAPI
 class AccessibilityTest : BaseSessionTest() {
     lateinit var view: View
-    val provider: AccessibilityNodeProvider get() = view.getAccessibilityNodeProvider()
+    val provider: AccessibilityNodeProvider get() = view.accessibilityNodeProvider
 
     // Given a child ID, return the virtual descendent ID.
     private fun getVirtualDescendantId(childId: Long): Int {
         try {
-            var getVirtualDescendantIdMethod =
+            val getVirtualDescendantIdMethod =
                 AccessibilityNodeInfo::class.java.getMethod("getVirtualDescendantId", Long::class.java)
             return getVirtualDescendantIdMethod.invoke(null, childId) as Int
         } catch (ex: Exception) {
             return 0
         }
     }
 
     // Retrieve the virtual descendent ID of the event's source.
     private fun getSourceId(event: AccessibilityEvent): Int {
         try {
-            var getSourceIdMethod =
+            val getSourceIdMethod =
                 AccessibilityRecord::class.java.getMethod("getSourceNodeId")
             return getVirtualDescendantId(getSourceIdMethod.invoke(event) as Long)
         } catch (ex: Exception) {
             return 0
         }
     }
 
     private interface EventDelegate {
         fun onAccessibilityFocused(event: AccessibilityEvent) { }
         fun onFocused(event: AccessibilityEvent) { }
+        fun onTextSelectionChanged(event: AccessibilityEvent) { }
+        fun onTextChanged(event: AccessibilityEvent) { }
     }
 
     @Before fun setup() {
         // We initialize a view with a parent and grandparent so that the
-        // accessibility events propogate up at least to the parent.
+        // accessibility events propagate up at least to the parent.
         view = FrameLayout(InstrumentationRegistry.getTargetContext())
         FrameLayout(InstrumentationRegistry.getTargetContext()).addView(view)
         FrameLayout(InstrumentationRegistry.getTargetContext()).addView(view.parent as View)
 
         // Force on accessibility and assign the session's accessibility
         // object a view.
         sessionRule.setPrefsUntilTestEnd(mapOf("accessibility.force_disabled" to -1))
         mainSession.accessibility.view = view
 
         // Set up an external delegate that will intercept accessibility events.
         sessionRule.addExternalDelegateUntilTestEnd(
             EventDelegate::class,
         { newDelegate -> (view.parent as View).setAccessibilityDelegate(object : View.AccessibilityDelegate() {
             override fun onRequestSendAccessibilityEvent(host: ViewGroup, child: View, event: AccessibilityEvent): Boolean {
-                when (event.getEventType()) {
+                when (event.eventType) {
                     AccessibilityEvent.TYPE_VIEW_FOCUSED -> newDelegate.onFocused(event)
                     AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED -> newDelegate.onAccessibilityFocused(event)
+                    AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED -> newDelegate.onTextSelectionChanged(event)
+                    AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED -> newDelegate.onTextChanged(event)
                     else -> {}
                 }
                 return false
             }
         }) },
         { (view.parent as View).setAccessibilityDelegate(null) },
         object : EventDelegate { })
     }
 
     @After fun teardown() {
         sessionRule.session.accessibility.view = null
     }
 
     @Test fun testRootNode() {
         assertThat("provider is not null", provider, notNullValue())
-        var node = provider.createAccessibilityNodeInfo(AccessibilityNodeProvider.HOST_VIEW_ID)
+        val node = provider.createAccessibilityNodeInfo(AccessibilityNodeProvider.HOST_VIEW_ID)
         assertThat("Root node should have WebView class name",
-            node.getClassName().toString(), equalTo("android.webkit.WebView"))
+            node.className.toString(), equalTo("android.webkit.WebView"))
     }
 
     @Test fun testPageLoad() {
         sessionRule.session.loadTestPath(INPUTS_PATH)
 
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
             override fun onFocused(event: AccessibilityEvent) { }
@@ -118,26 +124,48 @@ class AccessibilityTest : BaseSessionTes
 
         provider.performAction(AccessibilityNodeProvider.HOST_VIEW_ID,
             AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
 
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
             override fun onAccessibilityFocused(event: AccessibilityEvent) {
                 nodeId = getSourceId(event)
-                var node = provider.createAccessibilityNodeInfo(nodeId)
-                assertThat("Text node should not be focusable", node.isFocusable(), equalTo(false))
+                val node = provider.createAccessibilityNodeInfo(nodeId)
+                assertThat("Text node should not be focusable", node.isFocusable, equalTo(false))
             }
         })
 
         provider.performAction(nodeId,
             AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
 
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
             override fun onAccessibilityFocused(event: AccessibilityEvent) {
                 nodeId = getSourceId(event)
-                var node = provider.createAccessibilityNodeInfo(nodeId)
-                assertThat("Entry node should be focusable", node.isFocusable(), equalTo(true))
+                val node = provider.createAccessibilityNodeInfo(nodeId)
+                assertThat("Entry node should be focusable", node.isFocusable, equalTo(true))
             }
         })
     }
-}
+
+    @Test fun testTextEntryNode() {
+        sessionRule.session.loadString("<input aria-label='Name' value='Tobias'>", "text/html")
+        sessionRule.waitForPageStop()
+
+        mainSession.evaluateJS("$('input').focus()")
+
+        sessionRule.waitUntilCalled(object : EventDelegate {
+            @AssertCalled(count = 1)
+            override fun onAccessibilityFocused(event: AccessibilityEvent) {
+                val nodeId = getSourceId(event)
+                val node = provider.createAccessibilityNodeInfo(nodeId)
+                assertThat("Focused EditBox", node.className.toString(),
+                        equalTo("android.widget.EditText"))
+                if (Build.VERSION.SDK_INT >= 19) {
+                    assertThat("Hint has field name",
+                            node.extras.getString("AccessibilityNodeInfo.hint"),
+                            equalTo("Name"))
+                }
+            }
+        })
+    }
+}
\ No newline at end of file
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
@@ -6,19 +6,17 @@
 package org.mozilla.geckoview;
 
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.PrefsHelper;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
-import org.mozilla.gecko.util.ThreadUtils;
 
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.InputDevice;
 import android.view.MotionEvent;
@@ -128,17 +126,16 @@ public class SessionAccessibility {
                             info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
                             info.addChild(hostView, VIRTUAL_CONTENT_ID);
                             break;
                         default:
                             info.setParent(mView);
                             info.setSource(mView, virtualDescendantId);
                             info.setVisibleToUser(mView.isShown());
                             info.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
-                            info.setClassName(mView.getClass().getName());
                             info.setEnabled(true);
                             info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
                             info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
                             info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
                             info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
                             info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
                             info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
                             info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
@@ -319,29 +316,30 @@ public class SessionAccessibility {
             // "GeckoView:AccessibilityEnabled" is dispatched to the UI thread.
             EventDispatcher.getInstance().dispatch("GeckoView:AccessibilityEnabled", ret);
         }
     }
 
     private AccessibilityEvent obtainEvent(final int eventType, final int sourceId) {
         AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
         event.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
-        event.setClassName(SessionAccessibility.class.getName());
         event.setSource(mView, sourceId);
 
         return event;
     }
 
     private static void populateEventFromJSON(AccessibilityEvent event, final GeckoBundle message) {
         final String[] textArray = message.getStringArray("text");
         if (textArray != null) {
             for (int i = 0; i < textArray.length; i++)
                 event.getText().add(textArray[i] != null ? textArray[i] : "");
         }
 
+        if (message.containsKey("className"))
+            event.setClassName(message.getString("className"));
         event.setContentDescription(message.getString("description", ""));
         event.setEnabled(message.getBoolean("enabled", true));
         event.setChecked(message.getBoolean("checked"));
         event.setPassword(message.getBoolean("password"));
         event.setAddedCount(message.getInt("addedCount", -1));
         event.setRemovedCount(message.getInt("removedCount", -1));
         event.setFromIndex(message.getInt("fromIndex", -1));
         event.setItemCount(message.getInt("itemCount", -1));
@@ -358,50 +356,60 @@ public class SessionAccessibility {
     private void populateNodeInfoFromJSON(AccessibilityNodeInfo node, final GeckoBundle message) {
         node.setEnabled(message.getBoolean("enabled", true));
         node.setCheckable(message.getBoolean("checkable"));
         node.setChecked(message.getBoolean("checked"));
         node.setPassword(message.getBoolean("password"));
         node.setFocusable(message.getBoolean("focusable"));
         node.setFocused(message.getBoolean("focused"));
         if (Build.VERSION.SDK_INT >= 18) {
-          node.setEditable(message.getBoolean("editable"));
+            node.setEditable(message.getBoolean("editable"));
         }
 
+        node.setClassName(message.getString("className", "android.view.View"));
+
         final String[] textArray = message.getStringArray("text");
         StringBuilder sb = new StringBuilder();
         if (textArray != null && textArray.length > 0) {
             sb.append(textArray[0] != null ? textArray[0] : "");
             for (int i = 1; i < textArray.length; i++) {
                 sb.append(' ').append(textArray[i] != null ? textArray[i] : "");
             }
             node.setText(sb.toString());
         }
         node.setContentDescription(message.getString("description", ""));
 
         if (message.getBoolean("clickable")) {
             node.setClickable(true);
             node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
         }
 
-        final GeckoBundle bounds = message.getBundle("bounds");
-        if (bounds != null) {
-            Rect screenBounds = new Rect(bounds.getInt("left"), bounds.getInt("top"),
-                                         bounds.getInt("right"), bounds.getInt("bottom"));
-            node.setBoundsInScreen(screenBounds);
+        if (Build.VERSION.SDK_INT >= 19 && message.containsKey("hint")) {
+            Bundle bundle = node.getExtras();
+            bundle.putCharSequence("AccessibilityNodeInfo.hint", message.getString("hint"));
+        }
+    }
 
-            final Matrix matrix = new Matrix();
-            final float[] origin = new float[2];
-            mSession.getClientToScreenMatrix(matrix);
-            matrix.mapPoints(origin);
-
-            screenBounds.offset((int) -origin[0], (int) -origin[1]);
-            node.setBoundsInParent(screenBounds);
+    private void updateBounds(final AccessibilityNodeInfo node, final GeckoBundle message) {
+        final GeckoBundle bounds = message.getBundle("bounds");
+        if (bounds == null) {
+            return;
         }
 
+        Rect screenBounds = new Rect(bounds.getInt("left"), bounds.getInt("top"),
+                                     bounds.getInt("right"), bounds.getInt("bottom"));
+        node.setBoundsInScreen(screenBounds);
+
+        final Matrix matrix = new Matrix();
+        final float[] origin = new float[2];
+        mSession.getClientToScreenMatrix(matrix);
+        matrix.mapPoints(origin);
+
+        screenBounds.offset((int) -origin[0], (int) -origin[1]);
+        node.setBoundsInParent(screenBounds);
     }
 
     private void sendAccessibilityEvent(final GeckoBundle message) {
         if (mView == null || !Settings.isEnabled())
             return;
 
         final int eventType = message.getInt("eventType", -1);
         if (eventType < 0) {
@@ -432,16 +440,21 @@ public class SessionAccessibility {
             // it work with TalkBack.
             if (mVirtualContentNode != null) {
                 mVirtualContentNode.recycle();
             }
             mVirtualContentNode = AccessibilityNodeInfo.obtain(mView, eventSource);
             populateNodeInfoFromJSON(mVirtualContentNode, message);
         }
 
+        if (mVirtualContentNode != null) {
+            // Bounds for the virtual content can be updated from any event.
+            updateBounds(mVirtualContentNode, message);
+        }
+
         final AccessibilityEvent accessibilityEvent = obtainEvent(eventType, eventSource);
         populateEventFromJSON(accessibilityEvent, message);
         ((ViewParent) mView).requestSendAccessibilityEvent(mView, accessibilityEvent);
     }
 
     public boolean onMotionEvent(final MotionEvent event) {
         if (!Settings.isEnabled()) {
             return false;