Bug 1577003 - Add `GeckoSession.getAutofillElements()` r=geckoview-reviewers,esawin,agi
authorJames Willcox <snorp@snorp.net>
Fri, 04 Oct 2019 17:55:26 +0000
changeset 496372 8be3fae908128e6aac63b72a795ffb19836b741c
parent 496371 6967ce1ef236215386d51f8a84d985a6cb9abb8f
child 496373 58284841398d105f6b5742d5919a46848818e0d7
push id97206
push userjwillcox@mozilla.com
push dateFri, 04 Oct 2019 17:57:55 +0000
treeherderautoland@204658865fb7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgeckoview-reviewers, esawin, agi
bugs1577003
milestone71.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1577003 - Add `GeckoSession.getAutofillElements()` r=geckoview-reviewers,esawin,agi This is an alternative to `GeckoSession.provideAutofillVirtualStructure()` that does not rely on the `ViewStructure` class. This is necessary to support autofill functionality on older devices or without involving the Android autofill service. This patch also moves the existing autofill tests into AutofillDelegateTest.kt and converts them to use this new API instead of the `ViewStructure` one. This allows us to test on devices lower than SDK 26 (which includes automation). In addition to the API changes, some tests also needed some greening up. Differential Revision: https://phabricator.services.mozilla.com/D47482
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutofillDelegateTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/AutofillElement.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/AutofillSupport.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutofillDelegateTest.kt
@@ -0,0 +1,573 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.geckoview.test
+
+import android.graphics.Matrix
+import android.os.Bundle
+import android.os.LocaleList
+import android.support.test.filters.MediumTest
+import android.support.test.runner.AndroidJUnit4
+import android.util.Pair
+import android.util.SparseArray
+import android.view.View
+import android.view.ViewStructure
+import android.view.autofill.AutofillId
+import android.view.autofill.AutofillValue
+import org.hamcrest.Matchers.*
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.geckoview.AutofillElement
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
+import org.mozilla.geckoview.test.util.Callbacks
+
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class AutofillDelegateTest : BaseSessionTest() {
+
+    @Test fun autofill() {
+        // Test parts of the Oreo auto-fill API; there is another autofill test in
+        // SessionAccessibility for a11y auto-fill support.
+        mainSession.loadTestPath(FORMS_HTML_PATH)
+        // Wait for the auto-fill nodes to populate.
+        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
+            // For the root document and the iframe document, each has a form group and
+            // a group for inputs outside of forms, so the total count is 4.
+            @AssertCalled(count = 4)
+            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
+            }
+        })
+
+        val autoFills = mapOf(
+                "#user1" to "bar", "#user2" to "bar",
+                "#pass1" to "baz", "#pass2" to "baz", "#email1" to "a@b.c",
+                "#number1" to "24", "#tel1" to "42")
+
+        // Set up promises to monitor the values changing.
+        val promises = autoFills.flatMap { entry ->
+            // Repeat each test with both the top document and the iframe document.
+            arrayOf("document", "document.querySelector('#iframe').contentDocument").map { doc ->
+                mainSession.evaluatePromiseJS("""new Promise(resolve =>
+                    $doc.querySelector('${entry.key}').addEventListener(
+                      'input', event => {
+                        let eventInterface =
+                          event instanceof InputEvent ? "InputEvent" :
+                          event instanceof UIEvent ? "UIEvent" :
+                          event instanceof Event ? "Event" : "Unknown";
+                        resolve([
+                          '${entry.key}',
+                          event.target.value,
+                          '${entry.value}',
+                          eventInterface
+                        ]);
+                }, { once: true }))""")
+            }
+        }
+
+        val autoFillValues = SparseArray<CharSequence>()
+
+        // Perform auto-fill and return number of auto-fills performed.
+        fun checkAutoFillChild(child: AutofillElement) {
+            // Seal the node info instance so we can perform actions on it.
+            if (child.children.count() > 0) {
+                for (c in child.children) {
+                    checkAutoFillChild(c!!)
+                }
+            }
+
+            if (child.id == View.NO_ID) {
+                return
+            }
+
+            assertThat("Should have HTML tag",
+                       child.tag, not(isEmptyOrNullString()))
+            assertThat("Web domain should match",
+                       child.domain, equalTo(GeckoSessionTestRule.TEST_ENDPOINT))
+
+            if (child.inputType == AutofillElement.INPUT_TYPE_TEXT) {
+                assertThat("Input should be enabled", child.enabled, equalTo(true))
+                assertThat("Input should be focusable",
+                        child.focusable, equalTo(true))
+
+                assertThat("Should have HTML tag", child.tag, equalTo("input"))
+                assertThat("Should have ID attribute", child.attributes.get("id"), not(isEmptyOrNullString()))
+            }
+
+            autoFillValues.append(child.id, when (child.inputType) {
+                AutofillElement.INPUT_TYPE_NUMBER -> "24"
+                AutofillElement.INPUT_TYPE_PHONE -> "42"
+                AutofillElement.INPUT_TYPE_TEXT -> when (child.hint) {
+                    AutofillElement.HINT_PASSWORD -> "baz"
+                    AutofillElement.HINT_EMAIL_ADDRESS -> "a@b.c"
+                    else -> "bar"
+                }
+                else -> "bar"
+            })
+        }
+
+        val elements = mainSession.autofillElements
+        checkAutoFillChild(elements)
+
+        mainSession.autofill(autoFillValues)
+
+        // Wait on the promises and check for correct values.
+        for ((key, actual, expected, eventInterface) in promises.map { it.value.asJSList<String>() }) {
+            assertThat("Auto-filled value must match ($key)", actual, equalTo(expected))
+
+            // <input type=number> elements don't get InputEvent events.
+            if (key == "#number1") {
+                assertThat("input type=number event should be dispatched with Event interface", eventInterface, equalTo("Event"))
+            } else {
+                assertThat("input event should be dispatched with InputEvent interface", eventInterface, equalTo("InputEvent"))
+            }
+        }
+    }
+
+    private fun countAutoFillNodes(cond: (AutofillElement) -> Boolean =
+                                   { it.inputType != AutofillElement.INPUT_TYPE_NONE },
+                           root: AutofillElement? = null): Int {
+        val node = if (root !== null) root else mainSession.autofillElements
+        return (if (cond(node)) 1 else 0) +
+                node.children.sumBy {
+                    countAutoFillNodes(cond, it) }
+    }
+
+    @WithDisplay(width = 100, height = 100)
+    @Test fun autoFill_navigation() {
+        // Wait for the accessibility nodes to populate.
+        mainSession.loadTestPath(FORMS_HTML_PATH)
+        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
+            @AssertCalled(count = 4)
+            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be starting auto-fill", notification, equalTo(forEachCall(
+                        GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_STARTED,
+                        GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED)))
+                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
+            }
+        })
+
+        assertThat("Initial auto-fill count should match",
+                   countAutoFillNodes(), equalTo(14))
+
+        // Now wait for the nodes to clear.
+        mainSession.loadTestPath(HELLO_HTML_PATH)
+        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
+            @AssertCalled(count = 1)
+            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be canceling auto-fill",
+                           notification,
+                           equalTo(GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_CANCELED))
+                assertThat("ID should be valid", virtualId, equalTo(View.NO_ID))
+            }
+        })
+        assertThat("Should not have auto-fill fields",
+                   countAutoFillNodes(), equalTo(0))
+
+        // Now wait for the nodes to reappear.
+        mainSession.waitForPageStop()
+        mainSession.goBack()
+        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
+            @AssertCalled(count = 4)
+            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be starting auto-fill", notification, equalTo(forEachCall(
+                        GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_STARTED,
+                        GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED)))
+                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
+            }
+        })
+        assertThat("Should have auto-fill fields again",
+                   countAutoFillNodes(), equalTo(14))
+        assertThat("Should not have focused field",
+                   countAutoFillNodes({ it.focused }), equalTo(0))
+
+        mainSession.evaluateJS("document.querySelector('#pass2').focus()")
+        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
+            @AssertCalled(count = 1)
+            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be entering auto-fill view",
+                           notification,
+                           equalTo(GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED))
+                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
+            }
+        })
+        assertThat("Should have one focused field",
+                   countAutoFillNodes({ it.focused }), equalTo(1))
+        // The focused field, its siblings, its parent, and the root node should be visible.
+        assertThat("Should have seven visible nodes",
+                   countAutoFillNodes({ node -> node.dimensions.width() > 0 && node.dimensions.height() > 0 }),
+                   equalTo(7))
+
+        mainSession.evaluateJS("document.querySelector('#pass2').blur()")
+        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
+            @AssertCalled(count = 1)
+            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be exiting auto-fill view",
+                           notification,
+                           equalTo(GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_EXITED))
+                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
+            }
+        })
+        assertThat("Should not have focused field",
+                   countAutoFillNodes({ it.focused }), equalTo(0))
+    }
+
+    @WithDisplay(height = 100, width = 100)
+    @Test fun autofill_userpass() {
+        mainSession.loadTestPath(FORMS2_HTML_PATH)
+        // Wait for the auto-fill nodes to populate.
+        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
+            @AssertCalled(count = 3)
+            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Autofill notification should match", notification,
+                        equalTo(forEachCall(GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_STARTED,
+                                GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED,
+                                GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED)))
+            }
+        })
+
+        // Perform auto-fill and return number of auto-fills performed.
+        fun checkAutoFillChild(child: AutofillElement): Int {
+            var sum = 0
+            // Seal the node info instance so we can perform actions on it.
+            for (c in child.children) {
+                sum += checkAutoFillChild(c!!)
+            }
+
+            if (child.hint == AutofillElement.HINT_NONE) {
+                return sum
+            }
+
+            assertThat("ID should be valid", child.id, not(equalTo(View.NO_ID)))
+            assertThat("Should have HTML tag", child.tag, equalTo("input"))
+
+            return sum + 1
+        }
+
+        val root = mainSession.autofillElements
+
+        // form and iframe have each have 2 elements with hints.
+        assertThat("autofill hint count",
+                   checkAutoFillChild(root), equalTo(4))
+    }
+
+    @WithDisplay(width = 100, height = 100)
+    @Test fun autofillActiveChange() {
+        // We should blur the active autofill element if the session is set
+        // inactive. Likewise, we should focus an element once we return.
+        mainSession.loadTestPath(FORMS_HTML_PATH)
+        // Wait for the auto-fill nodes to populate.
+        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
+            // For the root document and the iframe document, each has a form group and
+            // a group for inputs outside of forms, so the total count is 4.
+            @AssertCalled(count = 4)
+            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
+            }
+        })
+
+        mainSession.evaluateJS("document.querySelector('#pass2').focus()")
+        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
+            @AssertCalled(count = 1)
+            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be entering auto-fill view",
+                        notification,
+                        equalTo(GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED))
+                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
+            }
+        })
+        assertThat("Should have one focused field",
+                countAutoFillNodes({ it.focused }), equalTo(1))
+
+        // Make sure we get VIEW_EXITED when inactive
+        mainSession.setActive(false)
+        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
+            @AssertCalled(count = 1)
+            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be exiting auto-fill view",
+                        notification,
+                        equalTo(GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_EXITED))
+                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
+            }
+        })
+
+        // Make sure we get VIEW_ENTERED when active once again
+        mainSession.setActive(true)
+        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
+            @AssertCalled(count = 1)
+            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be entering auto-fill view",
+                        notification,
+                        equalTo(GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED))
+                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
+            }
+        })
+        assertThat("Should have one focused field",
+                countAutoFillNodes({ it.focused }), equalTo(1))
+    }
+
+    class MockViewNode : ViewStructure() {
+        private var mClassName: String? = null
+        private var mEnabled = false
+        private var mVisibility = -1
+        private var mPackageName: String? = null
+        private var mTypeName: String? = null
+        private var mEntryName: String? = null
+        private var mAutofillType = -1
+        private var mAutofillHints: Array<String>? = null
+        private var mInputType = -1
+        private var mHtmlInfo: HtmlInfo? = null
+        private var mWebDomain: String? = null
+        private var mFocused = false
+        private var mFocusable = false
+
+        var children = ArrayList<MockViewNode?>()
+        var id = View.NO_ID
+        var height = 0
+        var width = 0
+
+        val className get() = mClassName
+        val htmlInfo get() = mHtmlInfo
+        val autofillHints get() = mAutofillHints
+        val autofillType get() = mAutofillType
+        val webDomain get() = mWebDomain
+        val isEnabled get() = mEnabled
+        val isFocused get() = mFocused
+        val isFocusable get() = mFocusable
+        val visibility get() = mVisibility
+        val inputType get() = mInputType
+
+        override fun setId(id: Int, packageName: String?, typeName: String?, entryName: String?) {
+            this.id = id
+            mPackageName = packageName
+            mTypeName = typeName
+            mEntryName = entryName
+        }
+
+        override fun setHint(hint: CharSequence?) {
+            TODO("not implemented")
+        }
+
+        override fun setElevation(elevation: Float) {
+            TODO("not implemented")
+        }
+
+        override fun getText(): CharSequence {
+            TODO("not implemented")
+        }
+
+        override fun setText(text: CharSequence?) {
+            TODO("not implemented")
+        }
+
+        override fun setText(text: CharSequence?, selectionStart: Int, selectionEnd: Int) {
+            TODO("not implemented")
+        }
+
+        override fun asyncCommit() {
+            TODO("not implemented")
+        }
+
+        override fun getChildCount(): Int = children.size
+
+        override fun setEnabled(state: Boolean) {
+            mEnabled = state
+        }
+
+        override fun setLocaleList(localeList: LocaleList?) {
+            TODO("not implemented")
+        }
+
+        override fun setDimens(left: Int, top: Int, scrollX: Int, scrollY: Int, width: Int, height: Int) {
+            this.width = width
+            this.height = height
+        }
+
+        override fun setChecked(state: Boolean) {
+            TODO("not implemented")
+        }
+
+        override fun setContextClickable(state: Boolean) {
+            TODO("not implemented")
+        }
+
+        override fun setAccessibilityFocused(state: Boolean) {
+            TODO("not implemented")
+        }
+
+        override fun setAlpha(alpha: Float) {
+            TODO("not implemented")
+        }
+
+        override fun setTransformation(matrix: Matrix?) {
+            TODO("not implemented")
+        }
+
+        override fun setClassName(className: String?) {
+            mClassName = className
+        }
+
+        override fun setLongClickable(state: Boolean) {
+            TODO("not implemented")
+        }
+
+        override fun newChild(index: Int): ViewStructure {
+            val child = MockViewNode()
+            children[index] = child
+            return child
+        }
+
+        override fun getHint(): CharSequence {
+            TODO("not implemented")
+        }
+
+        override fun setInputType(inputType: Int) {
+            mInputType = inputType
+        }
+
+        override fun setWebDomain(domain: String?) {
+            mWebDomain = domain
+        }
+
+        override fun setAutofillOptions(options: Array<out CharSequence>?) {
+            TODO("not implemented")
+        }
+
+        override fun setTextStyle(size: Float, fgColor: Int, bgColor: Int, style: Int) {
+            TODO("not implemented")
+        }
+
+        override fun setVisibility(visibility: Int) {
+            mVisibility = visibility
+        }
+
+        override fun getAutofillId(): AutofillId? {
+            TODO("not implemented")
+        }
+
+        override fun setHtmlInfo(htmlInfo: HtmlInfo) {
+            mHtmlInfo = htmlInfo
+        }
+
+        override fun setTextLines(charOffsets: IntArray?, baselines: IntArray?) {
+            TODO("not implemented")
+        }
+
+        override fun getExtras(): Bundle {
+            TODO("not implemented")
+        }
+
+        override fun setClickable(state: Boolean) {
+            TODO("not implemented")
+        }
+
+        override fun newHtmlInfoBuilder(tagName: String): HtmlInfo.Builder {
+            return MockHtmlInfoBuilder(tagName)
+        }
+
+        override fun getTextSelectionEnd(): Int {
+            TODO("not implemented")
+        }
+
+        override fun setAutofillId(id: AutofillId) {
+            TODO("not implemented")
+        }
+
+        override fun setAutofillId(parentId: AutofillId, virtualId: Int) {
+            TODO("not implemented")
+        }
+
+        override fun hasExtras(): Boolean {
+            TODO("not implemented")
+        }
+
+        override fun addChildCount(num: Int): Int {
+            TODO("not implemented")
+        }
+
+        override fun setAutofillType(type: Int) {
+            mAutofillType = type
+        }
+
+        override fun setActivated(state: Boolean) {
+            TODO("not implemented")
+        }
+
+        override fun setFocused(state: Boolean) {
+            mFocused = state
+        }
+
+        override fun getTextSelectionStart(): Int {
+            TODO("not implemented")
+        }
+
+        override fun setChildCount(num: Int) {
+            children = ArrayList()
+            for (i in 0 until num) {
+                children.add(null)
+            }
+        }
+
+        override fun setAutofillValue(value: AutofillValue?) {
+            TODO("not implemented")
+        }
+
+        override fun setAutofillHints(hint: Array<String>?) {
+            mAutofillHints = hint
+        }
+
+        override fun setContentDescription(contentDescription: CharSequence?) {
+            TODO("not implemented")
+        }
+
+        override fun setFocusable(state: Boolean) {
+            mFocusable = state
+        }
+
+        override fun setCheckable(state: Boolean) {
+            TODO("not implemented")
+        }
+
+        override fun asyncNewChild(index: Int): ViewStructure {
+            TODO("not implemented")
+        }
+
+        override fun setSelected(state: Boolean) {
+            TODO("not implemented")
+        }
+
+        override fun setDataIsSensitive(sensitive: Boolean) {
+            TODO("not implemented")
+        }
+
+        override fun setOpaque(opaque: Boolean) {
+            TODO("not implemented")
+        }
+    }
+
+    class MockHtmlInfoBuilder(tagName: String) : ViewStructure.HtmlInfo.Builder() {
+        val mTagName = tagName
+        val mAttributes: MutableList<Pair<String, String>> = mutableListOf()
+
+        override fun addAttribute(name: String, value: String): ViewStructure.HtmlInfo.Builder {
+            mAttributes.add(Pair(name, value))
+            return this
+        }
+
+        override fun build(): ViewStructure.HtmlInfo {
+            return MockHtmlInfo(mTagName, mAttributes)
+        }
+    }
+
+    class MockHtmlInfo(tagName: String, attributes: MutableList<Pair<String, String>>)
+            : ViewStructure.HtmlInfo() {
+        private val mTagName = tagName
+        private val mAttributes = attributes
+
+        override fun getTag() = mTagName
+        override fun getAttributes() = mAttributes
+    }
+}
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
@@ -8,45 +8,39 @@ import android.app.ActivityManager
 import android.content.Context
 import android.graphics.Matrix
 import android.graphics.SurfaceTexture
 import android.net.Uri
 import android.os.Build
 import android.os.Bundle
 import android.os.LocaleList
 import android.os.Process
-import org.mozilla.geckoview.AllowOrDeny
-import org.mozilla.geckoview.GeckoResult
-import org.mozilla.geckoview.GeckoSession
 import org.mozilla.geckoview.GeckoSession.NavigationDelegate.LoadRequest
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.IgnoreCrash
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
 import org.mozilla.geckoview.test.util.Callbacks
 
 import android.support.annotation.AnyThread
 import android.support.test.filters.MediumTest
-import android.support.test.filters.SdkSuppress
 import android.support.test.runner.AndroidJUnit4
-import android.text.InputType
 import android.util.Pair
 import android.util.SparseArray
 import android.view.Surface
 import android.view.View
 import android.view.ViewStructure
 import android.view.autofill.AutofillId
 import android.view.autofill.AutofillValue
-import android.widget.EditText
 import org.hamcrest.Matchers.*
 import org.json.JSONObject
 import org.junit.Assume.assumeThat
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mozilla.gecko.GeckoAppShell
-import org.mozilla.geckoview.SlowScriptResponse
+import org.mozilla.geckoview.*
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.NullDelegate
 
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class ContentDelegateTest : BaseSessionTest() {
     @Test fun titleChange() {
@@ -242,561 +236,16 @@ class ContentDelegateTest : BaseSessionT
                 @AssertCalled(count = 1)
                 override fun onKill(session: GeckoSession) {
                     remainingSessions.remove(session)
                 }
             })
         }
     }
 
-    // TextInputDelegateTest is parameterized, so we put this test under ContentDelegateTest.
-    @SdkSuppress(minSdkVersion = 23)
-    @Test fun autofill() {
-        // Test parts of the Oreo auto-fill API; there is another autofill test in
-        // SessionAccessibility for a11y auto-fill support.
-        mainSession.loadTestPath(FORMS_HTML_PATH)
-        // Wait for the auto-fill nodes to populate.
-        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
-            // For the root document and the iframe document, each has a form group and
-            // a group for inputs outside of forms, so the total count is 4.
-            @AssertCalled(count = 4)
-            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
-            }
-        })
-
-        val autoFills = mapOf(
-                "#user1" to "bar", "#user2" to "bar") +
-                if (Build.VERSION.SDK_INT >= 26) mapOf(
-                        "#pass1" to "baz", "#pass2" to "baz", "#email1" to "a@b.c",
-                        "#number1" to "24", "#tel1" to "42")
-                else mapOf(
-                        "#pass1" to "bar", "#pass2" to "bar", "#email1" to "bar",
-                        "#number1" to "", "#tel1" to "bar")
-
-        // Set up promises to monitor the values changing.
-        val promises = autoFills.flatMap { entry ->
-            // Repeat each test with both the top document and the iframe document.
-            arrayOf("document", "document.querySelector('#iframe').contentDocument").map { doc ->
-                mainSession.evaluatePromiseJS("""new Promise(resolve =>
-                    $doc.querySelector('${entry.key}').addEventListener(
-                      'input', event => {
-                        let eventInterface =
-                          event instanceof InputEvent ? "InputEvent" :
-                          event instanceof UIEvent ? "UIEvent" :
-                          event instanceof Event ? "Event" : "Unknown";
-                        resolve([
-                          '${entry.key}',
-                          event.target.value,
-                          '${entry.value}',
-                          eventInterface
-                        ]);
-                }, { once: true }))""")
-            }
-        }
-
-        val autoFillValues = SparseArray<CharSequence>()
-
-        // Perform auto-fill and return number of auto-fills performed.
-        fun checkAutoFillChild(child: MockViewNode) {
-            // Seal the node info instance so we can perform actions on it.
-            if (child.childCount > 0) {
-                for (c in child.children) {
-                    checkAutoFillChild(c!!)
-                }
-            }
-
-            if (child.id == View.NO_ID) {
-                return
-            }
-
-            if (Build.VERSION.SDK_INT >= 26) {
-                assertThat("Should have HTML tag",
-                           child.htmlInfo!!.tag, not(isEmptyOrNullString()))
-                assertThat("Web domain should match",
-                           child.webDomain, equalTo(GeckoSessionTestRule.TEST_ENDPOINT))
-            }
-
-            if (EditText::class.java.name == child.className) {
-                assertThat("Input should be enabled", child.isEnabled, equalTo(true))
-                assertThat("Input should be focusable",
-                           child.isFocusable, equalTo(true))
-                assertThat("Input should be visible",
-                           child.visibility, equalTo(View.VISIBLE))
-
-                if (Build.VERSION.SDK_INT < 26) {
-                    autoFillValues.append(child.id, "bar")
-                    return
-                }
-
-                val htmlInfo = child.htmlInfo
-                assertThat("Should have HTML tag", htmlInfo!!.tag, equalTo("input"))
-                assertThat("Should have ID attribute",
-                           htmlInfo.attributes!!.map { it.first }, hasItem("id"))
-
-                assertThat("Autofill type should match",
-                           child.autofillType, equalTo(View.AUTOFILL_TYPE_TEXT))
-
-                assertThat("Autofill hints should match", child.autofillHints, equalTo(
-                        when (child.inputType) {
-                            InputType.TYPE_CLASS_TEXT or
-                                    InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD ->
-                                arrayOf(View.AUTOFILL_HINT_PASSWORD)
-                            InputType.TYPE_CLASS_TEXT or
-                                    InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ->
-                                arrayOf(View.AUTOFILL_HINT_EMAIL_ADDRESS)
-                            InputType.TYPE_CLASS_PHONE -> arrayOf(View.AUTOFILL_HINT_PHONE)
-                            InputType.TYPE_CLASS_TEXT or
-                                    InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT ->
-                                arrayOf(View.AUTOFILL_HINT_USERNAME)
-                            else -> null
-                        }))
-
-                autoFillValues.append(child.id, when (child.inputType) {
-                    InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> "baz"
-                    InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> "a@b.c"
-                    InputType.TYPE_CLASS_NUMBER -> "24"
-                    InputType.TYPE_CLASS_PHONE -> "42"
-                    else -> "bar"
-                })
-            }
-        }
-
-        val rootStructure = MockViewNode()
-
-        mainSession.provideAutofillVirtualStructure(null, rootStructure, 0)
-        checkAutoFillChild(rootStructure)
-
-        mainSession.autofill(autoFillValues)
-
-        // Wait on the promises and check for correct values.
-        for ((key, actual, expected, eventInterface) in promises.map { it.value.asJSList<String>() }) {
-            assertThat("Auto-filled value must match", actual, equalTo(expected))
-
-            // <input type=number> elements don't get InputEvent events.
-            if (key == "#number1") {
-                assertThat("input type=number event should be dispatched with Event interface", eventInterface, equalTo("Event"))
-            } else {
-                assertThat("input event should be dispatched with InputEvent interface", eventInterface, equalTo("InputEvent"))
-            }
-        }
-    }
-
-    // TextInputDelegateTest is parameterized, so we put this test under ContentDelegateTest.
-    @SdkSuppress(minSdkVersion = 23)
-    // The small screen is so that the page is forced to scroll to show the input
-    // and firing the autofill event.
-    // XXX(agi): This shouldn't be necessary though? What if the page doesn't scroll?
-    @WithDisplay(width = 10, height = 10)
-    @Test fun autoFill_navigation() {
-        fun countAutoFillNodes(cond: (MockViewNode) -> Boolean =
-                                       { it.className == "android.widget.EditText" },
-                               root: MockViewNode? = null): Int {
-            val node = if (root !== null) root else MockViewNode().also {
-                mainSession.provideAutofillVirtualStructure(null, it, 0)
-            }
-
-            return (if (cond(node)) 1 else 0) +
-                    (if (node.childCount > 0) node.children.sumBy {
-                        countAutoFillNodes(cond, it) } else 0)
-        }
-
-        // Wait for the accessibility nodes to populate.
-        mainSession.loadTestPath(FORMS_HTML_PATH)
-        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
-            @AssertCalled(count = 4)
-            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
-                assertThat("Should be starting auto-fill", notification, equalTo(forEachCall(
-                        GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_STARTED,
-                        GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED)))
-                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
-            }
-        })
-        assertThat("Initial auto-fill count should match",
-                   countAutoFillNodes(), equalTo(14))
-
-        // Now wait for the nodes to clear.
-        mainSession.loadTestPath(HELLO_HTML_PATH)
-        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
-            @AssertCalled(count = 1)
-            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
-                assertThat("Should be canceling auto-fill",
-                           notification,
-                           equalTo(GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_CANCELED))
-                assertThat("ID should be valid", virtualId, equalTo(View.NO_ID))
-            }
-        })
-        assertThat("Should not have auto-fill fields",
-                   countAutoFillNodes(), equalTo(0))
-
-        // Now wait for the nodes to reappear.
-        mainSession.waitForPageStop()
-        mainSession.goBack()
-        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
-            @AssertCalled(count = 4)
-            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
-                assertThat("Should be starting auto-fill", notification, equalTo(forEachCall(
-                        GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_STARTED,
-                        GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED)))
-                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
-            }
-        })
-        assertThat("Should have auto-fill fields again",
-                   countAutoFillNodes(), equalTo(14))
-        assertThat("Should not have focused field",
-                   countAutoFillNodes({ it.isFocused }), equalTo(0))
-
-        mainSession.evaluateJS("document.querySelector('#pass2').focus()")
-        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
-            @AssertCalled(count = 1)
-            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
-                assertThat("Should be entering auto-fill view",
-                           notification,
-                           equalTo(GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED))
-                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
-            }
-        })
-        assertThat("Should have one focused field",
-                   countAutoFillNodes({ it.isFocused }), equalTo(1))
-        // The focused field, its siblings, its parent, and the root node should be visible.
-        assertThat("Should have seven visible nodes",
-                   countAutoFillNodes({ node -> node.width > 0 && node.height > 0 }),
-                   equalTo(7))
-
-        mainSession.evaluateJS("document.querySelector('#pass2').blur()")
-        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
-            @AssertCalled(count = 1)
-            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
-                assertThat("Should be exiting auto-fill view",
-                           notification,
-                           equalTo(GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_EXITED))
-                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
-            }
-        })
-        assertThat("Should not have focused field",
-                   countAutoFillNodes({ it.isFocused }), equalTo(0))
-    }
-
-    @WithDisplay(height = 100, width = 100)
-    @Test fun autofill_userpass() {
-        if (Build.VERSION.SDK_INT < 26) {
-            return
-        }
-
-        mainSession.loadTestPath(FORMS2_HTML_PATH)
-        // Wait for the auto-fill nodes to populate.
-        sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
-            @AssertCalled(count = 1)
-            override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
-            }
-        })
-
-        // Perform auto-fill and return number of auto-fills performed.
-        fun checkAutoFillChild(child: MockViewNode): Int {
-            var sum = 0
-            // Seal the node info instance so we can perform actions on it.
-            if (child.children.size > 0) {
-                for (c in child.children) {
-                    sum += checkAutoFillChild(c!!)
-                }
-            }
-
-            if (child.id == View.NO_ID) {
-                return sum
-            }
-
-            assertThat("ID should be valid", child.id, not(equalTo(View.NO_ID)))
-
-            if (EditText::class.java.name == child.className) {
-                val htmlInfo = child.htmlInfo!!
-                assertThat("Should have HTML tag", htmlInfo.tag, equalTo("input"))
-
-                if (child.autofillHints == null) {
-                    return sum
-                }
-                child.autofillHints!!.forEach {
-                    when (it) {
-                        View.AUTOFILL_HINT_USERNAME, View.AUTOFILL_HINT_PASSWORD -> {
-                            sum++
-                        }
-                    }
-                }
-            }
-            return sum
-        }
-
-        val rootStructure = MockViewNode()
-
-        mainSession.provideAutofillVirtualStructure(null, rootStructure, 0)
-        // form and iframe have each 2 hints.
-        assertThat("autofill hint count",
-                   checkAutoFillChild(rootStructure), equalTo(4))
-    }
-
-    class MockViewNode : ViewStructure() {
-        private var mClassName: String? = null
-        private var mEnabled = false
-        private var mVisibility = -1
-        private var mPackageName: String? = null
-        private var mTypeName: String? = null
-        private var mEntryName: String? = null
-        private var mAutofillType = -1
-        private var mAutofillHints: Array<String>? = null
-        private var mInputType = -1
-        private var mHtmlInfo: HtmlInfo? = null
-        private var mWebDomain: String? = null
-        private var mFocused = false
-        private var mFocusable = false
-
-        var children = ArrayList<MockViewNode?>()
-        var id = View.NO_ID
-        var height = 0
-        var width = 0
-
-        val className get() = mClassName
-        val htmlInfo get() = mHtmlInfo
-        val autofillHints get() = mAutofillHints
-        val autofillType get() = mAutofillType
-        val webDomain get() = mWebDomain
-        val isEnabled get() = mEnabled
-        val isFocused get() = mFocused
-        val isFocusable get() = mFocusable
-        val visibility get() = mVisibility
-        val inputType get() = mInputType
-
-        override fun setId(id: Int, packageName: String?, typeName: String?, entryName: String?) {
-            this.id = id
-            mPackageName = packageName
-            mTypeName = typeName
-            mEntryName = entryName
-        }
-
-        override fun setHint(hint: CharSequence?) {
-            TODO("not implemented")
-        }
-
-        override fun setElevation(elevation: Float) {
-            TODO("not implemented")
-        }
-
-        override fun getText(): CharSequence {
-            TODO("not implemented")
-        }
-
-        override fun setText(text: CharSequence?) {
-            TODO("not implemented")
-        }
-
-        override fun setText(text: CharSequence?, selectionStart: Int, selectionEnd: Int) {
-            TODO("not implemented")
-        }
-
-        override fun asyncCommit() {
-            TODO("not implemented")
-        }
-
-        override fun getChildCount(): Int = children.size
-
-        override fun setEnabled(state: Boolean) {
-            mEnabled = state
-        }
-
-        override fun setLocaleList(localeList: LocaleList?) {
-            TODO("not implemented")
-        }
-
-        override fun setDimens(left: Int, top: Int, scrollX: Int, scrollY: Int, width: Int, height: Int) {
-            this.width = width
-            this.height = height
-        }
-
-        override fun setChecked(state: Boolean) {
-            TODO("not implemented")
-        }
-
-        override fun setContextClickable(state: Boolean) {
-            TODO("not implemented")
-        }
-
-        override fun setAccessibilityFocused(state: Boolean) {
-            TODO("not implemented")
-        }
-
-        override fun setAlpha(alpha: Float) {
-            TODO("not implemented")
-        }
-
-        override fun setTransformation(matrix: Matrix?) {
-            TODO("not implemented")
-        }
-
-        override fun setClassName(className: String?) {
-            mClassName = className
-        }
-
-        override fun setLongClickable(state: Boolean) {
-            TODO("not implemented")
-        }
-
-        override fun newChild(index: Int): ViewStructure {
-            val child = MockViewNode()
-            children[index] = child
-            return child
-        }
-
-        override fun getHint(): CharSequence {
-            TODO("not implemented")
-        }
-
-        override fun setInputType(inputType: Int) {
-            mInputType = inputType
-        }
-
-        override fun setWebDomain(domain: String?) {
-            mWebDomain = domain
-        }
-
-        override fun setAutofillOptions(options: Array<out CharSequence>?) {
-            TODO("not implemented")
-        }
-
-        override fun setTextStyle(size: Float, fgColor: Int, bgColor: Int, style: Int) {
-            TODO("not implemented")
-        }
-
-        override fun setVisibility(visibility: Int) {
-            mVisibility = visibility
-        }
-
-        override fun getAutofillId(): AutofillId? {
-            TODO("not implemented")
-        }
-
-        override fun setHtmlInfo(htmlInfo: HtmlInfo) {
-            mHtmlInfo = htmlInfo
-        }
-
-        override fun setTextLines(charOffsets: IntArray?, baselines: IntArray?) {
-            TODO("not implemented")
-        }
-
-        override fun getExtras(): Bundle {
-            TODO("not implemented")
-        }
-
-        override fun setClickable(state: Boolean) {
-            TODO("not implemented")
-        }
-
-        override fun newHtmlInfoBuilder(tagName: String): HtmlInfo.Builder {
-            return MockHtmlInfoBuilder(tagName)
-        }
-
-        override fun getTextSelectionEnd(): Int {
-            TODO("not implemented")
-        }
-
-        override fun setAutofillId(id: AutofillId) {
-            TODO("not implemented")
-        }
-
-        override fun setAutofillId(parentId: AutofillId, virtualId: Int) {
-            TODO("not implemented")
-        }
-
-        override fun hasExtras(): Boolean {
-            TODO("not implemented")
-        }
-
-        override fun addChildCount(num: Int): Int {
-            TODO("not implemented")
-        }
-
-        override fun setAutofillType(type: Int) {
-            mAutofillType = type
-        }
-
-        override fun setActivated(state: Boolean) {
-            TODO("not implemented")
-        }
-
-        override fun setFocused(state: Boolean) {
-            mFocused = state
-        }
-
-        override fun getTextSelectionStart(): Int {
-            TODO("not implemented")
-        }
-
-        override fun setChildCount(num: Int) {
-            children = ArrayList()
-            for (i in 0 until num) {
-                children.add(null)
-            }
-        }
-
-        override fun setAutofillValue(value: AutofillValue?) {
-            TODO("not implemented")
-        }
-
-        override fun setAutofillHints(hint: Array<String>?) {
-            mAutofillHints = hint
-        }
-
-        override fun setContentDescription(contentDescription: CharSequence?) {
-            TODO("not implemented")
-        }
-
-        override fun setFocusable(state: Boolean) {
-            mFocusable = state
-        }
-
-        override fun setCheckable(state: Boolean) {
-            TODO("not implemented")
-        }
-
-        override fun asyncNewChild(index: Int): ViewStructure {
-            TODO("not implemented")
-        }
-
-        override fun setSelected(state: Boolean) {
-            TODO("not implemented")
-        }
-
-        override fun setDataIsSensitive(sensitive: Boolean) {
-            TODO("not implemented")
-        }
-
-        override fun setOpaque(opaque: Boolean) {
-            TODO("not implemented")
-        }
-    }
-
-    class MockHtmlInfoBuilder(tagName: String) : ViewStructure.HtmlInfo.Builder() {
-        val mTagName = tagName
-        val mAttributes: MutableList<Pair<String, String>> = mutableListOf()
-
-        override fun addAttribute(name: String, value: String): ViewStructure.HtmlInfo.Builder {
-            mAttributes.add(Pair(name, value))
-            return this
-        }
-
-        override fun build(): ViewStructure.HtmlInfo {
-            return MockHtmlInfo(mTagName, mAttributes)
-        }
-    }
-
-    class MockHtmlInfo(tagName: String, attributes: MutableList<Pair<String, String>>)
-            : ViewStructure.HtmlInfo() {
-        private val mTagName = tagName
-        private val mAttributes = attributes
-
-        override fun getTag() = mTagName
-        override fun getAttributes() = mAttributes
-    }
-
     private fun goFullscreen() {
         sessionRule.setPrefsUntilTestEnd(mapOf("full-screen-api.allow-trusted-requests-only" to false))
         mainSession.loadTestPath(FULLSCREEN_PATH)
         mainSession.waitForPageStop()
         mainSession.evaluateJS("document.querySelector('#fullscreen').requestFullscreen(); true")
         sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
             override  fun onFullScreen(session: GeckoSession, fullScreen: Boolean) {
                 assertThat("Div went fullscreen", fullScreen, equalTo(true))
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/AutofillElement.java
@@ -0,0 +1,241 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.geckoview;
+
+import android.graphics.Rect;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.v4.util.ArrayMap;
+import android.view.View;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.Map;
+
+/**
+ * Represents a single autofill element.
+ */
+public class AutofillElement {
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({HINT_NONE, HINT_EMAIL_ADDRESS, HINT_PASSWORD, HINT_URL, HINT_USERNAME})
+    /* package */ @interface AutofillHint {}
+
+    /**
+     * Hint indicating that no special handling is required.
+     */
+    public static final int HINT_NONE = -1;
+
+    /**
+     * Hint indicating that an element represents an email address.
+     */
+    public static final int HINT_EMAIL_ADDRESS = 0;
+
+    /**
+     * Hint indicating that an element represents a password.
+     */
+    public static final int HINT_PASSWORD = 1;
+
+    /**
+     * Hint indicating that an element represents an URL.
+     */
+    public static final int HINT_URL = 2;
+
+    /**
+     * Hint indicating that an element represents a username.
+     */
+    public static final int HINT_USERNAME = 3;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({INPUT_TYPE_NONE, INPUT_TYPE_TEXT, INPUT_TYPE_NUMBER, INPUT_TYPE_PHONE})
+    /* package */ @interface AutofillInputType {}
+
+    /**
+     * Indicates that an element is not a known input type.
+     */
+    public static final int INPUT_TYPE_NONE = -1;
+
+    /**
+     * Indicates that an element is a text input type (e.g., {@code <input type="text">})
+     */
+    public static final int INPUT_TYPE_TEXT = 0;
+
+    /**
+     * Indicates that an element is a number input type (e.g., {@code <input type="number">})
+     */
+    public static final int INPUT_TYPE_NUMBER = 1;
+
+    /**
+     * Indicates that an element is a phone input type (e.g., {@code <input type="tel">})
+     */
+    public static final int INPUT_TYPE_PHONE = 2;
+
+    /**
+     * A unique (within this page) id for this element.
+     */
+    public final int id;
+
+    /**
+     * The dimensions of this element in CSS coordinates.
+     */
+    public final @NonNull Rect dimensions;
+
+    /**
+     * The list of child elements for this element.
+     */
+    public final @NonNull Iterable<AutofillElement> children;
+
+    /**
+     * The HTML attributes for this element.
+     */
+    public final @NonNull Map<String, String> attributes;
+
+    /**
+     * Whether or not this element is enabled.
+     */
+    public final boolean enabled;
+
+    /**
+     * Whether or not this element is focusable.
+     */
+    public final boolean focusable;
+
+    /**
+     * Whether or not this element is focused.
+     */
+    public final boolean focused;
+
+    /**
+     * A hint for the type of data contained in this element, if any.
+     */
+    public final @AutofillHint int hint;
+
+    /**
+     * The input type of this element, if any.
+     */
+    public final @AutofillInputType int inputType;
+
+    /**
+     * The HTML tag for this element.
+     */
+    public final @NonNull String tag;
+
+    /**
+     * The web domain for this element.
+     */
+    public final @NonNull String domain;
+
+    private AutofillElement(final Builder builder) {
+        id = builder.mId;
+        dimensions = builder.mDimensions != null ? builder.mDimensions : new Rect(0, 0, 0, 0);
+        attributes = Collections.unmodifiableMap(builder.mAttributes != null ? builder.mAttributes : new ArrayMap<>());
+        enabled = builder.mEnabled;
+        focusable = builder.mFocusable;
+        focused = builder.mFocused;
+        hint = builder.mHint;
+        inputType = builder.mInputType;
+        tag = builder.mTag;
+        domain = builder.mDomain;
+
+        if (builder.mChildren != null) {
+            LinkedList<AutofillElement> children = new LinkedList<>();
+            for (Builder child : builder.mChildren) {
+                children.add(child.build());
+            }
+            this.children = children;
+        } else {
+            this.children = new LinkedList<>();
+        }
+    }
+
+    protected AutofillElement() {
+        id = View.NO_ID;
+        dimensions = new Rect(0, 0, 0, 0);
+        attributes = Collections.unmodifiableMap(new ArrayMap<>());
+        enabled = false;
+        focusable = false;
+        focused = false;
+        hint = HINT_NONE;
+        inputType = INPUT_TYPE_NONE;
+        tag = "";
+        domain = "";
+        children = new LinkedList<>();
+    }
+
+    /* package */ static class Builder {
+        private int mId = View.NO_ID;
+        private Rect mDimensions;
+        private LinkedList<Builder> mChildren;
+        private ArrayMap<String, String> mAttributes;
+        private boolean mEnabled;
+        private boolean mFocusable;
+        private boolean mFocused;
+        private int mHint = HINT_NONE;
+        private int mInputType = INPUT_TYPE_NONE;
+        private String mTag = "";
+        private String mDomain = "";
+
+        public void dimensions(final Rect rect) {
+            mDimensions = rect;
+        }
+
+        public AutofillElement build() {
+            return new AutofillElement(this);
+        }
+
+        public void id(final int id) {
+            mId = id;
+        }
+
+        public Builder child() {
+            if (mChildren == null) {
+                mChildren = new LinkedList<>();
+            }
+
+            final Builder child = new Builder();
+            mChildren.add(child);
+            return child;
+        }
+
+        public void attribute(final String key, final String value) {
+            if (mAttributes == null) {
+                mAttributes = new ArrayMap<>();
+            }
+
+            mAttributes.put(key, value);
+        }
+
+        public void enabled(final boolean enabled) {
+            mEnabled = enabled;
+        }
+
+        public void focusable(final boolean focusable) {
+            mFocusable = focusable;
+        }
+
+        public void focused(final boolean focused) {
+            mFocused = focused;
+        }
+
+        public void hint(final int hint) {
+            mHint = hint;
+        }
+
+        public void inputType(final int inputType) {
+            mInputType = inputType;
+        }
+
+        public void tag(final String tag) {
+            mTag = tag;
+        }
+
+        public void domain(final String domain) {
+            mDomain = domain;
+        }
+    }
+}
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/AutofillSupport.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/AutofillSupport.java
@@ -1,29 +1,21 @@
 package org.mozilla.geckoview;
 
-import android.annotation.TargetApi;
 import android.graphics.Rect;
-import android.os.Build;
-import android.support.annotation.AnyThread;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import android.support.annotation.UiThread;
-import android.text.InputType;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.View;
-import android.view.ViewStructure;
 
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 
-import java.util.Locale;
-
 /* package */ class AutofillSupport {
     private static final String LOGTAG = "AutofillSupport";
     private static final boolean DEBUG = false;
 
     private final GeckoSession mSession;
     private GeckoSession.AutofillDelegate mDelegate;
     private SparseArray<GeckoBundle> mAutoFillNodes;
     private SparseArray<EventCallback> mAutoFillRoots;
@@ -94,24 +86,115 @@ import java.util.Locale;
             response.putString(String.valueOf(id), String.valueOf(value));
         }
 
         if (callback != null) {
             callback.sendSuccess(response);
         }
     }
 
-    public void setDelegate(final GeckoSession.AutofillDelegate delegate) {
+    public void setDelegate(final @Nullable GeckoSession.AutofillDelegate delegate) {
         mDelegate = delegate;
     }
 
-    public GeckoSession.AutofillDelegate getDelegate() {
+    public @Nullable GeckoSession.AutofillDelegate getDelegate() {
         return mDelegate;
     }
 
+    public @NonNull AutofillElement getAutofillElements() {
+        final AutofillElement.Builder builder = new AutofillElement.Builder();
+
+        final Rect rect = getDummyAutoFillRect(mSession, false, null);
+        builder.dimensions(rect);
+
+        if (mAutoFillRoots != null) {
+            final int size = mAutoFillRoots.size();
+            for (int i = 0; i < size; i++) {
+                final int id = mAutoFillRoots.keyAt(i);
+                final GeckoBundle root = mAutoFillNodes.get(id);
+                fillAutofillElement(id, root, rect, builder.child());
+            }
+        }
+
+        return builder.build();
+    }
+
+    private void fillAutofillElement(final int id, final GeckoBundle bundle, final Rect rect, final AutofillElement.Builder builder) {
+        builder.id(id);
+        builder.domain(bundle.getString("origin"));
+
+        if (mAutoFillFocusedRoot != View.NO_ID && mAutoFillFocusedRoot == bundle.getInt("root", View.NO_ID)) {
+            builder.dimensions(rect);
+        }
+
+        final GeckoBundle[] children = bundle.getBundleArray("children");
+        if (children != null) {
+            for (final GeckoBundle childBundle : children) {
+                final int childId = childBundle.getInt("id");
+                fillAutofillElement(childId, childBundle, rect, builder.child());
+                mAutoFillNodes.append(childId, childBundle);
+            }
+        }
+
+        String tag = bundle.getString("tag", "");
+        builder.tag(tag.toLowerCase());
+
+        final String type = bundle.getString("type", "text");
+
+        final GeckoBundle attrs = bundle.getBundle("attributes");
+        for (final String key : attrs.keys()) {
+            builder.attribute(key, String.valueOf(attrs.get(key)));
+        }
+
+        if ("INPUT".equals(tag) && !bundle.getBoolean("editable", false)) {
+            tag = ""; // Don't process non-editable inputs (e.g. type="button").
+        }
+
+        switch (tag) {
+            case "INPUT":
+            case "TEXTAREA": {
+                final boolean disabled = bundle.getBoolean("disabled");
+
+                builder.enabled(!disabled);
+                builder.focusable(!disabled);
+                builder.focused(id == mAutoFillFocusedId);
+                break;
+            }
+            default:
+                break;
+        }
+
+        switch (type) {
+            case "email":
+                builder.hint(AutofillElement.HINT_EMAIL_ADDRESS);
+                builder.inputType(AutofillElement.INPUT_TYPE_TEXT);
+                break;
+            case "number":
+                builder.inputType(AutofillElement.INPUT_TYPE_NUMBER);
+                break;
+            case "password":
+                builder.hint(AutofillElement.HINT_PASSWORD);
+                builder.inputType(AutofillElement.INPUT_TYPE_TEXT);
+                break;
+            case "tel":
+                builder.inputType(AutofillElement.INPUT_TYPE_PHONE);
+                break;
+            case "url":
+                builder.hint(AutofillElement.HINT_URL);
+                builder.inputType(AutofillElement.INPUT_TYPE_TEXT);
+                break;
+            case "text":
+                final String autofillhint = bundle.getString("autofillhint", "");
+                if (autofillhint.equals("username")) {
+                    builder.hint(AutofillElement.HINT_USERNAME);
+                    builder.inputType(AutofillElement.INPUT_TYPE_TEXT);
+                }
+                break;
+        }
+    }
 
     /* package */ void addAutoFill(@NonNull final GeckoBundle message,
                                    @NonNull final EventCallback callback) {
         final boolean initializing;
         if (mAutoFillRoots == null) {
             mAutoFillRoots = new SparseArray<>();
             mAutoFillNodes = new SparseArray<>();
             initializing = true;
@@ -202,182 +285,37 @@ import java.util.Locale;
 
         if (mDelegate != null && mAutoFillFocusedId != View.NO_ID) {
             mDelegate.onAutofill(
                     mSession, GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED,
                     mAutoFillFocusedId);
         }
     }
 
-
-    /**
-     * Fill the specified {@link ViewStructure} with auto-fill fields from the current page.
-     *
-     * @param structure Structure to be filled.
-     * @param flags     Zero or a combination of {@link View#AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
-     *                  AUTOFILL_FLAG_*} constants.
-     */
-    @TargetApi(23)
-    @UiThread
-    public void provideAutofillVirtualStructure(@Nullable final View view,
-                                                @NonNull final ViewStructure structure,
-                                                final int flags) {
-        if (view != null) {
-            structure.setClassName(view.getClass().getName());
-        }
-        structure.setEnabled(true);
-        structure.setVisibility(View.VISIBLE);
-
-        final Rect rect = getDummyAutoFillRect(mSession, false, null);
-        structure.setDimens(rect.left, rect.top, 0, 0, rect.width(), rect.height());
-
-        if (mAutoFillRoots == null) {
-            structure.setChildCount(0);
-            return;
-        }
-
-        final int size = mAutoFillRoots.size();
-        structure.setChildCount(size);
-
-        for (int i = 0; i < size; i++) {
-            final int id = mAutoFillRoots.keyAt(i);
-            final GeckoBundle root = mAutoFillNodes.get(id);
-            fillAutoFillStructure(view, id, root, structure.newChild(i), rect);
-        }
-    }
-
-    @TargetApi(23)
-    private void fillAutoFillStructure(@Nullable final View view, final int id,
-                                       @NonNull final GeckoBundle bundle,
-                                       @NonNull final ViewStructure structure,
-                                       @NonNull final Rect rect) {
-        if (mAutoFillRoots == null) {
-            return;
-        }
-
-        if (DEBUG) {
-            Log.d(LOGTAG, "fillAutoFillStructure(" + id + ')');
-        }
-
-        if (Build.VERSION.SDK_INT >= 26) {
-            if (view != null) {
-                structure.setAutofillId(view.getAutofillId(), id);
-            }
-            structure.setWebDomain(bundle.getString("origin"));
-        }
-        structure.setId(id, null, null, null);
-
-        if (mAutoFillFocusedRoot != View.NO_ID &&
-                mAutoFillFocusedRoot == bundle.getInt("root", View.NO_ID)) {
-            structure.setDimens(0, 0, 0, 0, rect.width(), rect.height());
-        }
-
-        final GeckoBundle[] children = bundle.getBundleArray("children");
-        if (children != null) {
-            structure.setChildCount(children.length);
-            for (int i = 0; i < children.length; i++) {
-                final GeckoBundle childBundle = children[i];
-                final int childId = childBundle.getInt("id");
-                final ViewStructure childStructure = structure.newChild(i);
-                fillAutoFillStructure(view, childId, childBundle, childStructure, rect);
-                mAutoFillNodes.append(childId, childBundle);
-            }
-        }
-
-        String tag = bundle.getString("tag", "");
-        final String type = bundle.getString("type", "text");
-
-        if (Build.VERSION.SDK_INT >= 26) {
-            final GeckoBundle attrs = bundle.getBundle("attributes");
-            final ViewStructure.HtmlInfo.Builder builder =
-                    structure.newHtmlInfoBuilder(tag.toLowerCase(Locale.US));
-            for (final String key : attrs.keys()) {
-                builder.addAttribute(key, String.valueOf(attrs.get(key)));
-            }
-            structure.setHtmlInfo(builder.build());
-        }
-
-        if ("INPUT".equals(tag) && !bundle.getBoolean("editable", false)) {
-            tag = ""; // Don't process non-editable inputs (e.g. type="button").
-        }
-        switch (tag) {
-            case "INPUT":
-            case "TEXTAREA": {
-                final boolean disabled = bundle.getBoolean("disabled");
-                structure.setClassName("android.widget.EditText");
-                structure.setEnabled(!disabled);
-                structure.setFocusable(!disabled);
-                structure.setFocused(id == mAutoFillFocusedId);
-                structure.setVisibility(View.VISIBLE);
-
-                if (Build.VERSION.SDK_INT >= 26) {
-                    structure.setAutofillType(View.AUTOFILL_TYPE_TEXT);
-                }
-                break;
-            }
-            default:
-                if (children != null) {
-                    structure.setClassName("android.view.ViewGroup");
-                } else {
-                    structure.setClassName("android.view.View");
-                }
-                break;
-        }
-
-        if (Build.VERSION.SDK_INT >= 26 && "INPUT".equals(tag)) {
-            // LastPass will fill password to the feild that setAutofillHints is unset and setInputType is set.
-            switch (type) {
-                case "email":
-                    structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_EMAIL_ADDRESS });
-                    structure.setInputType(InputType.TYPE_CLASS_TEXT |
-                                           InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS);
-                    break;
-                case "number":
-                    structure.setInputType(InputType.TYPE_CLASS_NUMBER);
-                    break;
-                case "password":
-                    structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_PASSWORD });
-                    structure.setInputType(InputType.TYPE_CLASS_TEXT |
-                                           InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD);
-                    break;
-                case "tel":
-                    structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_PHONE });
-                    structure.setInputType(InputType.TYPE_CLASS_PHONE);
-                    break;
-                case "url":
-                    structure.setInputType(InputType.TYPE_CLASS_TEXT |
-                                           InputType.TYPE_TEXT_VARIATION_URI);
-                    break;
-                case "text":
-                    final String autofillhint = bundle.getString("autofillhint", "");
-                    if (autofillhint.equals("username")) {
-                        structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_USERNAME });
-                        structure.setInputType(InputType.TYPE_CLASS_TEXT |
-                                               InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
-                    }
-                    break;
-            }
-        }
-    }
-
     /* package */ static Rect getDummyAutoFillRect(@NonNull final GeckoSession session,
                                                    final boolean screen,
                                                    @Nullable final View view) {
         final Rect rect = new Rect();
         session.getSurfaceBounds(rect);
         if (screen) {
             if (view == null) {
                 throw new IllegalArgumentException();
             }
             final int[] offset = new int[2];
             view.getLocationOnScreen(offset);
             rect.offset(offset[0], offset[1]);
         }
         return rect;
     }
 
-    public void onScreenMetricsUpdated() {
-        if (mDelegate != null && mAutoFillFocusedId != View.NO_ID) {
-            getDelegate().onAutofill(
-                    mSession, GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED, mAutoFillFocusedId);
+    public void onActiveChanged(final boolean active) {
+        if (mDelegate == null || mAutoFillFocusedId == View.NO_ID) {
+            return;
         }
+
+        // We blur/focus the active element (if we have one) when the document is made inactive/active.
+        getDelegate().onAutofill(
+                mSession,
+                active ? GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED
+                : GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_EXITED,
+                mAutoFillFocusedId);
     }
 }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -29,17 +29,16 @@ import org.mozilla.gecko.IGeckoEditableP
 import org.mozilla.gecko.mozglue.JNIObject;
 import org.mozilla.gecko.NativeQueue;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.IntentUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
-import android.annotation.TargetApi;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.net.Uri;
@@ -2034,16 +2033,18 @@ public class GeckoSession implements Par
     public void setActive(final boolean active) {
         final GeckoBundle msg = new GeckoBundle(1);
         msg.putBoolean("active", active);
         mEventDispatcher.dispatch("GeckoView:SetActive", msg);
 
         if (!active) {
             mEventDispatcher.dispatch("GeckoView:FlushSessionState", null);
         }
+
+        getAutofillSupport().onActiveChanged(active);
     }
 
     /**
      * Move focus to this session or away from this session. Only one session has focus at
      * a given time. Note that a session can be unfocused but still active (i.e. visible).
      *
      * @param focused True if the session should gain focus or
      *                false if the session should lose focus.
@@ -5328,18 +5329,16 @@ public class GeckoSession implements Par
                                         final float zoom) {
         if (DEBUG) {
             ThreadUtils.assertOnUiThread();
         }
 
         mViewportLeft = scrollX;
         mViewportTop = scrollY;
         mViewportZoom = zoom;
-
-        getAutofillSupport().onScreenMetricsUpdated();
     }
 
     /* protected */ void onWindowBoundsChanged() {
         if (DEBUG) {
             ThreadUtils.assertOnUiThread();
         }
 
         final int toolbarHeight;
@@ -5669,16 +5668,21 @@ public class GeckoSession implements Par
      *
      * @param values Map of auto-fill IDs to values.
      */
     @UiThread
     public void autofill(final @NonNull SparseArray<CharSequence> values) {
         getAutofillSupport().autofill(values);
     }
 
-    @TargetApi(23)
+    /**
+     * Provides an autofill structure similar to {@link View#onProvideAutofillVirtualStructure(ViewStructure, int)} , but
+     * does not rely on {@link ViewStructure} to build the tree. This is useful for apps that want
+     * to provide autofill functionality without using the Android autofill system or requiring
+     * API 26.
+     *
+     * @return The elements available for autofill.
+     */
     @UiThread
-    public void provideAutofillVirtualStructure(@Nullable final View view,
-                                                @NonNull final ViewStructure structure,
-                                                final int flags) {
-        getAutofillSupport().provideAutofillVirtualStructure(view, structure, flags);
+    public @NonNull AutofillElement getAutofillElements() {
+        return getAutofillSupport().getAutofillElements();
     }
 }