diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-12 14:27:29 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-13 09:35:20 +0000 |
commit | c30a6232df03e1efbd9f3b226777b07e087a1122 (patch) | |
tree | e992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/components/autofill/android/provider/java | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-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')
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); + } +} |