Commit 9d51f685 authored by Tao Bai's avatar Tao Bai Committed by Commit Bot

Consolidate the AutofillProvider and AutofillProviderImpl

The AutofillProvider interface was added for downstream coding when
the Android autofill feature was first introduced. It wasn't
necessary now.

This patch shall also reduce the binary size a little bit.

Bug: 1094377
Change-Id: I8896fbaa6260750d8b9e92df9ed67b4f5a0b26c4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2248170Reviewed-by: default avatarDominic Battré <battre@chromium.org>
Reviewed-by: default avatarColin Blundell <blundell@chromium.org>
Commit-Queue: Tao Bai <michaelbai@chromium.org>
Cr-Commit-Position: refs/heads/master@{#779362}
parent a1c0f147
...@@ -55,7 +55,6 @@ import org.chromium.base.library_loader.NativeLibraries; ...@@ -55,7 +55,6 @@ import org.chromium.base.library_loader.NativeLibraries;
import org.chromium.base.metrics.RecordHistogram; import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.ScopedSysTraceEvent; import org.chromium.base.metrics.ScopedSysTraceEvent;
import org.chromium.components.autofill.AutofillProvider; import org.chromium.components.autofill.AutofillProvider;
import org.chromium.components.autofill.AutofillProviderImpl;
import org.chromium.components.embedder_support.application.ClassLoaderContextWrapperFactory; import org.chromium.components.embedder_support.application.ClassLoaderContextWrapperFactory;
import org.chromium.components.embedder_support.application.FirebaseConfig; import org.chromium.components.embedder_support.application.FirebaseConfig;
import org.chromium.content_public.browser.LGEmailActionModeWorkaround; import org.chromium.content_public.browser.LGEmailActionModeWorkaround;
...@@ -630,7 +629,7 @@ public class WebViewChromiumFactoryProvider implements WebViewFactoryProvider { ...@@ -630,7 +629,7 @@ public class WebViewChromiumFactoryProvider implements WebViewFactoryProvider {
AutofillProvider createAutofillProvider(Context context, ViewGroup containerView) { AutofillProvider createAutofillProvider(Context context, ViewGroup containerView) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null;
return new AutofillProviderImpl(context, containerView, "Android WebView"); return new AutofillProvider(context, containerView, "Android WebView");
} }
void startYourEngines(boolean onMainThread) { void startYourEngines(boolean onMainThread) {
......
...@@ -55,7 +55,6 @@ import org.chromium.base.test.util.MinAndroidSdkLevel; ...@@ -55,7 +55,6 @@ import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.components.autofill.AutofillManagerWrapper; import org.chromium.components.autofill.AutofillManagerWrapper;
import org.chromium.components.autofill.AutofillPopup; import org.chromium.components.autofill.AutofillPopup;
import org.chromium.components.autofill.AutofillProvider; import org.chromium.components.autofill.AutofillProvider;
import org.chromium.components.autofill.AutofillProviderImpl;
import org.chromium.components.autofill.AutofillProviderUMA; import org.chromium.components.autofill.AutofillProviderUMA;
import org.chromium.content_public.browser.UiThreadTaskTraits; import org.chromium.content_public.browser.UiThreadTaskTraits;
import org.chromium.content_public.browser.test.util.DOMUtils; import org.chromium.content_public.browser.test.util.DOMUtils;
...@@ -759,7 +758,7 @@ public class AwAutofillTest { ...@@ -759,7 +758,7 @@ public class AwAutofillTest {
private int mSubmissionSource; private int mSubmissionSource;
private TestAutofillManagerWrapper mTestAutofillManagerWrapper; private TestAutofillManagerWrapper mTestAutofillManagerWrapper;
private AwAutofillSessionUMATestHelper mUMATestHelper; private AwAutofillSessionUMATestHelper mUMATestHelper;
private AutofillProviderImpl mAutofillProviderImpl; private AutofillProvider mAutofillProvider;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
...@@ -772,9 +771,9 @@ public class AwAutofillTest { ...@@ -772,9 +771,9 @@ public class AwAutofillTest {
public AutofillProvider createAutofillProvider( public AutofillProvider createAutofillProvider(
Context context, ViewGroup containerView) { Context context, ViewGroup containerView) {
mTestAutofillManagerWrapper = new TestAutofillManagerWrapper(context); mTestAutofillManagerWrapper = new TestAutofillManagerWrapper(context);
mAutofillProviderImpl = new AutofillProviderImpl(containerView, mAutofillProvider = new AutofillProvider(containerView,
mTestAutofillManagerWrapper, context, "AwAutofillTest"); mTestAutofillManagerWrapper, context, "AwAutofillTest");
return mAutofillProviderImpl; return mAutofillProvider;
} }
}); });
mAwContents = mTestContainerView.getAwContents(); mAwContents = mTestContainerView.getAwContents();
...@@ -784,7 +783,7 @@ public class AwAutofillTest { ...@@ -784,7 +783,7 @@ public class AwAutofillTest {
@After @After
public void tearDown() { public void tearDown() {
mWebServer.shutdown(); mWebServer.shutdown();
mAutofillProviderImpl = null; mAutofillProvider = null;
} }
@Test @Test
...@@ -1785,7 +1784,7 @@ public class AwAutofillTest { ...@@ -1785,7 +1784,7 @@ public class AwAutofillTest {
List<Integer> expectedValues = new ArrayList<>(); List<Integer> expectedValues = new ArrayList<>();
// On Android version below P scroll triggers additional // On Android version below P scroll triggers additional
// AUTOFILL_VIEW_ENTERED (@see AutofillProviderImpl#onTextFieldDidScroll). // AUTOFILL_VIEW_ENTERED (@see AutofillProvider#onTextFieldDidScroll).
if (VERSION.SDK_INT < Build.VERSION_CODES.P) { if (VERSION.SDK_INT < Build.VERSION_CODES.P) {
expectedValues.add(AUTOFILL_VIEW_ENTERED); expectedValues.add(AUTOFILL_VIEW_ENTERED);
} }
...@@ -1914,7 +1913,7 @@ public class AwAutofillTest { ...@@ -1914,7 +1913,7 @@ public class AwAutofillTest {
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A); dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
pollDatalistPopupShown(); pollDatalistPopupShown();
TouchCommon.singleClickView( TouchCommon.singleClickView(
mAutofillProviderImpl.getDatalistPopupForTesting().getListView().getChildAt(1)); mAutofillProvider.getDatalistPopupForTesting().getListView().getChildAt(1));
// Verify the selection accepted by renderer. // Verify the selection accepted by renderer.
pollJavascriptResult("document.getElementById('text2').value;", "\"A2\""); pollJavascriptResult("document.getElementById('text2').value;", "\"A2\"");
} }
...@@ -1931,7 +1930,7 @@ public class AwAutofillTest { ...@@ -1931,7 +1930,7 @@ public class AwAutofillTest {
private void pollDatalistPopupShown() { private void pollDatalistPopupShown() {
AwActivityTestRule.pollInstrumentationThread(() -> { AwActivityTestRule.pollInstrumentationThread(() -> {
AutofillPopup popup = mAutofillProviderImpl.getDatalistPopupForTesting(); AutofillPopup popup = mAutofillProvider.getDatalistPopupForTesting();
return popup != null && popup.getListView().getChildCount() > 0; return popup != null && popup.getListView().getChildCount() > 0;
}); });
} }
......
...@@ -72,7 +72,6 @@ android_library("provider_java") { ...@@ -72,7 +72,6 @@ android_library("provider_java") {
"java/src/org/chromium/components/autofill/AutofillActionModeCallback.java", "java/src/org/chromium/components/autofill/AutofillActionModeCallback.java",
"java/src/org/chromium/components/autofill/AutofillManagerWrapper.java", "java/src/org/chromium/components/autofill/AutofillManagerWrapper.java",
"java/src/org/chromium/components/autofill/AutofillProvider.java", "java/src/org/chromium/components/autofill/AutofillProvider.java",
"java/src/org/chromium/components/autofill/AutofillProviderImpl.java",
"java/src/org/chromium/components/autofill/AutofillProviderUMA.java", "java/src/org/chromium/components/autofill/AutofillProviderUMA.java",
"java/src/org/chromium/components/autofill/FormData.java", "java/src/org/chromium/components/autofill/FormData.java",
"java/src/org/chromium/components/autofill/FormFieldData.java", "java/src/org/chromium/components/autofill/FormFieldData.java",
......
...@@ -4,49 +4,302 @@ ...@@ -4,49 +4,302 @@
package org.chromium.components.autofill; package org.chromium.components.autofill;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import android.os.Build;
import android.os.Bundle;
import android.util.SparseArray; import android.util.SparseArray;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewStructure; import android.view.ViewStructure;
import android.view.autofill.AutofillValue; import android.view.autofill.AutofillValue;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.StrictModeContext;
import org.chromium.base.ThreadUtils;
import org.chromium.base.annotations.CalledByNative; import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace; import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods; import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.annotations.VerifiesOnO;
import org.chromium.base.metrics.ScopedSysTraceEvent;
import org.chromium.components.version_info.VersionConstants;
import org.chromium.content_public.browser.WebContents; import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsAccessibility;
import org.chromium.ui.DropdownItem;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.display.DisplayAndroid;
/** /**
* This class defines interface of AutofillProvider, it doesn't use chrome's * This class works with Android autofill service to fill web form, it doesn't use chrome's
* autofill service or suggestion UI, instead, uses third party autofill service * autofill service or suggestion UI. All methods are supposed to be called in UI thread.
* by knowing of format structure and user's input.
* *
* AutofillProvider handles one autofill session at time, each call of * AutofillProvider handles one autofill session at time, each call of
* queryFormFieldAutofill cancels previous session and starts a new one, the * queryFormFieldAutofill cancels previous session and starts a new one, the
* calling of other methods shall associate with current session. * calling of other methods shall associate with current session.
* *
* This class doesn't have 1:1 mapping to native AutofillProviderAndroid; the
* normal ownership model is that this object is owned by the embedder-specific
* Java WebContents wrapper (e.g., AwContents.java in //android_webview), and
* AutofillProviderAndroid is owned by the embedder-specific C++ WebContents
* wrapper (e.g., native AwContents in //android_webview).
*
* VerifiesOnO since it causes class verification errors, see crbug.com/991851.
*/ */
@VerifiesOnO
@TargetApi(Build.VERSION_CODES.O)
@JNINamespace("autofill") @JNINamespace("autofill")
public abstract class AutofillProvider { public class AutofillProvider {
public AutofillProvider() {} private static final String TAG = "AutofillProvider";
private static class FocusField {
public final short fieldIndex;
public final Rect absBound;
public FocusField(short fieldIndex, Rect absBound) {
this.fieldIndex = fieldIndex;
this.absBound = absBound;
}
}
/** /**
* Invoked when container view is changed. * The class to wrap the request to framework.
* *
* @param containerView new container view. * Though framework guarantees always giving us the autofill value of current
* session, we still want to verify this by using unique virtual id which is
* composed of sessionId and form field index, we don't use the request id
* which comes from renderer as session id because it is not unique.
*/ */
public abstract void onContainerViewChanged(ViewGroup containerView); private static class AutofillRequest {
private static final int INIT_ID = 1; // ID can't be 0 in Android.
private static int sSessionId = INIT_ID;
public final int sessionId;
private FormData mFormData;
private FocusField mFocusField;
public AutofillRequest(FormData formData, FocusField focus) {
sessionId = getNextClientId();
mFormData = formData;
mFocusField = focus;
}
public void fillViewStructure(ViewStructure structure) {
structure.setWebDomain(mFormData.mHost);
structure.setHtmlInfo(structure.newHtmlInfoBuilder("form")
.addAttribute("name", mFormData.mName)
.build());
int index = structure.addChildCount(mFormData.mFields.size());
short fieldIndex = 0;
for (FormFieldData field : mFormData.mFields) {
ViewStructure child = structure.newChild(index++);
int virtualId = toVirtualId(sessionId, fieldIndex++);
child.setAutofillId(structure.getAutofillId(), virtualId);
if (field.mAutocompleteAttr != null && !field.mAutocompleteAttr.isEmpty()) {
child.setAutofillHints(field.mAutocompleteAttr.split(" +"));
}
child.setHint(field.mPlaceholder);
RectF bounds = field.getBoundsInContainerViewCoordinates();
// Field has no scroll.
child.setDimens((int) bounds.left, (int) bounds.top, 0 /* scrollX*/,
0 /* scrollY */, (int) bounds.width(), (int) bounds.height());
ViewStructure.HtmlInfo.Builder builder =
child.newHtmlInfoBuilder("input")
.addAttribute("name", field.mName)
.addAttribute("type", field.mType)
.addAttribute("label", field.mLabel)
.addAttribute("ua-autofill-hints", field.mHeuristicType)
.addAttribute("id", field.mId);
switch (field.getControlType()) {
case FormFieldData.ControlType.LIST:
child.setAutofillType(View.AUTOFILL_TYPE_LIST);
child.setAutofillOptions(field.mOptionContents);
int i = findIndex(field.mOptionValues, field.getValue());
if (i != -1) {
child.setAutofillValue(AutofillValue.forList(i));
}
break;
case FormFieldData.ControlType.TOGGLE:
child.setAutofillType(View.AUTOFILL_TYPE_TOGGLE);
child.setAutofillValue(AutofillValue.forToggle(field.isChecked()));
break;
case FormFieldData.ControlType.TEXT:
case FormFieldData.ControlType.DATALIST:
child.setAutofillType(View.AUTOFILL_TYPE_TEXT);
child.setAutofillValue(AutofillValue.forText(field.getValue()));
if (field.mMaxLength != 0) {
builder.addAttribute("maxlength", String.valueOf(field.mMaxLength));
}
if (field.getControlType() == FormFieldData.ControlType.DATALIST) {
child.setAutofillOptions(field.mDatalistValues);
}
break;
default:
break;
}
child.setHtmlInfo(builder.build());
}
}
public boolean autofill(final SparseArray<AutofillValue> values) {
for (int i = 0; i < values.size(); ++i) {
int id = values.keyAt(i);
if (toSessionId(id) != sessionId) return false;
AutofillValue value = values.get(id);
if (value == null) continue;
short index = toIndex(id);
if (index < 0 || index >= mFormData.mFields.size()) return false;
FormFieldData field = mFormData.mFields.get(index);
if (field == null) return false;
try {
switch (field.getControlType()) {
case FormFieldData.ControlType.LIST:
int j = value.getListValue();
if (j < 0 && j >= field.mOptionValues.length) continue;
field.setAutofillValue(field.mOptionValues[j]);
break;
case FormFieldData.ControlType.TOGGLE:
field.setChecked(value.getToggleValue());
break;
case FormFieldData.ControlType.TEXT:
case FormFieldData.ControlType.DATALIST:
field.setAutofillValue((String) value.getTextValue());
break;
default:
break;
}
} catch (IllegalStateException e) {
// Refer to crbug.com/1080580 .
Log.e(TAG, "The given AutofillValue wasn't expected, abort autofill.", e);
return false;
}
}
return true;
}
public void setFocusField(FocusField focusField) {
mFocusField = focusField;
}
public FocusField getFocusField() {
return mFocusField;
}
public int getFieldCount() {
return mFormData.mFields.size();
}
public AutofillValue getFieldNewValue(int index) {
FormFieldData field = mFormData.mFields.get(index);
if (field == null) return null;
switch (field.getControlType()) {
case FormFieldData.ControlType.LIST:
int i = findIndex(field.mOptionValues, field.getValue());
if (i == -1) return null;
return AutofillValue.forList(i);
case FormFieldData.ControlType.TOGGLE:
return AutofillValue.forToggle(field.isChecked());
case FormFieldData.ControlType.TEXT:
case FormFieldData.ControlType.DATALIST:
return AutofillValue.forText(field.getValue());
default:
return null;
}
}
public int getVirtualId(short index) {
return toVirtualId(sessionId, index);
}
public FormFieldData getField(short index) {
return mFormData.mFields.get(index);
}
private static int findIndex(String[] values, String value) {
if (values != null && value != null) {
for (int i = 0; i < values.length; i++) {
if (value.equals(values[i])) return i;
}
}
return -1;
}
private static int getNextClientId() {
ThreadUtils.assertOnUiThread();
if (sSessionId == 0xffff) sSessionId = INIT_ID;
return sSessionId++;
}
private static int toSessionId(int virtualId) {
return (virtualId & 0xffff0000) >> 16;
}
private static short toIndex(int virtualId) {
return (short) (virtualId & 0xffff);
}
private static int toVirtualId(int clientId, short index) {
return (clientId << 16) | index;
}
}
private final String mProviderName;
private AutofillManagerWrapper mAutofillManager;
private ViewGroup mContainerView;
private WebContents mWebContents;
private AutofillRequest mRequest;
private long mNativeAutofillProvider;
private AutofillProviderUMA mAutofillUMA;
private AutofillManagerWrapper.InputUIObserver mInputUIObserver;
private long mAutofillTriggeredTimeMillis;
private Context mContext;
private AutofillPopup mDatalistPopup;
private WebContentsAccessibility mWebContentsAccessibility;
private View mAnchorView;
public abstract void setWebContents(WebContents webContents); public AutofillProvider(Context context, ViewGroup containerView, String providerName) {
this(containerView, new AutofillManagerWrapper(context), context, providerName);
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public AutofillProvider(ViewGroup containerView, AutofillManagerWrapper manager,
Context context, String providerName) {
mProviderName = providerName;
try (ScopedSysTraceEvent e = ScopedSysTraceEvent.scoped("AutofillProvider.constructor")) {
assert Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
mAutofillManager = manager;
mContainerView = containerView;
mAutofillUMA = new AutofillProviderUMA(context);
mInputUIObserver = new AutofillManagerWrapper.InputUIObserver() {
@Override
public void onInputUIShown() {
// Not need to report suggestion window displayed if there is no live autofill
// session.
if (mRequest == null) return;
mAutofillUMA.onSuggestionDisplayed(
System.currentTimeMillis() - mAutofillTriggeredTimeMillis);
}
};
mAutofillManager.addInputUIObserver(mInputUIObserver);
mContext = context;
}
}
/** /**
* Invoked when autofill value is available, AutofillProvider shall fill the * Invoked when container view is changed.
* form with the provided values.
* *
* @param values the array of autofill values, the key is virtual id of form * @param containerView new container view.
* field.
*/ */
public abstract void autofill(final SparseArray<AutofillValue> values); public void onContainerViewChanged(ViewGroup containerView) {
mContainerView = containerView;
}
/** /**
* Invoked when autofill service needs the form structure. * Invoked when autofill service needs the form structure.
...@@ -54,14 +307,58 @@ public abstract class AutofillProvider { ...@@ -54,14 +307,58 @@ public abstract class AutofillProvider {
* @param structure see View.onProvideAutofillVirtualStructure() * @param structure see View.onProvideAutofillVirtualStructure()
* @param flags see View.onProvideAutofillVirtualStructure() * @param flags see View.onProvideAutofillVirtualStructure()
*/ */
public abstract void onProvideAutoFillVirtualStructure(ViewStructure structure, int flags); public void onProvideAutoFillVirtualStructure(ViewStructure structure, int flags) {
// This method could be called for the session started by the native
// control outside of the scope of autofill, e.g. the URL bar, in this case, we simply
// return.
if (mRequest == null) return;
Bundle bundle = structure.getExtras();
if (bundle != null) {
bundle.putCharSequence("VIRTUAL_STRUCTURE_PROVIDER_NAME", mProviderName);
bundle.putCharSequence(
"VIRTUAL_STRUCTURE_PROVIDER_VERSION", VersionConstants.PRODUCT_VERSION);
}
mRequest.fillViewStructure(structure);
if (AutofillManagerWrapper.isLoggable()) {
AutofillManagerWrapper.log(
"onProvideAutoFillVirtualStructure fields:" + structure.getChildCount());
}
mAutofillUMA.onVirtualStructureProvided();
}
/**
* Invoked when autofill value is available, AutofillProvider shall fill the
* form with the provided values.
*
* @param values the array of autofill values, the key is virtual id of form
* field.
*/
public void autofill(final SparseArray<AutofillValue> values) {
if (mNativeAutofillProvider != 0 && mRequest != null && mRequest.autofill((values))) {
autofill(mNativeAutofillProvider, mRequest.mFormData);
if (AutofillManagerWrapper.isLoggable()) {
AutofillManagerWrapper.log("autofill values:" + values.size());
}
mAutofillUMA.onAutofill();
}
}
/** /**
* @return whether query autofill suggestion. * @return whether query autofill suggestion.
*/ */
public abstract boolean shouldQueryAutofillSuggestion(); public boolean shouldQueryAutofillSuggestion() {
return mRequest != null && mRequest.getFocusField() != null
&& !mAutofillManager.isAutofillInputUIShowing();
}
public abstract void queryAutofillSuggestion(); public void queryAutofillSuggestion() {
if (shouldQueryAutofillSuggestion()) {
FocusField focusField = mRequest.getFocusField();
mAutofillManager.requestAutofill(mContainerView,
mRequest.getVirtualId(focusField.fieldIndex), focusField.absBound);
}
}
/** /**
* Invoked when filling form is need. AutofillProvider shall ask autofill * Invoked when filling form is need. AutofillProvider shall ask autofill
...@@ -75,8 +372,26 @@ public abstract class AutofillProvider { ...@@ -75,8 +372,26 @@ public abstract class AutofillProvider {
* @param height the boundary of focus field. * @param height the boundary of focus field.
*/ */
@CalledByNative @CalledByNative
protected abstract void startAutofillSession( public void startAutofillSession(
FormData formData, int focus, float x, float y, float width, float height); FormData formData, int focus, float x, float y, float width, float height) {
// Check focusField inside short value?
// Autofill Manager might have session that wasn't started by AutofillProvider,
// we just always cancel existing session here.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
mAutofillManager.cancel();
}
Rect absBound = transformToWindowBounds(new RectF(x, y, x + width, y + height));
if (mRequest != null) notifyViewExitBeforeDestroyRequest();
transformFormFieldToContainViewCoordinates(formData);
mRequest = new AutofillRequest(formData, new FocusField((short) focus, absBound));
int virtualId = mRequest.getVirtualId((short) focus);
notifyVirtualViewEntered(mContainerView, virtualId, absBound);
mAutofillUMA.onSessionStarted(mAutofillManager.isDisabled());
mAutofillTriggeredTimeMillis = System.currentTimeMillis();
mAutofillManager.notifyNewSessionStarted();
}
/** /**
* Invoked when form field's value is changed. * Invoked when form field's value is changed.
...@@ -89,8 +404,31 @@ public abstract class AutofillProvider { ...@@ -89,8 +404,31 @@ public abstract class AutofillProvider {
* *
*/ */
@CalledByNative @CalledByNative
protected abstract void onFormFieldDidChange( public void onFormFieldDidChange(int index, float x, float y, float width, float height) {
int index, float x, float y, float width, float height); // Check index inside short value?
if (mRequest == null) return;
short sIndex = (short) index;
FocusField focusField = mRequest.getFocusField();
if (focusField == null || sIndex != focusField.fieldIndex) {
onFocusChangedImpl(true, index, x, y, width, height, true /*causedByValueChange*/);
} else {
// Currently there is no api to notify both value and position
// change, before the API is available, we still need to call
// notifyVirtualViewEntered() to tell current coordinates because
// the position could be changed.
int virtualId = mRequest.getVirtualId(sIndex);
Rect absBound = transformToWindowBounds(new RectF(x, y, x + width, y + height));
if (!focusField.absBound.equals(absBound)) {
notifyVirtualViewExited(mContainerView, virtualId);
notifyVirtualViewEntered(mContainerView, virtualId, absBound);
// Update focus field position.
mRequest.setFocusField(new FocusField(focusField.fieldIndex, absBound));
}
}
notifyVirtualValueChanged(index, /* forceNotify = */ false);
mAutofillUMA.onUserChangeFieldValue(mRequest.getField(sIndex).hasPreviouslyAutofilled());
}
/** /**
* Invoked when text field is scrolled. * Invoked when text field is scrolled.
...@@ -103,8 +441,59 @@ public abstract class AutofillProvider { ...@@ -103,8 +441,59 @@ public abstract class AutofillProvider {
* *
*/ */
@CalledByNative @CalledByNative
protected abstract void onTextFieldDidScroll( public void onTextFieldDidScroll(int index, float x, float y, float width, float height) {
int index, float x, float y, float width, float height); // crbug.com/730764 - from P and above, Android framework listens to the onScrollChanged()
// and repositions the autofill UI automatically.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) return;
if (mRequest == null) return;
short sIndex = (short) index;
FocusField focusField = mRequest.getFocusField();
if (focusField == null || sIndex != focusField.fieldIndex) return;
int virtualId = mRequest.getVirtualId(sIndex);
Rect absBound = transformToWindowBounds(new RectF(x, y, x + width, y + height));
// Notify the new position to the Android framework. Note that we do not call
// notifyVirtualViewExited() here intentionally to avoid flickering.
notifyVirtualViewEntered(mContainerView, virtualId, absBound);
// Update focus field position.
mRequest.setFocusField(new FocusField(focusField.fieldIndex, absBound));
}
private boolean isDatalistField(int childId) {
FormFieldData field = mRequest.getField((short) childId);
return field.mControlType == FormFieldData.ControlType.DATALIST;
}
private void notifyVirtualValueChanged(int index, boolean forceNotify) {
// The ValueChanged, ViewEntered and ViewExited aren't notified to the autofill service for
// the focused datalist to avoid the potential UI conflict.
// The datalist support was added later and the option list is displayed by WebView, the
// autofill service might also show its suggestions when the datalist (associated the input
// field) is focused, the two UI overlap, the solution is to completely hide the fact that
// the datalist is being focused to the autofill service to prevent it from displaying the
// suggestion.
// The ValueChange will still be sent to autofill service when the form
// submitted or autofilled.
if (!forceNotify && isDatalistField(index)) return;
AutofillValue autofillValue = mRequest.getFieldNewValue(index);
if (autofillValue == null) return;
mAutofillManager.notifyVirtualValueChanged(
mContainerView, mRequest.getVirtualId((short) index), autofillValue);
}
private void notifyVirtualViewEntered(View parent, int childId, Rect absBounds) {
// Refer to notifyVirtualValueChanged() for the reason of the datalist's special handling.
if (isDatalistField(childId)) return;
mAutofillManager.notifyVirtualViewEntered(parent, childId, absBounds);
}
private void notifyVirtualViewExited(View parent, int childId) {
// Refer to notifyVirtualValueChanged() for the reason of the datalist's special handling.
if (isDatalistField(childId)) return;
mAutofillManager.notifyVirtualViewExited(parent, childId);
}
/** /**
* Invoked when current form will be submitted. * Invoked when current form will be submitted.
...@@ -112,7 +501,14 @@ public abstract class AutofillProvider { ...@@ -112,7 +501,14 @@ public abstract class AutofillProvider {
* SubmissionSource.java * SubmissionSource.java
*/ */
@CalledByNative @CalledByNative
protected abstract void onFormSubmitted(int submissionSource); public void onFormSubmitted(int submissionSource) {
// The changes could be missing, like those made by Javascript, we'd better to notify
// AutofillManager current values. also see crbug.com/353001 and crbug.com/732856.
forceNotifyFormValues();
mAutofillManager.commit(submissionSource);
mRequest = null;
mAutofillUMA.onFormSubmitted(submissionSource);
}
/** /**
* Invoked when focus field changed. * Invoked when focus field changed.
...@@ -125,48 +521,256 @@ public abstract class AutofillProvider { ...@@ -125,48 +521,256 @@ public abstract class AutofillProvider {
* @param height the boundary of focus field. * @param height the boundary of focus field.
*/ */
@CalledByNative @CalledByNative
protected abstract void onFocusChanged( public void onFocusChanged(
boolean focusOnForm, int focusItem, float x, float y, float width, float height); boolean focusOnForm, int focusField, float x, float y, float width, float height) {
onFocusChangedImpl(
focusOnForm, focusField, x, y, width, height, false /*causedByValueChange*/);
}
@CalledByNative
protected void hidePopup() {
if (mDatalistPopup != null) {
mDatalistPopup.dismiss();
mDatalistPopup = null;
}
if (mWebContentsAccessibility != null) {
mWebContentsAccessibility.onAutofillPopupDismissed();
}
}
private void notifyViewExitBeforeDestroyRequest() {
if (mRequest == null) return;
FocusField focusField = mRequest.getFocusField();
if (focusField == null) return;
notifyVirtualViewExited(mContainerView, mRequest.getVirtualId(focusField.fieldIndex));
mRequest.setFocusField(null);
}
private void onFocusChangedImpl(boolean focusOnForm, int focusField, float x, float y,
float width, float height, boolean causedByValueChange) {
// Check focusField inside short value?
// FocusNoLongerOnForm is called after form submitted.
if (mRequest == null) return;
FocusField prev = mRequest.getFocusField();
if (focusOnForm) {
Rect absBound = transformToWindowBounds(new RectF(x, y, x + width, y + height));
if (prev != null && prev.fieldIndex == focusField && absBound.equals(prev.absBound)) {
return;
}
// Notify focus changed.
if (prev != null) {
notifyVirtualViewExited(mContainerView, mRequest.getVirtualId(prev.fieldIndex));
}
notifyVirtualViewEntered(
mContainerView, mRequest.getVirtualId((short) focusField), absBound);
if (!causedByValueChange) {
// The focus field value might not sync with platform's
// AutofillManager, just notify it value changed.
notifyVirtualValueChanged(focusField, /* forceNotify = */ false);
mAutofillTriggeredTimeMillis = System.currentTimeMillis();
}
mRequest.setFocusField(new FocusField((short) focusField, absBound));
} else {
if (prev == null) return;
// Notify focus changed.
notifyVirtualViewExited(mContainerView, mRequest.getVirtualId(prev.fieldIndex));
mRequest.setFocusField(null);
}
}
@CalledByNative
protected void showDatalistPopup(
String[] datalistValues, String[] datalistLabels, boolean isRtl) {
if (mRequest == null) return;
FocusField focusField = mRequest.getFocusField();
if (focusField != null) {
showDatalistPopup(datalistValues, datalistLabels,
mRequest.getField(focusField.fieldIndex).getBounds(), isRtl);
}
}
/** /**
* Send form to renderer for filling. * Display the simplest popup for the datalist. This is same as WebView's datalist popup in
* * Android pre-o. No suggestion from the autofill service will be presented, No advance
* @param nativeAutofillProvider the native autofill provider. * features of AutofillPopup are used.
* @param formData the form to fill.
*/ */
protected void autofill(long nativeAutofillProvider, FormData formData) { private void showDatalistPopup(
AutofillProviderJni.get().onAutofillAvailable( String[] datalistValues, String[] datalistLabels, RectF bounds, boolean isRtl) {
nativeAutofillProvider, AutofillProvider.this, formData); final AutofillSuggestion[] suggestions = new AutofillSuggestion[datalistValues.length];
for (int i = 0; i < suggestions.length; i++) {
suggestions[i] = new AutofillSuggestion(datalistValues[i], datalistLabels[i],
DropdownItem.NO_ICON, false /* isIconAtLeft */, i, false /* isDeletable */,
false /* isMultilineLabel */, false /* isBoldLabel */);
}
if (mWebContentsAccessibility == null) {
mWebContentsAccessibility = WebContentsAccessibility.fromWebContents(mWebContents);
}
if (mDatalistPopup == null) {
if (ContextUtils.activityFromContext(mContext) == null) return;
ViewAndroidDelegate delegate = mWebContents.getViewAndroidDelegate();
if (mAnchorView == null) mAnchorView = delegate.acquireView();
setAnchorViewRect(bounds);
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
mDatalistPopup = new AutofillPopup(mContext, mAnchorView, new AutofillDelegate() {
@Override
public void dismissed() {
onDatalistPopupDismissed();
} }
protected void acceptDataListSuggestion(long nativeAutofillProvider, String value) { @Override
AutofillProviderJni.get().onAcceptDataListSuggestion( public void suggestionSelected(int listIndex) {
nativeAutofillProvider, AutofillProvider.this, value); onSuggestionSelected(suggestions[listIndex].getLabel());
} }
protected void setAnchorViewRect(long nativeAutofillProvider, View anchorView, RectF rect) { @Override
AutofillProviderJni.get().setAnchorViewRect(nativeAutofillProvider, AutofillProvider.this, public void deleteSuggestion(int listIndex) {}
anchorView, rect.left, rect.top, rect.width(), rect.height());
@Override
public void accessibilityFocusCleared() {
mWebContentsAccessibility.onAutofillPopupAccessibilityFocusCleared();
}
});
} catch (RuntimeException e) {
// Deliberately swallowing exception because bad framework implementation can
// throw exceptions in ListPopupWindow constructor.
onDatalistPopupDismissed();
return;
}
}
mDatalistPopup.filterAndShow(suggestions, isRtl, false);
if (mWebContentsAccessibility != null) {
mWebContentsAccessibility.onAutofillPopupDisplayed(mDatalistPopup.getListView());
}
}
private void onDatalistPopupDismissed() {
ViewAndroidDelegate delegate = mWebContents.getViewAndroidDelegate();
delegate.removeView(mAnchorView);
mAnchorView = null;
}
private void onSuggestionSelected(String value) {
acceptDataListSuggestion(mNativeAutofillProvider, value);
hidePopup();
}
private void setAnchorViewRect(RectF rect) {
setAnchorViewRect(mNativeAutofillProvider, mAnchorView, rect);
} }
/** /**
* Invoked when current query need to be reset. * Invoked when current query need to be reset.
*/ */
@CalledByNative @CalledByNative
protected abstract void reset(); protected void reset() {
// We don't need to reset anything here, it should be safe to cancel
// current autofill session when new one starts in
// startAutofillSession().
}
@CalledByNative @CalledByNative
protected abstract void setNativeAutofillProvider(long nativeAutofillProvider); protected void setNativeAutofillProvider(long nativeAutofillProvider) {
if (nativeAutofillProvider == mNativeAutofillProvider) return;
// Setting the mNativeAutofillProvider to 0 may occur as a
// result of WebView.destroy, or because a WebView has been
// gc'ed. In the former case we can go ahead and clean up the
// frameworks autofill manager, but in the latter case the
// binder connection has already been dropped in a framework
// finalizer, and so the methods we call will throw. It's not
// possible to know which case we're in, so just catch the exception
// in AutofillManagerWrapper.destroy().
if (mNativeAutofillProvider != 0) mRequest = null;
mNativeAutofillProvider = nativeAutofillProvider;
if (nativeAutofillProvider == 0) mAutofillManager.destroy();
}
@CalledByNative public void setWebContents(WebContents webContents) {
protected abstract void onDidFillAutofillFormData(); if (webContents == mWebContents) return;
if (mWebContents != null) mRequest = null;
mWebContents = webContents;
}
@CalledByNative @CalledByNative
protected abstract void hidePopup(); protected void onDidFillAutofillFormData() {
// The changes were caused by the autofill service autofill form,
// notified it about the result.
forceNotifyFormValues();
}
@CalledByNative private void forceNotifyFormValues() {
protected abstract void showDatalistPopup( if (mRequest == null) return;
String[] datalistValues, String[] datalistLabels, boolean isRtl); for (int i = 0; i < mRequest.getFieldCount(); ++i) {
notifyVirtualValueChanged(i, /* forceNotify = */ true);
}
}
@VisibleForTesting
public AutofillPopup getDatalistPopupForTesting() {
return mDatalistPopup;
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public Rect transformToWindowBounds(RectF rect) {
// Convert bounds to device pixel.
WindowAndroid windowAndroid = mWebContents.getTopLevelNativeWindow();
DisplayAndroid displayAndroid = windowAndroid.getDisplay();
float dipScale = displayAndroid.getDipScale();
RectF bounds = new RectF(rect);
Matrix matrix = new Matrix();
matrix.setScale(dipScale, dipScale);
int[] location = new int[2];
mContainerView.getLocationOnScreen(location);
matrix.postTranslate(location[0], location[1]);
matrix.mapRect(bounds);
return new Rect(
(int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom);
}
/**
* Transform FormFieldData's bounds to ContainView's coordinates and update the bounds with the
* transformed one.
*
* @param formData the form need to be transformed.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public void transformFormFieldToContainViewCoordinates(FormData formData) {
WindowAndroid windowAndroid = mWebContents.getTopLevelNativeWindow();
DisplayAndroid displayAndroid = windowAndroid.getDisplay();
float dipScale = displayAndroid.getDipScale();
Matrix matrix = new Matrix();
matrix.setScale(dipScale, dipScale);
matrix.postTranslate(mContainerView.getScrollX(), mContainerView.getScrollY());
for (FormFieldData field : formData.mFields) {
RectF bounds = new RectF();
matrix.mapRect(bounds, field.getBounds());
field.setBoundsInContainerViewCoordinates(bounds);
}
}
/**
* Send form to renderer for filling.
*
* @param nativeAutofillProvider the native autofill provider.
* @param formData the form to fill.
*/
private void autofill(long nativeAutofillProvider, FormData formData) {
AutofillProviderJni.get().onAutofillAvailable(
nativeAutofillProvider, AutofillProvider.this, formData);
}
private void acceptDataListSuggestion(long nativeAutofillProvider, String value) {
AutofillProviderJni.get().onAcceptDataListSuggestion(
nativeAutofillProvider, AutofillProvider.this, value);
}
private void setAnchorViewRect(long nativeAutofillProvider, View anchorView, RectF rect) {
AutofillProviderJni.get().setAnchorViewRect(nativeAutofillProvider, AutofillProvider.this,
anchorView, rect.left, rect.top, rect.width(), rect.height());
}
@NativeMethods @NativeMethods
interface Natives { interface Natives {
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.autofill;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.Bundle;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStructure;
import android.view.autofill.AutofillValue;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.StrictModeContext;
import org.chromium.base.ThreadUtils;
import org.chromium.base.annotations.VerifiesOnO;
import org.chromium.base.metrics.ScopedSysTraceEvent;
import org.chromium.components.version_info.VersionConstants;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsAccessibility;
import org.chromium.ui.DropdownItem;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.display.DisplayAndroid;
/**
* This class uses Android autofill service to fill web form. All methods are
* supposed to be called in UI thread.
*
* This class doesn't have 1:1 mapping to native AutofillProviderAndroid; the
* normal ownership model is that this object is owned by the embedder-specific
* Java WebContents wrapper (e.g., AwContents.java in //android_webview), and
* AutofillProviderAndroid is owned by the embedder-specific C++ WebContents
* wrapper (e.g., native AwContents in //android_webview).
*
* VerifiesOnO since it causes class verification errors, see crbug.com/991851.
*/
@VerifiesOnO
@TargetApi(Build.VERSION_CODES.O)
public class AutofillProviderImpl extends AutofillProvider {
private static final String TAG = "AutofillProviderImpl";
private static class FocusField {
public final short fieldIndex;
public final Rect absBound;
public FocusField(short fieldIndex, Rect absBound) {
this.fieldIndex = fieldIndex;
this.absBound = absBound;
}
}
/**
* The class to wrap the request to framework.
*
* Though framework guarantees always giving us the autofill value of current
* session, we still want to verify this by using unique virtual id which is
* composed of sessionId and form field index, we don't use the request id
* which comes from renderer as session id because it is not unique.
*/
private static class AutofillRequest {
private static final int INIT_ID = 1; // ID can't be 0 in Android.
private static int sSessionId = INIT_ID;
public final int sessionId;
private FormData mFormData;
private FocusField mFocusField;
public AutofillRequest(FormData formData, FocusField focus) {
sessionId = getNextClientId();
mFormData = formData;
mFocusField = focus;
}
public void fillViewStructure(ViewStructure structure) {
structure.setWebDomain(mFormData.mHost);
structure.setHtmlInfo(structure.newHtmlInfoBuilder("form")
.addAttribute("name", mFormData.mName)
.build());
int index = structure.addChildCount(mFormData.mFields.size());
short fieldIndex = 0;
for (FormFieldData field : mFormData.mFields) {
ViewStructure child = structure.newChild(index++);
int virtualId = toVirtualId(sessionId, fieldIndex++);
child.setAutofillId(structure.getAutofillId(), virtualId);
if (field.mAutocompleteAttr != null && !field.mAutocompleteAttr.isEmpty()) {
child.setAutofillHints(field.mAutocompleteAttr.split(" +"));
}
child.setHint(field.mPlaceholder);
RectF bounds = field.getBoundsInContainerViewCoordinates();
// Field has no scroll.
child.setDimens((int) bounds.left, (int) bounds.top, 0 /* scrollX*/,
0 /* scrollY */, (int) bounds.width(), (int) bounds.height());
ViewStructure.HtmlInfo.Builder builder =
child.newHtmlInfoBuilder("input")
.addAttribute("name", field.mName)
.addAttribute("type", field.mType)
.addAttribute("label", field.mLabel)
.addAttribute("ua-autofill-hints", field.mHeuristicType)
.addAttribute("id", field.mId);
switch (field.getControlType()) {
case FormFieldData.ControlType.LIST:
child.setAutofillType(View.AUTOFILL_TYPE_LIST);
child.setAutofillOptions(field.mOptionContents);
int i = findIndex(field.mOptionValues, field.getValue());
if (i != -1) {
child.setAutofillValue(AutofillValue.forList(i));
}
break;
case FormFieldData.ControlType.TOGGLE:
child.setAutofillType(View.AUTOFILL_TYPE_TOGGLE);
child.setAutofillValue(AutofillValue.forToggle(field.isChecked()));
break;
case FormFieldData.ControlType.TEXT:
case FormFieldData.ControlType.DATALIST:
child.setAutofillType(View.AUTOFILL_TYPE_TEXT);
child.setAutofillValue(AutofillValue.forText(field.getValue()));
if (field.mMaxLength != 0) {
builder.addAttribute("maxlength", String.valueOf(field.mMaxLength));
}
if (field.getControlType() == FormFieldData.ControlType.DATALIST) {
child.setAutofillOptions(field.mDatalistValues);
}
break;
default:
break;
}
child.setHtmlInfo(builder.build());
}
}
public boolean autofill(final SparseArray<AutofillValue> values) {
for (int i = 0; i < values.size(); ++i) {
int id = values.keyAt(i);
if (toSessionId(id) != sessionId) return false;
AutofillValue value = values.get(id);
if (value == null) continue;
short index = toIndex(id);
if (index < 0 || index >= mFormData.mFields.size()) return false;
FormFieldData field = mFormData.mFields.get(index);
if (field == null) return false;
try {
switch (field.getControlType()) {
case FormFieldData.ControlType.LIST:
int j = value.getListValue();
if (j < 0 && j >= field.mOptionValues.length) continue;
field.setAutofillValue(field.mOptionValues[j]);
break;
case FormFieldData.ControlType.TOGGLE:
field.setChecked(value.getToggleValue());
break;
case FormFieldData.ControlType.TEXT:
case FormFieldData.ControlType.DATALIST:
field.setAutofillValue((String) value.getTextValue());
break;
default:
break;
}
} catch (IllegalStateException e) {
// Refer to crbug.com/1080580 .
Log.e(TAG, "The given AutofillValue wasn't expected, abort autofill.", e);
return false;
}
}
return true;
}
public void setFocusField(FocusField focusField) {
mFocusField = focusField;
}
public FocusField getFocusField() {
return mFocusField;
}
public int getFieldCount() {
return mFormData.mFields.size();
}
public AutofillValue getFieldNewValue(int index) {
FormFieldData field = mFormData.mFields.get(index);
if (field == null) return null;
switch (field.getControlType()) {
case FormFieldData.ControlType.LIST:
int i = findIndex(field.mOptionValues, field.getValue());
if (i == -1) return null;
return AutofillValue.forList(i);
case FormFieldData.ControlType.TOGGLE:
return AutofillValue.forToggle(field.isChecked());
case FormFieldData.ControlType.TEXT:
case FormFieldData.ControlType.DATALIST:
return AutofillValue.forText(field.getValue());
default:
return null;
}
}
public int getVirtualId(short index) {
return toVirtualId(sessionId, index);
}
public FormFieldData getField(short index) {
return mFormData.mFields.get(index);
}
private static int findIndex(String[] values, String value) {
if (values != null && value != null) {
for (int i = 0; i < values.length; i++) {
if (value.equals(values[i])) return i;
}
}
return -1;
}
private static int getNextClientId() {
ThreadUtils.assertOnUiThread();
if (sSessionId == 0xffff) sSessionId = INIT_ID;
return sSessionId++;
}
private static int toSessionId(int virtualId) {
return (virtualId & 0xffff0000) >> 16;
}
private static short toIndex(int virtualId) {
return (short) (virtualId & 0xffff);
}
private static int toVirtualId(int clientId, short index) {
return (clientId << 16) | index;
}
}
private final String mProviderName;
private AutofillManagerWrapper mAutofillManager;
private ViewGroup mContainerView;
private WebContents mWebContents;
private AutofillRequest mRequest;
private long mNativeAutofillProvider;
private AutofillProviderUMA mAutofillUMA;
private AutofillManagerWrapper.InputUIObserver mInputUIObserver;
private long mAutofillTriggeredTimeMillis;
private Context mContext;
private AutofillPopup mDatalistPopup;
private WebContentsAccessibility mWebContentsAccessibility;
private View mAnchorView;
public AutofillProviderImpl(Context context, ViewGroup containerView, String providerName) {
this(containerView, new AutofillManagerWrapper(context), context, providerName);
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public AutofillProviderImpl(ViewGroup containerView, AutofillManagerWrapper manager,
Context context, String providerName) {
mProviderName = providerName;
try (ScopedSysTraceEvent e =
ScopedSysTraceEvent.scoped("AutofillProviderImpl.constructor")) {
assert Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
mAutofillManager = manager;
mContainerView = containerView;
mAutofillUMA = new AutofillProviderUMA(context);
mInputUIObserver = new AutofillManagerWrapper.InputUIObserver() {
@Override
public void onInputUIShown() {
// Not need to report suggestion window displayed if there is no live autofill
// session.
if (mRequest == null) return;
mAutofillUMA.onSuggestionDisplayed(
System.currentTimeMillis() - mAutofillTriggeredTimeMillis);
}
};
mAutofillManager.addInputUIObserver(mInputUIObserver);
mContext = context;
}
}
@Override
public void onContainerViewChanged(ViewGroup containerView) {
mContainerView = containerView;
}
@Override
public void onProvideAutoFillVirtualStructure(ViewStructure structure, int flags) {
// This method could be called for the session started by the native
// control outside of the scope of autofill, e.g. the URL bar, in this case, we simply
// return.
if (mRequest == null) return;
Bundle bundle = structure.getExtras();
if (bundle != null) {
bundle.putCharSequence("VIRTUAL_STRUCTURE_PROVIDER_NAME", mProviderName);
bundle.putCharSequence(
"VIRTUAL_STRUCTURE_PROVIDER_VERSION", VersionConstants.PRODUCT_VERSION);
}
mRequest.fillViewStructure(structure);
if (AutofillManagerWrapper.isLoggable()) {
AutofillManagerWrapper.log(
"onProvideAutoFillVirtualStructure fields:" + structure.getChildCount());
}
mAutofillUMA.onVirtualStructureProvided();
}
@Override
public void autofill(final SparseArray<AutofillValue> values) {
if (mNativeAutofillProvider != 0 && mRequest != null && mRequest.autofill((values))) {
autofill(mNativeAutofillProvider, mRequest.mFormData);
if (AutofillManagerWrapper.isLoggable()) {
AutofillManagerWrapper.log("autofill values:" + values.size());
}
mAutofillUMA.onAutofill();
}
}
@Override
public boolean shouldQueryAutofillSuggestion() {
return mRequest != null && mRequest.getFocusField() != null
&& !mAutofillManager.isAutofillInputUIShowing();
}
@Override
public void queryAutofillSuggestion() {
if (shouldQueryAutofillSuggestion()) {
FocusField focusField = mRequest.getFocusField();
mAutofillManager.requestAutofill(mContainerView,
mRequest.getVirtualId(focusField.fieldIndex), focusField.absBound);
}
}
@Override
public void startAutofillSession(
FormData formData, int focus, float x, float y, float width, float height) {
// Check focusField inside short value?
// Autofill Manager might have session that wasn't started by AutofillProviderImpl,
// we just always cancel existing session here.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
mAutofillManager.cancel();
}
Rect absBound = transformToWindowBounds(new RectF(x, y, x + width, y + height));
if (mRequest != null) notifyViewExitBeforeDestroyRequest();
transformFormFieldToContainViewCoordinates(formData);
mRequest = new AutofillRequest(formData, new FocusField((short) focus, absBound));
int virtualId = mRequest.getVirtualId((short) focus);
notifyVirtualViewEntered(mContainerView, virtualId, absBound);
mAutofillUMA.onSessionStarted(mAutofillManager.isDisabled());
mAutofillTriggeredTimeMillis = System.currentTimeMillis();
mAutofillManager.notifyNewSessionStarted();
}
@Override
public void onFormFieldDidChange(int index, float x, float y, float width, float height) {
// Check index inside short value?
if (mRequest == null) return;
short sIndex = (short) index;
FocusField focusField = mRequest.getFocusField();
if (focusField == null || sIndex != focusField.fieldIndex) {
onFocusChangedImpl(true, index, x, y, width, height, true /*causedByValueChange*/);
} else {
// Currently there is no api to notify both value and position
// change, before the API is available, we still need to call
// notifyVirtualViewEntered() to tell current coordinates because
// the position could be changed.
int virtualId = mRequest.getVirtualId(sIndex);
Rect absBound = transformToWindowBounds(new RectF(x, y, x + width, y + height));
if (!focusField.absBound.equals(absBound)) {
notifyVirtualViewExited(mContainerView, virtualId);
notifyVirtualViewEntered(mContainerView, virtualId, absBound);
// Update focus field position.
mRequest.setFocusField(new FocusField(focusField.fieldIndex, absBound));
}
}
notifyVirtualValueChanged(index, /* forceNotify = */ false);
mAutofillUMA.onUserChangeFieldValue(mRequest.getField(sIndex).hasPreviouslyAutofilled());
}
@Override
public void onTextFieldDidScroll(int index, float x, float y, float width, float height) {
// crbug.com/730764 - from P and above, Android framework listens to the onScrollChanged()
// and repositions the autofill UI automatically.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) return;
if (mRequest == null) return;
short sIndex = (short) index;
FocusField focusField = mRequest.getFocusField();
if (focusField == null || sIndex != focusField.fieldIndex) return;
int virtualId = mRequest.getVirtualId(sIndex);
Rect absBound = transformToWindowBounds(new RectF(x, y, x + width, y + height));
// Notify the new position to the Android framework. Note that we do not call
// notifyVirtualViewExited() here intentionally to avoid flickering.
notifyVirtualViewEntered(mContainerView, virtualId, absBound);
// Update focus field position.
mRequest.setFocusField(new FocusField(focusField.fieldIndex, absBound));
}
private boolean isDatalistField(int childId) {
FormFieldData field = mRequest.getField((short) childId);
return field.mControlType == FormFieldData.ControlType.DATALIST;
}
private void notifyVirtualValueChanged(int index, boolean forceNotify) {
// The ValueChanged, ViewEntered and ViewExited aren't notified to the autofill service for
// the focused datalist to avoid the potential UI conflict.
// The datalist support was added later and the option list is displayed by WebView, the
// autofill service might also show its suggestions when the datalist (associated the input
// field) is focused, the two UI overlap, the solution is to completely hide the fact that
// the datalist is being focused to the autofill service to prevent it from displaying the
// suggestion.
// The ValueChange will still be sent to autofill service when the form
// submitted or autofilled.
if (!forceNotify && isDatalistField(index)) return;
AutofillValue autofillValue = mRequest.getFieldNewValue(index);
if (autofillValue == null) return;
mAutofillManager.notifyVirtualValueChanged(
mContainerView, mRequest.getVirtualId((short) index), autofillValue);
}
private void notifyVirtualViewEntered(View parent, int childId, Rect absBounds) {
// Refer to notifyVirtualValueChanged() for the reason of the datalist's special handling.
if (isDatalistField(childId)) return;
mAutofillManager.notifyVirtualViewEntered(parent, childId, absBounds);
}
private void notifyVirtualViewExited(View parent, int childId) {
// Refer to notifyVirtualValueChanged() for the reason of the datalist's special handling.
if (isDatalistField(childId)) return;
mAutofillManager.notifyVirtualViewExited(parent, childId);
}
@Override
public void onFormSubmitted(int submissionSource) {
// The changes could be missing, like those made by Javascript, we'd better to notify
// AutofillManager current values. also see crbug.com/353001 and crbug.com/732856.
forceNotifyFormValues();
mAutofillManager.commit(submissionSource);
mRequest = null;
mAutofillUMA.onFormSubmitted(submissionSource);
}
@Override
public void onFocusChanged(
boolean focusOnForm, int focusField, float x, float y, float width, float height) {
onFocusChangedImpl(
focusOnForm, focusField, x, y, width, height, false /*causedByValueChange*/);
}
@Override
protected void hidePopup() {
if (mDatalistPopup != null) {
mDatalistPopup.dismiss();
mDatalistPopup = null;
}
if (mWebContentsAccessibility != null) {
mWebContentsAccessibility.onAutofillPopupDismissed();
}
}
private void notifyViewExitBeforeDestroyRequest() {
if (mRequest == null) return;
FocusField focusField = mRequest.getFocusField();
if (focusField == null) return;
notifyVirtualViewExited(mContainerView, mRequest.getVirtualId(focusField.fieldIndex));
mRequest.setFocusField(null);
}
private void onFocusChangedImpl(boolean focusOnForm, int focusField, float x, float y,
float width, float height, boolean causedByValueChange) {
// Check focusField inside short value?
// FocusNoLongerOnForm is called after form submitted.
if (mRequest == null) return;
FocusField prev = mRequest.getFocusField();
if (focusOnForm) {
Rect absBound = transformToWindowBounds(new RectF(x, y, x + width, y + height));
if (prev != null && prev.fieldIndex == focusField && absBound.equals(prev.absBound)) {
return;
}
// Notify focus changed.
if (prev != null) {
notifyVirtualViewExited(mContainerView, mRequest.getVirtualId(prev.fieldIndex));
}
notifyVirtualViewEntered(
mContainerView, mRequest.getVirtualId((short) focusField), absBound);
if (!causedByValueChange) {
// The focus field value might not sync with platform's
// AutofillManager, just notify it value changed.
notifyVirtualValueChanged(focusField, /* forceNotify = */ false);
mAutofillTriggeredTimeMillis = System.currentTimeMillis();
}
mRequest.setFocusField(new FocusField((short) focusField, absBound));
} else {
if (prev == null) return;
// Notify focus changed.
notifyVirtualViewExited(mContainerView, mRequest.getVirtualId(prev.fieldIndex));
mRequest.setFocusField(null);
}
}
@Override
protected void showDatalistPopup(
String[] datalistValues, String[] datalistLabels, boolean isRtl) {
if (mRequest == null) return;
FocusField focusField = mRequest.getFocusField();
if (focusField != null) {
showDatalistPopup(datalistValues, datalistLabels,
mRequest.getField(focusField.fieldIndex).getBounds(), isRtl);
}
}
/**
* Display the simplest popup for the datalist. This is same as WebView's datalist popup in
* Android pre-o. No suggestion from the autofill service will be presented, No advance
* features of AutofillPopup are used.
*/
private void showDatalistPopup(
String[] datalistValues, String[] datalistLabels, RectF bounds, boolean isRtl) {
final AutofillSuggestion[] suggestions = new AutofillSuggestion[datalistValues.length];
for (int i = 0; i < suggestions.length; i++) {
suggestions[i] = new AutofillSuggestion(datalistValues[i], datalistLabels[i],
DropdownItem.NO_ICON, false /* isIconAtLeft */, i, false /* isDeletable */,
false /* isMultilineLabel */, false /* isBoldLabel */);
}
if (mWebContentsAccessibility == null) {
mWebContentsAccessibility = WebContentsAccessibility.fromWebContents(mWebContents);
}
if (mDatalistPopup == null) {
if (ContextUtils.activityFromContext(mContext) == null) return;
ViewAndroidDelegate delegate = mWebContents.getViewAndroidDelegate();
if (mAnchorView == null) mAnchorView = delegate.acquireView();
setAnchorViewRect(bounds);
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
mDatalistPopup = new AutofillPopup(mContext, mAnchorView, new AutofillDelegate() {
@Override
public void dismissed() {
onDatalistPopupDismissed();
}
@Override
public void suggestionSelected(int listIndex) {
onSuggestionSelected(suggestions[listIndex].getLabel());
}
@Override
public void deleteSuggestion(int listIndex) {}
@Override
public void accessibilityFocusCleared() {
mWebContentsAccessibility.onAutofillPopupAccessibilityFocusCleared();
}
});
} catch (RuntimeException e) {
// Deliberately swallowing exception because bad framework implementation can
// throw exceptions in ListPopupWindow constructor.
onDatalistPopupDismissed();
return;
}
}
mDatalistPopup.filterAndShow(suggestions, isRtl, false);
if (mWebContentsAccessibility != null) {
mWebContentsAccessibility.onAutofillPopupDisplayed(mDatalistPopup.getListView());
}
}
private void onDatalistPopupDismissed() {
ViewAndroidDelegate delegate = mWebContents.getViewAndroidDelegate();
delegate.removeView(mAnchorView);
mAnchorView = null;
}
private void onSuggestionSelected(String value) {
acceptDataListSuggestion(mNativeAutofillProvider, value);
hidePopup();
}
private void setAnchorViewRect(RectF rect) {
setAnchorViewRect(mNativeAutofillProvider, mAnchorView, rect);
}
@Override
protected void reset() {
// We don't need to reset anything here, it should be safe to cancel
// current autofill session when new one starts in
// startAutofillSession().
}
@Override
protected void setNativeAutofillProvider(long nativeAutofillProvider) {
if (nativeAutofillProvider == mNativeAutofillProvider) return;
// Setting the mNativeAutofillProvider to 0 may occur as a
// result of WebView.destroy, or because a WebView has been
// gc'ed. In the former case we can go ahead and clean up the
// frameworks autofill manager, but in the latter case the
// binder connection has already been dropped in a framework
// finalizer, and so the methods we call will throw. It's not
// possible to know which case we're in, so just catch the exception
// in AutofillManagerWrapper.destroy().
if (mNativeAutofillProvider != 0) mRequest = null;
mNativeAutofillProvider = nativeAutofillProvider;
if (nativeAutofillProvider == 0) mAutofillManager.destroy();
}
@Override
public void setWebContents(WebContents webContents) {
if (webContents == mWebContents) return;
if (mWebContents != null) mRequest = null;
mWebContents = webContents;
}
@Override
protected void onDidFillAutofillFormData() {
// The changes were caused by the autofill service autofill form,
// notified it about the result.
forceNotifyFormValues();
}
private void forceNotifyFormValues() {
if (mRequest == null) return;
for (int i = 0; i < mRequest.getFieldCount(); ++i) {
notifyVirtualValueChanged(i, /* forceNotify = */ true);
}
}
@VisibleForTesting
public AutofillPopup getDatalistPopupForTesting() {
return mDatalistPopup;
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public Rect transformToWindowBounds(RectF rect) {
// Convert bounds to device pixel.
WindowAndroid windowAndroid = mWebContents.getTopLevelNativeWindow();
DisplayAndroid displayAndroid = windowAndroid.getDisplay();
float dipScale = displayAndroid.getDipScale();
RectF bounds = new RectF(rect);
Matrix matrix = new Matrix();
matrix.setScale(dipScale, dipScale);
int[] location = new int[2];
mContainerView.getLocationOnScreen(location);
matrix.postTranslate(location[0], location[1]);
matrix.mapRect(bounds);
return new Rect(
(int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom);
}
/**
* Transform FormFieldData's bounds to ContainView's coordinates and update the bounds with the
* transformed one.
*
* @param formData the form need to be transformed.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public void transformFormFieldToContainViewCoordinates(FormData formData) {
WindowAndroid windowAndroid = mWebContents.getTopLevelNativeWindow();
DisplayAndroid displayAndroid = windowAndroid.getDisplay();
float dipScale = displayAndroid.getDipScale();
Matrix matrix = new Matrix();
matrix.setScale(dipScale, dipScale);
matrix.postTranslate(mContainerView.getScrollX(), mContainerView.getScrollY());
for (FormFieldData field : formData.mFields) {
RectF bounds = new RectF();
matrix.mapRect(bounds, field.getBounds());
field.setBoundsInContainerViewCoordinates(bounds);
}
}
}
...@@ -9,8 +9,7 @@ java_library("components_autofill_junit_tests") { ...@@ -9,8 +9,7 @@ java_library("components_autofill_junit_tests") {
# Platform checks are broken for Robolectric. See https://crbug.com/1071638. # Platform checks are broken for Robolectric. See https://crbug.com/1071638.
bypass_platform_checks = true bypass_platform_checks = true
testonly = true testonly = true
sources = sources = [ "src/org/chromium/components/autofill/AutofillProviderTest.java" ]
[ "src/org/chromium/components/autofill/AutofillProviderImplTest.java" ]
deps = [ deps = [
"//base:base_java_test_support", "//base:base_java_test_support",
"//base:base_junit_test_support", "//base:base_junit_test_support",
......
...@@ -31,11 +31,11 @@ import org.chromium.ui.display.DisplayAndroid; ...@@ -31,11 +31,11 @@ import org.chromium.ui.display.DisplayAndroid;
import java.util.ArrayList; import java.util.ArrayList;
/** /**
* The unit tests for AutofillProviderImpl. * The unit tests for AutofillProvider.
*/ */
@RunWith(BaseRobolectricTestRunner.class) @RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE) @Config(manifest = Config.NONE)
public class AutofillProviderImplTest { public class AutofillProviderTest {
private static final float EXPECTED_DIP_SCALE = 2; private static final float EXPECTED_DIP_SCALE = 2;
private static final int SCROLL_X = 15; private static final int SCROLL_X = 15;
private static final int SCROLL_Y = 155; private static final int SCROLL_Y = 155;
...@@ -46,7 +46,7 @@ public class AutofillProviderImplTest { ...@@ -46,7 +46,7 @@ public class AutofillProviderImplTest {
private WindowAndroid mWindowAndroid; private WindowAndroid mWindowAndroid;
private WebContents mWebContents; private WebContents mWebContents;
private ViewGroup mContainerView; private ViewGroup mContainerView;
private AutofillProviderImpl mAutofillProvider; private AutofillProvider mAutofillProvider;
private DisplayAndroid mDisplayAndroid; private DisplayAndroid mDisplayAndroid;
@Before @Before
...@@ -57,8 +57,7 @@ public class AutofillProviderImplTest { ...@@ -57,8 +57,7 @@ public class AutofillProviderImplTest {
mDisplayAndroid = Mockito.mock(DisplayAndroid.class); mDisplayAndroid = Mockito.mock(DisplayAndroid.class);
mWebContents = Mockito.mock(WebContents.class); mWebContents = Mockito.mock(WebContents.class);
mContainerView = Mockito.mock(ViewGroup.class); mContainerView = Mockito.mock(ViewGroup.class);
mAutofillProvider = mAutofillProvider = new AutofillProvider(mContext, mContainerView, "AutofillProviderTest");
new AutofillProviderImpl(mContext, mContainerView, "AutofillProviderImplTest");
mAutofillProvider.setWebContents(mWebContents); mAutofillProvider.setWebContents(mWebContents);
when(mWebContents.getTopLevelNativeWindow()).thenReturn(mWindowAndroid); when(mWebContents.getTopLevelNativeWindow()).thenReturn(mWindowAndroid);
......
...@@ -26,7 +26,6 @@ import org.chromium.base.annotations.JNINamespace; ...@@ -26,7 +26,6 @@ import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods; import org.chromium.base.annotations.NativeMethods;
import org.chromium.components.autofill.AutofillActionModeCallback; import org.chromium.components.autofill.AutofillActionModeCallback;
import org.chromium.components.autofill.AutofillProvider; import org.chromium.components.autofill.AutofillProvider;
import org.chromium.components.autofill.AutofillProviderImpl;
import org.chromium.components.browser_ui.http_auth.LoginPrompt; import org.chromium.components.browser_ui.http_auth.LoginPrompt;
import org.chromium.components.browser_ui.util.BrowserControlsVisibilityDelegate; import org.chromium.components.browser_ui.util.BrowserControlsVisibilityDelegate;
import org.chromium.components.browser_ui.util.ComposedBrowserControlsVisibilityDelegate; import org.chromium.components.browser_ui.util.ComposedBrowserControlsVisibilityDelegate;
...@@ -291,7 +290,7 @@ public final class TabImpl extends ITab.Stub implements LoginPrompt.Observer { ...@@ -291,7 +290,7 @@ public final class TabImpl extends ITab.Stub implements LoginPrompt.Observer {
// Set up |mAutofillProvider| to operate in the new Context. It's safe to assume // Set up |mAutofillProvider| to operate in the new Context. It's safe to assume
// the context won't change unless it is first nulled out, since the fragment // the context won't change unless it is first nulled out, since the fragment
// must be detached before it can be reattached to a new Context. // must be detached before it can be reattached to a new Context.
mAutofillProvider = new AutofillProviderImpl( mAutofillProvider = new AutofillProvider(
mBrowser.getContext(), mBrowser.getAutofillView(), "WebLayer"); mBrowser.getContext(), mBrowser.getAutofillView(), "WebLayer");
TabImplJni.get().onAutofillProviderChanged(mNativeTab, mAutofillProvider); TabImplJni.get().onAutofillProviderChanged(mNativeTab, mAutofillProvider);
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment