Bug 1770010 - Keep reference to autofill session instead of using mSession. r=jonalmeida,calu

The android autofill framework requests a virtual structure for the webpage in
onProvideAutofillVirtualStructure and then at a later time will call autofill
when the user selects a value in the autofill app.

These method calls happen on the GeckoView, but our data is stored in the
GeckoSession (or rather, in the window that the GeckoSession represents).

Because of the asynchronicity of this process, we're not guaranteed that the
app hasn't switched the sesssion behind us in between the
onProvideAutofillVirtualStructure and autofill call, so we need to keep a
reference to the current autofill session when onProvide is called so that
we're sure that we are autofilling the right session.

We use a WeakReference to avoid keeping a window alive more than necessary, as
if the window is unloaded, there is no point in autofilling anyway.

Differential Revision: https://phabricator.services.mozilla.com/D146724
This commit is contained in:
Agi Sferro 2022-05-19 22:07:38 +00:00
Родитель b4111998a3
Коммит 31f8c98911
7 изменённых файлов: 288 добавлений и 58 удалений

Просмотреть файл

@ -374,6 +374,7 @@ package org.mozilla.geckoview {
}
public static final class Autofill.Session {
method @UiThread public void autofill(@NonNull SparseArray<CharSequence>);
method @NonNull @UiThread public Autofill.NodeData dataFor(@NonNull Autofill.Node);
method @UiThread public void fillViewStructure(@NonNull View, @NonNull ViewStructure, int);
method @UiThread public void fillViewStructure(@NonNull Autofill.Node, @NonNull View, @NonNull ViewStructure, int);
@ -886,7 +887,7 @@ package org.mozilla.geckoview {
ctor public GeckoSession();
ctor public GeckoSession(@Nullable GeckoSessionSettings);
method @NonNull @UiThread public GeckoDisplay acquireDisplay();
method @UiThread public void autofill(@NonNull SparseArray<CharSequence>);
method @Deprecated @DeprecationSchedule(id="autofill-node",version=104) @UiThread public void autofill(@NonNull SparseArray<CharSequence>);
method @UiThread public void close();
method @AnyThread public void exitFullScreen();
method @NonNull @UiThread public SessionAccessibility getAccessibility();

Просмотреть файл

@ -219,7 +219,7 @@ class AutofillDelegateTest : BaseSessionTest() {
val nodes = mainSession.autofillSession.root
checkAutofillChild(nodes, "")
mainSession.autofill(autofillValues)
mainSession.autofillSession.autofill(autofillValues)
// Wait on the promises and check for correct values.
for (values in promises.map { it.value.asJsonArray() }) {
@ -252,7 +252,7 @@ class AutofillDelegateTest : BaseSessionTest() {
val autofillValues = SparseArray<CharSequence>()
autofillValues.append(-1, "lobster")
mainSession.autofill(autofillValues)
mainSession.autofillSession.autofill(autofillValues)
}
private fun countAutofillNodes(cond: (Autofill.Node) -> Boolean =

Просмотреть файл

@ -1,9 +1,21 @@
package org.mozilla.geckoview.test
import android.graphics.Matrix
import android.os.Build
import android.os.Bundle
import android.os.LocaleList
import android.text.InputType
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 androidx.test.filters.LargeTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.core.view.ViewCompat
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.filters.SdkSuppress
import org.hamcrest.Matchers.equalTo
import org.junit.*
@ -228,4 +240,204 @@ class GeckoViewTest : BaseSessionTest() {
high = listOf(mainSession), low = listOf(otherSession)
)
}
private fun visit(node: MockViewStructure, callback: (MockViewStructure) -> Unit) {
callback(node)
for (child in node.children) {
if (child != null) {
visit(child, callback)
}
}
}
@Test
@NullDelegate(Autofill.Delegate::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun autofillWithNoSession() {
mainSession.loadTestPath(FORMS_XORIGIN_HTML_PATH)
mainSession.waitForPageStop()
val autofills = mapOf(
"#user1" to "username@example.com",
"#user2" to "username@example.com",
"#pass1" to "test-password",
"#pass2" to "test-password")
// Set up promises to monitor the values changing.
val promises = autofills.map { entry ->
// Repeat each test with both the top document and the iframe document.
mainSession.evaluatePromiseJS("""
window.getDataForAllFrames('${entry.key}', '${entry.value}')
""")
}
activityRule.scenario.onActivity {
val root = MockViewStructure(View.NO_ID)
it.view.onProvideAutofillVirtualStructure(root, 0)
val data = SparseArray<AutofillValue>()
visit(root) { node ->
if (node.hints?.indexOf(View.AUTOFILL_HINT_USERNAME) != -1) {
data.set(node.id, AutofillValue.forText("username@example.com"))
} else if (node.hints?.indexOf(View.AUTOFILL_HINT_PASSWORD) != -1) {
data.set(node.id, AutofillValue.forText("test-password"))
}
}
// Releasing the session will set mSession in GeckoView to null
// this test verifies that we can still autofill correctly even in released state
val session = it.view.releaseSession()!!
it.view.autofill(data)
// Put back the session and verifies that the autofill went through anyway
it.view.setSession(session)
// Wait on the promises and check for correct values.
for (values in promises.map { p -> p.value.asJsonArray() }) {
for (i in 0 until values.length()) {
val (key, actual, expected, eventInterface) = values.get(i).asJSList<String>()
assertThat("Auto-filled value must match ($key)", actual, equalTo(expected))
assertThat(
"input event should be dispatched with InputEvent interface",
eventInterface,
equalTo("InputEvent")
)
}
}
}
}
class MockViewStructure(var id: Int, var parent: MockViewStructure? = null) : ViewStructure() {
private var enabled: Boolean = false
private var inputType = 0
var children = Array<MockViewStructure?>(0, { null })
var childIndex = 0
var hints : Array<out String>? = null
override fun setId(p0: Int, p1: String?, p2: String?, p3: String?) {
id = p0
}
override fun setEnabled(p0: Boolean) {
enabled = p0
}
override fun setChildCount(p0: Int) {
children = Array(p0, { null })
}
override fun getChildCount(): Int {
return children.size
}
override fun newChild(p0: Int): ViewStructure {
val child = MockViewStructure(p0, this)
children[childIndex++] = child
return child
}
override fun asyncNewChild(p0: Int): ViewStructure {
return newChild(p0)
}
override fun setInputType(p0: Int) {
inputType = p0
}
fun getInputType() : Int {
return inputType
}
override fun setAutofillHints(p0: Array<out String>?) {
hints = p0
}
override fun addChildCount(p0: Int): Int {
TODO()
}
override fun setDimens(p0: Int, p1: Int, p2: Int, p3: Int, p4: Int, p5: Int) {}
override fun setTransformation(p0: Matrix?) {}
override fun setElevation(p0: Float) {}
override fun setAlpha(p0: Float) {}
override fun setVisibility(p0: Int) {}
override fun setClickable(p0: Boolean) {}
override fun setLongClickable(p0: Boolean) {}
override fun setContextClickable(p0: Boolean) {}
override fun setFocusable(p0: Boolean) {}
override fun setFocused(p0: Boolean) {}
override fun setAccessibilityFocused(p0: Boolean) {}
override fun setCheckable(p0: Boolean) {}
override fun setChecked(p0: Boolean) {}
override fun setSelected(p0: Boolean) {}
override fun setActivated(p0: Boolean) {}
override fun setOpaque(p0: Boolean) {}
override fun setClassName(p0: String?) {}
override fun setContentDescription(p0: CharSequence?) {}
override fun setText(p0: CharSequence?) {}
override fun setText(p0: CharSequence?, p1: Int, p2: Int) {}
override fun setTextStyle(p0: Float, p1: Int, p2: Int, p3: Int) {}
override fun setTextLines(p0: IntArray?, p1: IntArray?) {}
override fun setHint(p0: CharSequence?) {}
override fun getText(): CharSequence {
return ""
}
override fun getTextSelectionStart(): Int {
return 0
}
override fun getTextSelectionEnd(): Int {
return 0
}
override fun getHint(): CharSequence {
return ""
}
override fun getExtras(): Bundle {
return Bundle()
}
override fun hasExtras(): Boolean {
return false
}
override fun getAutofillId(): AutofillId? {
return null
}
override fun setAutofillId(p0: AutofillId) {}
override fun setAutofillId(p0: AutofillId, p1: Int) {}
override fun setAutofillType(p0: Int) {}
override fun setAutofillValue(p0: AutofillValue?) {}
override fun setAutofillOptions(p0: Array<out CharSequence>?) {}
override fun setDataIsSensitive(p0: Boolean) {}
override fun asyncCommit() {}
override fun setWebDomain(p0: String?) {}
override fun setLocaleList(p0: LocaleList?) {}
override fun newHtmlInfoBuilder(p0: String): HtmlInfo.Builder {
return MockHtmlInfoBuilder()
}
override fun setHtmlInfo(p0: HtmlInfo) {
}
}
class MockHtmlInfoBuilder : ViewStructure.HtmlInfo.Builder() {
override fun addAttribute(p0: String, p1: String): ViewStructure.HtmlInfo.Builder {
return this
}
override fun build(): ViewStructure.HtmlInfo {
return MockHtmlInfo()
}
}
class MockHtmlInfo : ViewStructure.HtmlInfo() {
override fun getTag(): String {
TODO("Not yet implemented")
}
override fun getAttributes(): MutableList<Pair<String, String>>? {
TODO("Not yet implemented")
}
}
}

Просмотреть файл

@ -257,6 +257,57 @@ public class Autofill {
return data;
}
/**
* Perform auto-fill using the specified values.
*
* @param values Map of auto-fill IDs to values.
*/
@UiThread
public void autofill(@NonNull final SparseArray<CharSequence> values) {
ThreadUtils.assertOnUiThread();
if (isEmpty()) {
return;
}
final HashMap<Node, GeckoBundle> valueBundles = new HashMap<>();
for (int i = 0; i < values.size(); i++) {
final int id = values.keyAt(i);
final Node node = getNode(id);
if (node == null) {
Log.w(LOGTAG, "Could not find node id=" + id);
continue;
}
final CharSequence value = values.valueAt(i);
if (DEBUG) {
Log.d(LOGTAG, "Process autofill for id=" + id + ", value=" + value);
}
if (node == getRoot()) {
// We cannot autofill the session root as it does not correspond to a
// real element on the page.
Log.w(LOGTAG, "Ignoring autofill on session root.");
continue;
}
final Node root = node.getRoot();
if (!valueBundles.containsKey(root)) {
valueBundles.put(root, new GeckoBundle());
}
valueBundles.get(root).putString(node.getUuid(), String.valueOf(value));
}
for (final Node root : valueBundles.keySet()) {
final NodeData data = dataFor(root);
Objects.requireNonNull(data);
final EventCallback callback = data.callback;
callback.sendSuccess(valueBundles.get(root));
}
}
/* package */ void addRoot(@NonNull final Node node, final EventCallback callback) {
if (DEBUG) {
Log.d(LOGTAG, "addRoot: " + node);
@ -1085,57 +1136,6 @@ public class Autofill {
}
}
/**
* Perform auto-fill using the specified values.
*
* @param values Map of auto-fill IDs to values.
*/
@UiThread
public void autofill(final SparseArray<CharSequence> values) {
ThreadUtils.assertOnUiThread();
if (getAutofillSession().isEmpty()) {
return;
}
final HashMap<Node, GeckoBundle> valueBundles = new HashMap<>();
for (int i = 0; i < values.size(); i++) {
final int id = values.keyAt(i);
final Node node = getAutofillSession().getNode(id);
if (node == null) {
Log.w(LOGTAG, "Could not find node id=" + id);
continue;
}
final CharSequence value = values.valueAt(i);
if (DEBUG) {
Log.d(LOGTAG, "Process autofill for id=" + id + ", value=" + value);
}
if (node == getAutofillSession().getRoot()) {
// We cannot autofill the session root as it does not correspond to a
// real element on the page.
Log.w(LOGTAG, "Ignoring autofill on session root.");
continue;
}
final Node root = node.getRoot();
if (!valueBundles.containsKey(root)) {
valueBundles.put(root, new GeckoBundle());
}
valueBundles.get(root).putString(node.getUuid(), String.valueOf(value));
}
for (final Node root : valueBundles.keySet()) {
final NodeData data = getAutofillSession().dataFor(root);
Objects.requireNonNull(data);
final EventCallback callback = data.callback;
callback.sendSuccess(valueBundles.get(root));
}
}
@UiThread
public void setDelegate(final @Nullable Delegate delegate) {
ThreadUtils.assertOnUiThread();

Просмотреть файл

@ -6482,10 +6482,13 @@ public class GeckoSession {
* Perform autofill using the specified values.
*
* @param values Map of autofill IDs to values.
* @deprecated Use {@link Autofill.Session#autofill} instead.
*/
@UiThread
@Deprecated
@DeprecationSchedule(id = "autofill-node", version = 104)
public void autofill(final @NonNull SparseArray<CharSequence> values) {
getAutofillSupport().autofill(values);
getAutofillSession().autofill(values);
}
/**

Просмотреть файл

@ -49,6 +49,7 @@ import androidx.annotation.UiThread;
import androidx.core.view.ViewCompat;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.Objects;
import org.mozilla.gecko.AndroidGamepadManager;
import org.mozilla.gecko.EventDispatcher;
@ -65,6 +66,8 @@ public class GeckoView extends FrameLayout {
private Integer mLastCoverColor;
protected @Nullable GeckoSession mSession;
WeakReference<Autofill.Session> mAutofillSession = new WeakReference<>(null);
// Whether this GeckoView instance has a session that is no longer valid, e.g. because the session
// associated to this GeckoView was attached to a different GeckoView instance.
private boolean mIsSessionPoisoned = false;
@ -852,13 +855,20 @@ public class GeckoView extends FrameLayout {
}
final Autofill.Session autofillSession = mSession.getAutofillSession();
// Let's store the session here in case we need to autofill it later
mAutofillSession = new WeakReference<>(autofillSession);
autofillSession.fillViewStructure(this, structure, flags);
}
@Override
@TargetApi(26)
public void autofill(@NonNull final SparseArray<AutofillValue> values) {
if (mSession == null) {
// Note: we can't use mSession.getAutofillSession() because the app might have swapped
// the session under us between the onProvideAutofillVirtualStructure and this call
// so mSession could refer to a different session or we might not have a session at all.
final Autofill.Session session = mAutofillSession.get();
if (session == null) {
return;
}
final SparseArray<CharSequence> strValues = new SparseArray<>(values.size());
@ -869,7 +879,7 @@ public class GeckoView extends FrameLayout {
strValues.put(values.keyAt(i), value.getTextValue());
}
}
mSession.autofill(strValues);
session.autofill(strValues);
}
@Override

Просмотреть файл

@ -28,6 +28,8 @@ exclude: true
[`onSessionStart`][102.16].
- Added [`PromptInstanceDelegate.onPromptUpdate`][102.17] to allow GeckoView to update current prompts.
([bug 1758800]({{bugzilla}}1758800))
- Deprecated [`GeckoSession.autofill`][102.18], use [`Autofill.Session.autofill`][102.19] instead.
([bug 1770010]({{bugzilla}}1770010))
[102.1]: {{javadoc_uri}}/GeckoSession.PromptDelegate.DateTimePrompt.html#stepValue
[102.2]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#step
@ -46,6 +48,8 @@ exclude: true
[102.15]: {{javadoc_uri}}/Autofill.Delegate.html#onSessionCommit(org.mozilla.geckoview.GeckoSession,org.mozilla.geckoview.Autofill.Node,org.mozilla.geckoview.Autofill.NodeData)
[102.16]: {{javadoc_uri}}/Autofill.Delegate.html#onSessionStart(org.mozilla.geckoview.GeckoSession)
[102.17]: {{javadoc_uri}}/GeckoSession.PromptDelegate.PromptInstanceDelegate.html#onPromptUpdate(org.mozilla.geckoview.GeckoSession.PromptDelegate.BasePrompt)
[102.18]: {{javadoc_uri}}/GeckoSession.html#autofill(android.util.SparseArray)
[102.19]: {{javadoc_uri}}/Autofill.Session.html#autofill(android.util.SparseArray)
## v101
- Added [`GeckoDisplay.surfaceChanged`][101.1] function taking new type [`GeckoDisplay.SurfaceInfo`][101.2].
@ -1200,4 +1204,4 @@ to allow adding gecko profiler markers.
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport(android.content.Context,android.os.Bundle,java.lang.String)
[65.25]: {{javadoc_uri}}/GeckoResult.html
[api-version]: 1d08fc97ff3a0f8c0fc6c90b1ec1829da437ad90
[api-version]: dc9516b62971d881de4059eae2ea1b88a1ebb3d1