Bug 1577003 - Handle Android autofill service in GeckoView class r=geckoview-reviewers,esawin

This also adds `GeckoView.setAutofillEnabled()`, which is enabled by
default. Apps can disable this if they don't want to use the Android
autofill service. This will allow them to set their own
`AutofillDelegate` on the `GeckoSession` and provide their own
autofill implementation.

Differential Revision: https://phabricator.services.mozilla.com/D47483

--HG--
extra : moz-landing-system : lando
This commit is contained in:
James Willcox 2019-10-04 17:54:39 +00:00
Родитель e1ff7e07eb
Коммит e4bf4e9cdc
2 изменённых файлов: 214 добавлений и 2 удалений

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

@ -286,6 +286,8 @@ class SessionLifecycleTest : BaseSessionTest() {
ontoSession: GeckoSession?) =
GeckoView(InstrumentationRegistry.getTargetContext()).apply {
id = 0
autofillEnabled = false
if (fromSession != null) {
setSession(fromSession)
}

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

@ -20,7 +20,9 @@ import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.os.Build;
import android.os.Handler;
@ -31,6 +33,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.v4.view.ViewCompat;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.SparseArray;
@ -42,6 +45,7 @@ import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStructure;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
@ -61,7 +65,10 @@ public class GeckoView extends FrameLayout {
private boolean mIsResettingFocus;
private boolean mAutofillEnabled = true;
private GeckoSession.SelectionActionDelegate mSelectionActionDelegate;
private GeckoSession.AutofillDelegate mAutofillDelegate;
private static class SavedState extends BaseSavedState {
public final GeckoSession session;
@ -235,6 +242,8 @@ public class GeckoView extends FrameLayout {
if (activity != null) {
mSelectionActionDelegate = new BasicSelectionActionDelegate(activity);
}
mAutofillDelegate = new AndroidAutofillDelegate();
}
/**
@ -326,6 +335,10 @@ public class GeckoView extends FrameLayout {
mSession.setSelectionActionDelegate(null);
}
if (mSession.getAutofillDelegate() == mAutofillDelegate) {
mSession.setAutofillDelegate(null);
}
if (isFocused()) {
mSession.setFocused(false);
}
@ -398,6 +411,10 @@ public class GeckoView extends FrameLayout {
session.setSelectionActionDelegate(mSelectionActionDelegate);
}
if (mAutofillEnabled) {
session.setAutofillDelegate(mAutofillDelegate);
}
if (isFocused()) {
session.setFocused(true);
}
@ -713,8 +730,99 @@ public class GeckoView extends FrameLayout {
final int flags) {
super.onProvideAutofillVirtualStructure(structure, flags);
if (mSession != null) {
mSession.provideAutofillVirtualStructure(this, structure, flags);
if (mSession == null) {
return;
}
final AutofillElement root = mSession.getAutofillElements();
fillViewStructure(root, structure, flags);
}
@TargetApi(23)
private void fillViewStructure(final AutofillElement element, final ViewStructure structure, final int flags) {
if (Build.VERSION.SDK_INT >= 26) {
structure.setAutofillId(getAutofillId(), element.id);
structure.setWebDomain(element.domain);
}
structure.setId(element.id, null, null, null);
structure.setDimens(0, 0, 0, 0, element.dimensions.width(), element.dimensions.height());
if (Build.VERSION.SDK_INT >= 26) {
final ViewStructure.HtmlInfo.Builder htmlBuilder = structure.newHtmlInfoBuilder(element.tag);
for (final String key : element.attributes.keySet()) {
htmlBuilder.addAttribute(key, String.valueOf(element.attributes.get(key)));
}
structure.setHtmlInfo(htmlBuilder.build());
}
int childCount = 0;
for (final AutofillElement child : element.children) {
final ViewStructure childStructure = structure.newChild(childCount);
fillViewStructure(child, childStructure, flags);
childCount++;
}
structure.setChildCount(childCount);
switch (element.tag) {
case "input":
case "textarea":
structure.setClassName("android.widget.EditText");
structure.setEnabled(element.enabled);
structure.setFocusable(element.focusable);
structure.setFocused(element.focused);
structure.setVisibility(View.VISIBLE);
if (Build.VERSION.SDK_INT >= 26) {
structure.setAutofillType(View.AUTOFILL_TYPE_TEXT);
}
break;
default:
if (childCount > 0) {
structure.setClassName("android.view.ViewGroup");
} else {
structure.setClassName("android.view.View");
}
break;
}
if (Build.VERSION.SDK_INT >= 26 && "input".equals(element.tag)) {
// LastPass will fill password to the field that setAutofillHints is unset and setInputType is set.
switch (element.hint) {
case AutofillElement.HINT_EMAIL_ADDRESS:
structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_EMAIL_ADDRESS });
structure.setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
break;
case AutofillElement.HINT_PASSWORD:
structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_PASSWORD });
structure.setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD);
break;
case AutofillElement.HINT_URL:
structure.setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_VARIATION_URI);
break;
case AutofillElement.HINT_USERNAME:
structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_USERNAME });
structure.setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
break;
}
switch (element.inputType) {
case AutofillElement.INPUT_TYPE_NUMBER:
structure.setInputType(InputType.TYPE_CLASS_NUMBER);
break;
case AutofillElement.INPUT_TYPE_PHONE:
structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_PHONE });
structure.setInputType(InputType.TYPE_CLASS_PHONE);
break;
default:
break;
}
}
}
@ -750,4 +858,106 @@ public class GeckoView extends FrameLayout {
public @NonNull GeckoResult<Bitmap> capturePixels() {
return mDisplay.capturePixels();
}
/**
* Sets whether or not this View participates in Android autofill.
*
* When enabled, this will set an {@link GeckoSession.AutofillDelegate} on the
* {@link GeckoSession} for this instance.
*
* @param enabled Whether or not Android autofill is enabled for this view.
*/
@TargetApi(26)
public void setAutofillEnabled(final boolean enabled) {
mAutofillEnabled = enabled;
if (mSession != null) {
if (!enabled && mSession.getAutofillDelegate() == mAutofillDelegate) {
mSession.setAutofillDelegate(null);
} else if (enabled) {
mSession.setAutofillDelegate(mAutofillDelegate);
}
}
}
/**
* @return Whether or not Android autofill is enabled for this view.
*/
@TargetApi(26)
public boolean getAutofillEnabled() {
return mAutofillEnabled;
}
private class AndroidAutofillDelegate implements GeckoSession.AutofillDelegate {
private AutofillElement findElementWithId(final AutofillElement root, final int id) {
if (root.id == id) {
return root;
}
for (AutofillElement child : root.children) {
final AutofillElement found = findElementWithId(child, id);
if (found != null) {
return found;
}
}
return null;
}
private Rect displayRectForId(@NonNull final GeckoSession session,
@NonNull final int virtualId,
@Nullable final View view) {
final AutofillElement structure = session.getAutofillElements();
final AutofillElement element = findElementWithId(structure, virtualId);
if (element == null) {
return new Rect(0, 0, 0, 0);
}
final Matrix matrix = new Matrix();
final RectF rectF = new RectF(element.dimensions);
session.getPageToScreenMatrix(matrix);
matrix.mapRect(rectF);
final Rect screenRect = new Rect();
rectF.roundOut(screenRect);
return screenRect;
}
@Override
public void onAutofill(@NonNull final GeckoSession session,
@GeckoSession.AutofillNotification final int notification,
final int virtualId) {
ThreadUtils.assertOnUiThread();
if (Build.VERSION.SDK_INT < 26) {
return;
}
final AutofillManager manager =
GeckoView.this.getContext().getSystemService(AutofillManager.class);
if (manager == null) {
return;
}
switch (notification) {
case AUTO_FILL_NOTIFY_STARTED:
// This line seems necessary for auto-fill to work on the initial page.
manager.cancel();
break;
case AUTO_FILL_NOTIFY_COMMITTED:
manager.commit();
break;
case AUTO_FILL_NOTIFY_CANCELED:
manager.cancel();
break;
case AUTO_FILL_NOTIFY_VIEW_ENTERED:
manager.notifyViewEntered(GeckoView.this, virtualId, displayRectForId(session, virtualId, GeckoView.this));
break;
case AUTO_FILL_NOTIFY_VIEW_EXITED:
manager.notifyViewExited(GeckoView.this, virtualId);
break;
}
}
}
}