summaryrefslogtreecommitdiff
path: root/chromium/components/autofill/android/provider/java
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2020-10-12 14:27:29 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2020-10-13 09:35:20 +0000
commitc30a6232df03e1efbd9f3b226777b07e087a1122 (patch)
treee992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/components/autofill/android/provider/java
parent7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff)
downloadqtwebengine-chromium-85-based.tar.gz
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057 Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/components/autofill/android/provider/java')
-rw-r--r--chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillActionModeCallback.java60
-rw-r--r--chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillManagerWrapper.java206
-rw-r--r--chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillProvider.java786
-rw-r--r--chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillProviderUMA.java255
-rw-r--r--chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/FormData.java56
-rw-r--r--chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/FormFieldData.java162
6 files changed, 1525 insertions, 0 deletions
diff --git a/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillActionModeCallback.java b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillActionModeCallback.java
new file mode 100644
index 00000000000..6921fcab52c
--- /dev/null
+++ b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillActionModeCallback.java
@@ -0,0 +1,60 @@
+// 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.content.Context;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuItem;
+
+/**
+ * The class to implement autofill context menu. To match the Android native view behavior, the
+ * autofill context menu only appears when there is no text selected.
+ */
+public class AutofillActionModeCallback implements ActionMode.Callback {
+ private final Context mContext;
+ private final AutofillProvider mAutofillProvider;
+ private final int mAutofillMenuItemTitle;
+ private final int mAutofillMenuItem;
+
+ public AutofillActionModeCallback(Context context, AutofillProvider autofillProvider) {
+ mContext = context;
+ mAutofillProvider = autofillProvider;
+ // TODO(michaelbai): Uses the resource directly after sdk roll to Android O MR1.
+ // crbug.com/740628
+ mAutofillMenuItemTitle =
+ mContext.getResources().getIdentifier("autofill", "string", "android");
+ mAutofillMenuItem = mContext.getResources().getIdentifier("autofill", "id", "android");
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ return mAutofillMenuItemTitle != 0 && mAutofillMenuItem != 0;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ if (mAutofillMenuItemTitle != 0 && mAutofillProvider.shouldQueryAutofillSuggestion()) {
+ MenuItem item = menu.add(
+ Menu.NONE, mAutofillMenuItem, Menu.CATEGORY_SECONDARY, mAutofillMenuItemTitle);
+ item.setShowAsActionFlags(
+ MenuItem.SHOW_AS_ACTION_NEVER | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ if (item.getItemId() == mAutofillMenuItem) {
+ mAutofillProvider.queryAutofillSuggestion();
+ mode.finish();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {}
+}
diff --git a/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillManagerWrapper.java b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillManagerWrapper.java
new file mode 100644
index 00000000000..0287ac38f97
--- /dev/null
+++ b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillManagerWrapper.java
@@ -0,0 +1,206 @@
+// 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.Rect;
+import android.os.Build;
+import android.view.View;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+
+import androidx.annotation.VisibleForTesting;
+
+import org.chromium.base.Log;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * The class to call Android's AutofillManager.
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public class AutofillManagerWrapper {
+ // Don't change TAG, it is used for runtime log.
+ // NOTE: As a result of the above, the tag below still references the name of this class from
+ // when it was originally developed specifically for Android WebView.
+ public static final String TAG = "AwAutofillManager";
+
+ /**
+ * The observer of suggestion window.
+ */
+ public static interface InputUIObserver { void onInputUIShown(); }
+
+ private static class AutofillInputUIMonitor extends AutofillManager.AutofillCallback {
+ private WeakReference<AutofillManagerWrapper> mManager;
+
+ public AutofillInputUIMonitor(AutofillManagerWrapper manager) {
+ mManager = new WeakReference<AutofillManagerWrapper>(manager);
+ }
+
+ @Override
+ public void onAutofillEvent(View view, int virtualId, int event) {
+ AutofillManagerWrapper manager = mManager.get();
+ if (manager == null) return;
+ manager.mIsAutofillInputUIShowing = (event == EVENT_INPUT_SHOWN);
+ if (event == EVENT_INPUT_SHOWN) manager.notifyInputUIChange();
+ }
+ }
+
+ private static boolean sIsLoggable;
+ private AutofillManager mAutofillManager;
+ private boolean mIsAutofillInputUIShowing;
+ private AutofillInputUIMonitor mMonitor;
+ private boolean mDestroyed;
+ private boolean mDisabled;
+ private ArrayList<WeakReference<InputUIObserver>> mInputUIObservers;
+
+ public AutofillManagerWrapper(Context context) {
+ updateLogStat();
+ if (isLoggable()) log("constructor");
+ mAutofillManager = context.getSystemService(AutofillManager.class);
+ mDisabled = mAutofillManager == null || !mAutofillManager.isEnabled();
+ if (mDisabled) {
+ if (isLoggable()) log("disabled");
+ return;
+ }
+
+ mMonitor = new AutofillInputUIMonitor(this);
+ mAutofillManager.registerCallback(mMonitor);
+ }
+
+ public void notifyVirtualValueChanged(View parent, int childId, AutofillValue value) {
+ if (mDisabled || checkAndWarnIfDestroyed()) return;
+ if (isLoggable()) log("notifyVirtualValueChanged");
+ mAutofillManager.notifyValueChanged(parent, childId, value);
+ }
+
+ public void commit(int submissionSource) {
+ if (mDisabled || checkAndWarnIfDestroyed()) return;
+ if (isLoggable()) log("commit source:" + submissionSource);
+ mAutofillManager.commit();
+ }
+
+ public void cancel() {
+ if (mDisabled || checkAndWarnIfDestroyed()) return;
+ if (isLoggable()) log("cancel");
+ mAutofillManager.cancel();
+ }
+
+ public void notifyVirtualViewEntered(View parent, int childId, Rect absBounds) {
+ // Log warning only when the autofill is triggered.
+ if (mDisabled) {
+ Log.w(TAG, "Autofill is disabled: AutofillManager isn't available in given Context.");
+ return;
+ }
+ if (checkAndWarnIfDestroyed()) return;
+ if (isLoggable()) log("notifyVirtualViewEntered");
+ mAutofillManager.notifyViewEntered(parent, childId, absBounds);
+ }
+
+ public void notifyVirtualViewExited(View parent, int childId) {
+ if (mDisabled || checkAndWarnIfDestroyed()) return;
+ if (isLoggable()) log("notifyVirtualViewExited");
+ mAutofillManager.notifyViewExited(parent, childId);
+ }
+
+ public void requestAutofill(View parent, int virtualId, Rect absBounds) {
+ if (mDisabled || checkAndWarnIfDestroyed()) return;
+ if (isLoggable()) log("requestAutofill");
+ mAutofillManager.requestAutofill(parent, virtualId, absBounds);
+ }
+
+ public boolean isAutofillInputUIShowing() {
+ if (mDisabled || checkAndWarnIfDestroyed()) return false;
+ if (isLoggable()) log("isAutofillInputUIShowing: " + mIsAutofillInputUIShowing);
+ return mIsAutofillInputUIShowing;
+ }
+
+ public void destroy() {
+ if (mDisabled || checkAndWarnIfDestroyed()) return;
+ if (isLoggable()) log("destroy");
+ try {
+ // The binder in the autofill service side might already be dropped,
+ // unregisterCallback() will cause various exceptions in this
+ // scenario (see crbug.com/1078337), catching RuntimeException here prevents crash.
+ mAutofillManager.unregisterCallback(mMonitor);
+ } catch (RuntimeException e) {
+ // We are not logging anything here since some of the exceptions are raised as 'generic'
+ // RuntimeException which makes it difficult to catch and ignore separately; and the
+ // RuntimeException seemed only happen in Android O, therefore, isn't actionable.
+ } finally {
+ mAutofillManager = null;
+ mDestroyed = true;
+ }
+ }
+
+ public boolean isDisabled() {
+ return mDisabled;
+ }
+
+ private boolean checkAndWarnIfDestroyed() {
+ if (mDestroyed) {
+ Log.w(TAG, "Application attempted to call on a destroyed AutofillManagerWrapper",
+ new Throwable());
+ }
+ return mDestroyed;
+ }
+
+ public void addInputUIObserver(InputUIObserver observer) {
+ if (observer == null) return;
+ if (mInputUIObservers == null) {
+ mInputUIObservers = new ArrayList<WeakReference<InputUIObserver>>();
+ }
+ mInputUIObservers.add(new WeakReference<InputUIObserver>(observer));
+ }
+
+ public void removeInputUIObserver(InputUIObserver observer) {
+ if (observer == null) return;
+ for (Iterator<WeakReference<InputUIObserver>> i = mInputUIObservers.listIterator();
+ i.hasNext();) {
+ WeakReference<InputUIObserver> o = i.next();
+ if (o.get() == null || o.get() == observer) i.remove();
+ }
+ }
+
+ @VisibleForTesting
+ public void notifyInputUIChange() {
+ for (Iterator<WeakReference<InputUIObserver>> i = mInputUIObservers.listIterator();
+ i.hasNext();) {
+ WeakReference<InputUIObserver> o = i.next();
+ InputUIObserver observer = o.get();
+ if (observer == null) {
+ i.remove();
+ continue;
+ }
+ observer.onInputUIShown();
+ }
+ }
+
+ public void notifyNewSessionStarted() {
+ updateLogStat();
+ if (isLoggable()) log("Session starts");
+ }
+
+ /**
+ * Always check isLoggable() before call this method.
+ */
+ public static void log(String log) {
+ // Log.i() instead of Log.d() is used here because log.d() is stripped out in release build.
+ Log.i(TAG, log);
+ }
+
+ public static boolean isLoggable() {
+ return sIsLoggable;
+ }
+
+ private static void updateLogStat() {
+ // Use 'setprop log.tag.AwAutofillManager DEBUG' to enable the log at runtime.
+ // NOTE: See the comment on TAG above for why this is still AwAutofillManager.
+ sIsLoggable = Log.isLoggable(TAG, Log.DEBUG);
+ }
+}
diff --git a/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillProvider.java b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillProvider.java
new file mode 100644
index 00000000000..acbf7a61a22
--- /dev/null
+++ b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillProvider.java
@@ -0,0 +1,786 @@
+// 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.CalledByNative;
+import org.chromium.base.annotations.JNINamespace;
+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.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 works with Android autofill service to fill web form, it doesn't use chrome's
+ * autofill service or suggestion UI. All methods are supposed to be called in UI thread.
+ *
+ * AutofillProvider handles one autofill session at time, each call of
+ * queryFormFieldAutofill cancels previous session and starts a new one, the
+ * 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")
+public class 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;
+ }
+ }
+ /**
+ * 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 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 container view is changed.
+ *
+ * @param containerView new container view.
+ */
+ public void onContainerViewChanged(ViewGroup containerView) {
+ mContainerView = containerView;
+ }
+
+ /**
+ * Invoked when autofill service needs the form structure.
+ *
+ * @param structure see View.onProvideAutofillVirtualStructure()
+ * @param flags see View.onProvideAutofillVirtualStructure()
+ */
+ 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.
+ */
+ public boolean shouldQueryAutofillSuggestion() {
+ return mRequest != null && mRequest.getFocusField() != null
+ && !mAutofillManager.isAutofillInputUIShowing();
+ }
+
+ 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
+ * service for the values with which to fill the form.
+ *
+ * @param formData the form needs to fill.
+ * @param focus the index of focus field in formData
+ * @param x the boundary of focus field.
+ * @param y the boundary of focus field.
+ * @param width the boundary of focus field.
+ * @param height the boundary of focus field.
+ */
+ @CalledByNative
+ 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 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.
+ *
+ * @param index index of field in current form.
+ * @param x the boundary of focus field.
+ * @param y the boundary of focus field.
+ * @param width the boundary of focus field.
+ * @param height the boundary of focus field.
+ *
+ */
+ @CalledByNative
+ 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());
+ }
+
+ /**
+ * Invoked when text field is scrolled.
+ *
+ * @param index index of field in current form.
+ * @param x the boundary of focus field.
+ * @param y the boundary of focus field.
+ * @param width the boundary of focus field.
+ * @param height the boundary of focus field.
+ *
+ */
+ @CalledByNative
+ 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);
+ }
+
+ /**
+ * Invoked when current form will be submitted.
+ * @param submissionSource the submission source, could be any member defined in
+ * SubmissionSource.java
+ */
+ @CalledByNative
+ 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.
+ *
+ * @param focusOnForm whether focus is still on form.
+ * @param focusItem the index of field has focus
+ * @param x the boundary of focus field.
+ * @param y the boundary of focus field.
+ * @param width the boundary of focus field.
+ * @param height the boundary of focus field.
+ */
+ @CalledByNative
+ public void onFocusChanged(
+ boolean focusOnForm, int focusField, float x, float y, float width, float height) {
+ onFocusChangedImpl(
+ focusOnForm, focusField, x, y, width, height, false /*causedByValueChange*/);
+ }
+
+ @CalledByNative
+ public 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);
+ }
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Invoked when current query need to be reset.
+ */
+ @CalledByNative
+ 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
+ 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();
+ }
+
+ public void setWebContents(WebContents webContents) {
+ if (webContents == mWebContents) return;
+ if (mWebContents != null) mRequest = null;
+ mWebContents = webContents;
+ }
+
+ @CalledByNative
+ 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);
+ }
+ }
+
+ /**
+ * 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
+ interface Natives {
+ void onAutofillAvailable(
+ long nativeAutofillProviderAndroid, AutofillProvider caller, FormData formData);
+
+ void onAcceptDataListSuggestion(
+ long nativeAutofillProviderAndroid, AutofillProvider caller, String value);
+
+ void setAnchorViewRect(long nativeAutofillProviderAndroid, AutofillProvider caller,
+ View anchorView, float x, float y, float width, float height);
+ }
+}
diff --git a/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillProviderUMA.java b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillProviderUMA.java
new file mode 100644
index 00000000000..394cd849c95
--- /dev/null
+++ b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillProviderUMA.java
@@ -0,0 +1,255 @@
+// Copyright 2018 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.content.Context;
+
+import org.chromium.autofill.mojom.SubmissionSource;
+import org.chromium.base.ContextUtils;
+import org.chromium.base.metrics.RecordHistogram;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The class for AutofillProvider-related UMA. Note that most of the concrete histogram
+ * names include "WebView"; when this class was originally developed it was WebView-specific,
+ * and when generalizing it we did not change these names to maintain continuity when
+ * analyzing the histograms.
+ */
+public class AutofillProviderUMA {
+ // Records whether the Autofill service is enabled or not.
+ public static final String UMA_AUTOFILL_ENABLED = "Autofill.WebView.Enabled";
+
+ // Records whether the Autofill provider is created by activity context or not.
+ public static final String UMA_AUTOFILL_CREATED_BY_ACTIVITY_CONTEXT =
+ "Autofill.WebView.CreatedByActivityContext";
+
+ // Records what happened in an autofill session.
+ public static final String UMA_AUTOFILL_AUTOFILL_SESSION = "Autofill.WebView.AutofillSession";
+ // The possible value of UMA_AUTOFILL_AUTOFILL_SESSION.
+ public static final int SESSION_UNKNOWN = 0;
+ public static final int NO_CALLBACK_FORM_FRAMEWORK = 1;
+ public static final int NO_SUGGESTION_USER_CHANGE_FORM_FORM_SUBMITTED = 2;
+ public static final int NO_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED = 3;
+ public static final int NO_SUGGESTION_USER_NOT_CHANGE_FORM_FORM_SUBMITTED = 4;
+ public static final int NO_SUGGESTION_USER_NOT_CHANGE_FORM_NO_FORM_SUBMITTED = 5;
+ public static final int USER_SELECT_SUGGESTION_USER_CHANGE_FORM_FORM_SUBMITTED = 6;
+ public static final int USER_SELECT_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED = 7;
+ public static final int USER_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_FORM_SUBMITTED = 8;
+ public static final int USER_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_NO_FORM_SUBMITTED = 9;
+ public static final int USER_NOT_SELECT_SUGGESTION_USER_CHANGE_FORM_FORM_SUBMITTED = 10;
+ public static final int USER_NOT_SELECT_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED = 11;
+ public static final int USER_NOT_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_FORM_SUBMITTED = 12;
+ public static final int USER_NOT_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_NO_FORM_SUBMITTED = 13;
+ public static final int AUTOFILL_SESSION_HISTOGRAM_COUNT = 14;
+
+ // Records whether user changed autofilled field if user ever changed the form. The action isn't
+ // recorded if user didn't change form at all.
+ public static final String UMA_AUTOFILL_USER_CHANGED_AUTOFILLED_FIELD =
+ "Autofill.WebView.UserChangedAutofilledField";
+
+ public static final String UMA_AUTOFILL_SUBMISSION_SOURCE = "Autofill.WebView.SubmissionSource";
+ // The possible value of UMA_AUTOFILL_SUBMISSION_SOURCE.
+ public static final int SAME_DOCUMENT_NAVIGATION = 0;
+ public static final int XHR_SUCCEEDED = 1;
+ public static final int FRAME_DETACHED = 2;
+ public static final int DOM_MUTATION_AFTER_XHR = 3;
+ public static final int PROBABLY_FORM_SUBMITTED = 4;
+ public static final int FORM_SUBMISSION = 5;
+ public static final int SUBMISSION_SOURCE_HISTOGRAM_COUNT = 6;
+
+ // The million seconds from user touched the field to the autofill session starting.
+ public static final String UMA_AUTOFILL_TRIGGERING_TIME = "Autofill.WebView.TriggeringTime";
+
+ // The million seconds from the autofill session starting to the suggestion being displayed.
+ public static final String UMA_AUTOFILL_SUGGESTION_TIME = "Autofill.WebView.SuggestionTime";
+
+ // The expected time range of time is from 10ms to 2 seconds, and 50 buckets is sufficient.
+ private static final long MIN_TIME_MILLIS = 10;
+ private static final long MAX_TIME_MILLIS = TimeUnit.SECONDS.toMillis(2);
+ private static final int NUM_OF_BUCKETS = 50;
+
+ private static void recordTimesHistogram(String name, long durationMillis) {
+ RecordHistogram.recordCustomTimesHistogram(
+ name, durationMillis, MIN_TIME_MILLIS, MAX_TIME_MILLIS, NUM_OF_BUCKETS);
+ }
+
+ private static class SessionRecorder {
+ public static final int EVENT_VIRTUAL_STRUCTURE_PROVIDED = 0x1 << 0;
+ public static final int EVENT_SUGGESTION_DISPLAYED = 0x1 << 1;
+ public static final int EVENT_FORM_AUTOFILLED = 0x1 << 2;
+ public static final int EVENT_USER_CHANGED_FIELD_VALUE = 0x1 << 3;
+ public static final int EVENT_FORM_SUBMITTED = 0x1 << 4;
+ public static final int EVENT_USER_CHANGED_AUTOFILLED_FIELD = 0x1 << 5;
+
+ private Long mSuggestionTimeMillis;
+
+ public void record(int event) {
+ // Not record any event until we get EVENT_VIRTUAL_STRUCTURE_PROVIDED which makes the
+ // following events meaningful.
+ if (event != EVENT_VIRTUAL_STRUCTURE_PROVIDED && mState == 0) return;
+ if (EVENT_USER_CHANGED_FIELD_VALUE == event && mUserChangedAutofilledField == null) {
+ mUserChangedAutofilledField = Boolean.valueOf(false);
+ } else if (EVENT_USER_CHANGED_AUTOFILLED_FIELD == event) {
+ if (mUserChangedAutofilledField == null) {
+ mUserChangedAutofilledField = Boolean.valueOf(true);
+ }
+ mUserChangedAutofilledField = true;
+ event = EVENT_USER_CHANGED_FIELD_VALUE;
+ }
+ mState |= event;
+ }
+
+ public void setSuggestionTimeMillis(long suggestionTimeMillis) {
+ // Only record first suggestion.
+ if (mSuggestionTimeMillis == null) {
+ mSuggestionTimeMillis = Long.valueOf(suggestionTimeMillis);
+ }
+ }
+
+ public void recordHistogram() {
+ RecordHistogram.recordEnumeratedHistogram(UMA_AUTOFILL_AUTOFILL_SESSION,
+ toUMAAutofillSessionValue(), AUTOFILL_SESSION_HISTOGRAM_COUNT);
+ // Only record if user ever changed form.
+ if (mUserChangedAutofilledField != null) {
+ RecordHistogram.recordBooleanHistogram(
+ UMA_AUTOFILL_USER_CHANGED_AUTOFILLED_FIELD, mUserChangedAutofilledField);
+ }
+ if (mSuggestionTimeMillis != null) {
+ recordTimesHistogram(UMA_AUTOFILL_SUGGESTION_TIME, mSuggestionTimeMillis);
+ }
+ }
+
+ private int toUMAAutofillSessionValue() {
+ if (mState == 0) {
+ return NO_CALLBACK_FORM_FRAMEWORK;
+ } else if (mState == EVENT_VIRTUAL_STRUCTURE_PROVIDED) {
+ return NO_SUGGESTION_USER_NOT_CHANGE_FORM_NO_FORM_SUBMITTED;
+ } else if (mState
+ == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_USER_CHANGED_FIELD_VALUE)) {
+ return NO_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED;
+ } else if (mState == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_FORM_SUBMITTED)) {
+ return NO_SUGGESTION_USER_NOT_CHANGE_FORM_FORM_SUBMITTED;
+ } else if (mState
+ == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_USER_CHANGED_FIELD_VALUE
+ | EVENT_FORM_SUBMITTED)) {
+ return NO_SUGGESTION_USER_CHANGE_FORM_FORM_SUBMITTED;
+ } else if (mState
+ == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED
+ | EVENT_FORM_AUTOFILLED)) {
+ return USER_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_NO_FORM_SUBMITTED;
+ } else if (mState
+ == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED
+ | EVENT_FORM_AUTOFILLED | EVENT_FORM_SUBMITTED)) {
+ return USER_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_FORM_SUBMITTED;
+ } else if (mState
+ == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED
+ | EVENT_FORM_AUTOFILLED | EVENT_USER_CHANGED_FIELD_VALUE
+ | EVENT_FORM_SUBMITTED)) {
+ return USER_SELECT_SUGGESTION_USER_CHANGE_FORM_FORM_SUBMITTED;
+ } else if (mState
+ == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED
+ | EVENT_FORM_AUTOFILLED | EVENT_USER_CHANGED_FIELD_VALUE)) {
+ return USER_SELECT_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED;
+ } else if (mState == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED)) {
+ return USER_NOT_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_NO_FORM_SUBMITTED;
+ } else if (mState
+ == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED
+ | EVENT_FORM_SUBMITTED)) {
+ return USER_NOT_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_FORM_SUBMITTED;
+ } else if (mState
+ == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED
+ | EVENT_USER_CHANGED_FIELD_VALUE | EVENT_FORM_SUBMITTED)) {
+ return USER_NOT_SELECT_SUGGESTION_USER_CHANGE_FORM_FORM_SUBMITTED;
+ } else if (mState
+ == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED
+ | EVENT_USER_CHANGED_FIELD_VALUE)) {
+ return USER_NOT_SELECT_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED;
+ } else {
+ return SESSION_UNKNOWN;
+ }
+ }
+
+ private int mState;
+ private Boolean mUserChangedAutofilledField;
+ }
+
+ private SessionRecorder mRecorder;
+ private Boolean mAutofillDisabled;
+
+ public AutofillProviderUMA(Context context) {
+ RecordHistogram.recordBooleanHistogram(UMA_AUTOFILL_CREATED_BY_ACTIVITY_CONTEXT,
+ ContextUtils.activityFromContext(context) != null);
+ }
+
+ public void onFormSubmitted(int submissionSource) {
+ if (mRecorder != null) mRecorder.record(SessionRecorder.EVENT_FORM_SUBMITTED);
+ recordSession();
+ // We record this no matter autofill service is disabled or not.
+ RecordHistogram.recordEnumeratedHistogram(UMA_AUTOFILL_SUBMISSION_SOURCE,
+ toUMASubmissionSource(submissionSource), SUBMISSION_SOURCE_HISTOGRAM_COUNT);
+ }
+
+ public void onSessionStarted(boolean autofillDisabled) {
+ // Record autofill status once per instance and only if user triggers the autofill.
+ if (mAutofillDisabled == null || mAutofillDisabled.booleanValue() != autofillDisabled) {
+ RecordHistogram.recordBooleanHistogram(UMA_AUTOFILL_ENABLED, !autofillDisabled);
+ mAutofillDisabled = Boolean.valueOf(autofillDisabled);
+ }
+
+ if (mRecorder != null) recordSession();
+ mRecorder = new SessionRecorder();
+ }
+
+ public void onVirtualStructureProvided() {
+ if (mRecorder != null) mRecorder.record(SessionRecorder.EVENT_VIRTUAL_STRUCTURE_PROVIDED);
+ }
+
+ public void onSuggestionDisplayed(long suggestionTimeMillis) {
+ if (mRecorder != null) {
+ mRecorder.record(SessionRecorder.EVENT_SUGGESTION_DISPLAYED);
+ mRecorder.setSuggestionTimeMillis(suggestionTimeMillis);
+ }
+ }
+
+ public void onAutofill() {
+ if (mRecorder != null) mRecorder.record(SessionRecorder.EVENT_FORM_AUTOFILLED);
+ }
+
+ public void onUserChangeFieldValue(boolean isPreviouslyAutofilled) {
+ if (mRecorder == null) return;
+ if (isPreviouslyAutofilled) {
+ mRecorder.record(SessionRecorder.EVENT_USER_CHANGED_AUTOFILLED_FIELD);
+ } else {
+ mRecorder.record(SessionRecorder.EVENT_USER_CHANGED_FIELD_VALUE);
+ }
+ }
+
+ private void recordSession() {
+ if (mAutofillDisabled != null && !mAutofillDisabled.booleanValue() && mRecorder != null) {
+ mRecorder.recordHistogram();
+ }
+ mRecorder = null;
+ }
+
+ private int toUMASubmissionSource(int source) {
+ switch (source) {
+ case SubmissionSource.SAME_DOCUMENT_NAVIGATION:
+ return SAME_DOCUMENT_NAVIGATION;
+ case SubmissionSource.XHR_SUCCEEDED:
+ return XHR_SUCCEEDED;
+ case SubmissionSource.FRAME_DETACHED:
+ return FRAME_DETACHED;
+ case SubmissionSource.DOM_MUTATION_AFTER_XHR:
+ return DOM_MUTATION_AFTER_XHR;
+ case SubmissionSource.PROBABLY_FORM_SUBMITTED:
+ return PROBABLY_FORM_SUBMITTED;
+ case SubmissionSource.FORM_SUBMISSION:
+ return FORM_SUBMISSION;
+ default:
+ return SUBMISSION_SOURCE_HISTOGRAM_COUNT;
+ }
+ }
+}
diff --git a/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/FormData.java b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/FormData.java
new file mode 100644
index 00000000000..8069aa887b6
--- /dev/null
+++ b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/FormData.java
@@ -0,0 +1,56 @@
+// 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 androidx.annotation.VisibleForTesting;
+
+import org.chromium.base.annotations.CalledByNative;
+import org.chromium.base.annotations.JNINamespace;
+import org.chromium.base.annotations.NativeMethods;
+
+import java.util.ArrayList;
+
+/**
+ * The wrap class of native autofill::FormDataAndroid.
+ */
+@JNINamespace("autofill")
+public class FormData {
+ public final String mName;
+ public final String mHost;
+ public final ArrayList<FormFieldData> mFields;
+
+ @CalledByNative
+ private static FormData createFormData(
+ long nativeObj, String name, String origin, int fieldCount) {
+ return new FormData(nativeObj, name, origin, fieldCount);
+ }
+
+ private static ArrayList<FormFieldData> popupFormFields(long nativeObj, int fieldCount) {
+ FormFieldData formFieldData = FormDataJni.get().getNextFormFieldData(nativeObj);
+ ArrayList<FormFieldData> fields = new ArrayList<FormFieldData>(fieldCount);
+ while (formFieldData != null) {
+ fields.add(formFieldData);
+ formFieldData = FormDataJni.get().getNextFormFieldData(nativeObj);
+ }
+ assert fields.size() == fieldCount;
+ return fields;
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ public FormData(String name, String host, ArrayList<FormFieldData> fields) {
+ mName = name;
+ mHost = host;
+ mFields = fields;
+ }
+
+ private FormData(long nativeObj, String name, String host, int fieldCount) {
+ this(name, host, popupFormFields(nativeObj, fieldCount));
+ }
+
+ @NativeMethods
+ interface Natives {
+ FormFieldData getNextFormFieldData(long nativeFormDataAndroid);
+ }
+}
diff --git a/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/FormFieldData.java b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/FormFieldData.java
new file mode 100644
index 00000000000..eb0a94a6bce
--- /dev/null
+++ b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/FormFieldData.java
@@ -0,0 +1,162 @@
+// 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.graphics.RectF;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.VisibleForTesting;
+
+import org.chromium.base.annotations.CalledByNative;
+import org.chromium.base.annotations.JNINamespace;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The wrap class of native autofill::FormFieldDataAndroid.
+ */
+@JNINamespace("autofill")
+public class FormFieldData {
+ /**
+ * Define the control types supported by android.view.autofill.AutofillValue.
+ *
+ * Android doesn't have DATALIST control, it is sent to the Autofill service as
+ * View.AUTOFILL_TYPE_TEXT with AutofillOptions.
+ */
+ @IntDef({ControlType.TEXT, ControlType.TOGGLE, ControlType.LIST, ControlType.DATALIST})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ControlType {
+ int TEXT = 0;
+ int TOGGLE = 1;
+ int LIST = 2;
+ int DATALIST = 3;
+ }
+
+ public final String mLabel;
+ public final String mName;
+ public final String mAutocompleteAttr;
+ public final boolean mShouldAutocomplete;
+ public final String mPlaceholder;
+ public final String mType;
+ public final String mId;
+ public final String[] mOptionValues;
+ public final String[] mOptionContents;
+ public final @ControlType int mControlType;
+ public final int mMaxLength;
+ public final String mHeuristicType;
+ public final String[] mDatalistValues;
+ public final String[] mDatalistLabels;
+
+ // The bounds in the viewport's coordinates
+ private final RectF mBounds;
+ // The bounds in the container view's coordinates.
+ private RectF mBoundsInContainerViewCoordinates;
+
+ private boolean mIsChecked;
+ private String mValue;
+ // Indicates whether mValue is autofilled.
+ private boolean mAutofilled;
+ // Indicates whether this fields was autofilled, but changed by user.
+ private boolean mPreviouslyAutofilled;
+
+ private FormFieldData(String name, String label, String value, String autocompleteAttr,
+ boolean shouldAutocomplete, String placeholder, String type, String id,
+ String[] optionValues, String[] optionContents, boolean isCheckField, boolean isChecked,
+ int maxLength, String heuristicType, float left, float top, float right, float bottom,
+ String[] datalistValues, String[] datalistLabels) {
+ mName = name;
+ mLabel = label;
+ mValue = value;
+ mAutocompleteAttr = autocompleteAttr;
+ mShouldAutocomplete = shouldAutocomplete;
+ mPlaceholder = placeholder;
+ mType = type;
+ mId = id;
+ mOptionValues = optionValues;
+ mOptionContents = optionContents;
+ mIsChecked = isChecked;
+ mDatalistLabels = datalistLabels;
+ mDatalistValues = datalistValues;
+ if (mOptionValues != null && mOptionValues.length != 0) {
+ mControlType = ControlType.LIST;
+ } else if (mDatalistValues != null && mDatalistValues.length != 0) {
+ mControlType = ControlType.DATALIST;
+ } else if (isCheckField) {
+ mControlType = ControlType.TOGGLE;
+ } else {
+ mControlType = ControlType.TEXT;
+ }
+ mMaxLength = maxLength;
+ mHeuristicType = heuristicType;
+ mBounds = new RectF(left, top, right, bottom);
+ }
+
+ public @ControlType int getControlType() {
+ return mControlType;
+ }
+
+ public RectF getBounds() {
+ return mBounds;
+ }
+
+ public void setBoundsInContainerViewCoordinates(RectF bounds) {
+ mBoundsInContainerViewCoordinates = bounds;
+ }
+
+ public RectF getBoundsInContainerViewCoordinates() {
+ return mBoundsInContainerViewCoordinates;
+ }
+
+ /**
+ * @return value of field.
+ */
+ @CalledByNative
+ public String getValue() {
+ return mValue;
+ }
+
+ public void setAutofillValue(String value) {
+ mValue = value;
+ updateAutofillState(true);
+ }
+
+ public void setChecked(boolean checked) {
+ mIsChecked = checked;
+ updateAutofillState(true);
+ }
+
+ @CalledByNative
+ private void updateValue(String value) {
+ mValue = value;
+ updateAutofillState(false);
+ }
+
+ @CalledByNative
+ public boolean isChecked() {
+ return mIsChecked;
+ }
+
+ public boolean hasPreviouslyAutofilled() {
+ return mPreviouslyAutofilled;
+ }
+
+ private void updateAutofillState(boolean autofilled) {
+ if (mAutofilled && !autofilled) mPreviouslyAutofilled = true;
+ mAutofilled = autofilled;
+ }
+
+ @CalledByNative
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ public static FormFieldData createFormFieldData(String name, String label, String value,
+ String autocompleteAttr, boolean shouldAutocomplete, String placeholder, String type,
+ String id, String[] optionValues, String[] optionContents, boolean isCheckField,
+ boolean isChecked, int maxLength, String heuristicType, float left, float top,
+ float right, float bottom, String[] datalistValues, String[] datalistLabels) {
+ return new FormFieldData(name, label, value, autocompleteAttr, shouldAutocomplete,
+ placeholder, type, id, optionValues, optionContents, isCheckField, isChecked,
+ maxLength, heuristicType, left, top, right, bottom, datalistValues, datalistLabels);
+ }
+}