chromium/components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java

// Copyright 2017 The Chromium Authors
// 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.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.Build.VERSION_CODES;
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.RequiresApi;
import androidx.annotation.VisibleForTesting;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;

import org.chromium.base.ContextUtils;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.StrictModeContext;
import org.chromium.base.metrics.ScopedSysTraceEvent;
import org.chromium.base.version_info.VersionConstants;
import org.chromium.components.autofill.AutofillRequest.FocusField;
import org.chromium.content_public.browser.RenderCoordinates;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsAccessibility;
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.
 *
 * <p>AutofillProvider handles one autofill session at time, each call of startAutofillSession
 * cancels previous session and starts a new one, the calling of other methods shall associate with
 * current session.
 *
 * <p>This class doesn't have 1:1 mapping to native AndroidAutofillProvider; 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 AndroidAutofillProvider is owned by the
 * embedder-specific C++ WebContents wrapper (e.g., native AwContents in //android_webview).
 */
@JNINamespace("autofill")
public class AutofillProvider {
    /**
     * Factory interface for testing. AutofillManagerWrapper must be created in AutofillProvider
     * constructor.
     */
    public static interface AutofillManagerWrapperFactoryForTesting {
        AutofillManagerWrapper create(Context context);
    }

    private static AutofillManagerWrapperFactoryForTesting sAutofillManagerFactoryForTesting;

    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 AutofillSuggestion[] mDatalistSuggestions;
    private WebContentsAccessibility mWebContentsAccessibility;
    private View mAnchorView;
    private PrefillRequest mPrefillRequest;
    // Whether onProvideAutofillVirtualStructure has been called for the current PrefillRequest.
    // Used solely for metrics.
    private boolean mStructureProvidedForPrefillRequest;

    public AutofillProvider(
            Context context,
            ViewGroup containerView,
            WebContents webContents,
            String providerName) {
        mWebContents = webContents;
        mProviderName = providerName;
        try (ScopedSysTraceEvent e = ScopedSysTraceEvent.scoped("AutofillProvider.constructor")) {
            if (sAutofillManagerFactoryForTesting != null) {
                mAutofillManager = sAutofillManagerFactoryForTesting.create(context);
            } else {
                mAutofillManager = new AutofillManagerWrapper(context);
            }
            mContainerView = containerView;
            mAutofillUMA =
                    new AutofillProviderUMA(
                            context,
                            mAutofillManager.isAwGCurrentAutofillService(),
                            mAutofillManager.getPackageName());
            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;
        }
        initializeNativeAutofillProvider(webContents);
    }

    public void destroy() {
        mAutofillUMA.recordSession();
        detachFromJavaAutofillProvider();
        mAutofillManager.destroy();
    }

    /**
     * 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 && mPrefillRequest == 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);

            if (mRequest != null && mRequest.getAutofillHintsService() != null) {
                bundle.putBinder(
                        "AUTOFILL_HINTS_SERVICE", mRequest.getAutofillHintsService().getBinder());
            }
        }
        // We should have one of them available here, we start with AutofillRequest as it should be
        // available only if we started a session.
        FormData form;
        short focusFieldIndex = -1;
        if (mRequest != null) {
            form = mRequest.getForm();
            focusFieldIndex =
                    mRequest.getFocusField() != null ? mRequest.getFocusField().fieldIndex : -1;
            mAutofillUMA.onVirtualStructureProvided();
        } else {
            form = mPrefillRequest.getForm();
            mStructureProvidedForPrefillRequest = true;
        }
        form.fillViewStructure(structure, focusFieldIndex);
        if (AutofillManagerWrapper.isLoggable()) {
            AutofillManagerWrapper.log(
                    "onProvideAutoFillVirtualStructure fields:" + structure.getChildCount());
        }
    }

    /**
     * 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);
            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.getFieldVirtualId(focusField.fieldIndex),
                    focusField.absBound);
        }
    }

    public static void setAutofillManagerWrapperFactoryForTesting(
            AutofillManagerWrapperFactoryForTesting factory) {
        sAutofillManagerFactoryForTesting = factory;
        ResettersForTesting.register(() -> sAutofillManagerFactoryForTesting = null);
    }

    public void replaceAutofillManagerWrapperForTesting(AutofillManagerWrapper wrapper) {
        mAutofillManager = wrapper;
    }

    /**
     * Sends a prefill (cache) request to the Android Autofill Framework.
     *
     * @param form the form to send the prefill request for.
     */
    @CalledByNative
    public void sendPrefillRequest(FormData form) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return;
        // Return early if there's a session running already.
        if (mRequest != null && mRequest.getFocusField() != null) {
            return;
        }

        transformFormFieldToContainViewCoordinates(form);
        mPrefillRequest = new PrefillRequest(form);
        mStructureProvidedForPrefillRequest = false;

        mAutofillManager.notifyVirtualViewsReady(mContainerView, mPrefillRequest.getPrefillHints());
    }

    /**
     * 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.
     * @param hasServerPrediction whether the server prediction arrived.
     */
    @CalledByNative
    public void startAutofillSession(
            FormData formData,
            int focus,
            float x,
            float y,
            float width,
            float height,
            boolean hasServerPrediction) {
        Rect absBound = transformToWindowBounds(new RectF(x, y, x + width, y + height));
        if (mRequest != null) notifyViewExitBeforeDestroyRequest();

        // 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();
        }

        transformFormFieldToContainViewCoordinates(formData);
        mAutofillUMA.onSessionStarted(mAutofillManager.isDisabled());
        mRequest =
                new AutofillRequest(
                        formData, new FocusField((short) focus, absBound), hasServerPrediction);
        if (maybeShowBottomSheet(focus)) {
            mAutofillUMA.onBottomSheetShown();
        } else {
            notifyVirtualViewEntered(mContainerView, focus, absBound);
        }
        if (hasServerPrediction) {
            mAutofillUMA.onServerTypeAvailable(formData, /* afterSessionStarted= */ false);
        }
        mAutofillTriggeredTimeMillis = System.currentTimeMillis();

        mAutofillManager.notifyNewSessionStarted(hasServerPrediction);
    }

    /**
     * Attempts to show a bottom sheet if the Android version is U+ and there has been a prefill
     * request for the form in mRequest.
     *
     * @param focus the index of the focused field in mRequest.
     * @return whether the bottom sheet was shown.
     */
    boolean maybeShowBottomSheet(int focus) {
        if (Build.VERSION.SDK_INT < VERSION_CODES.UPSIDE_DOWN_CAKE) return false;
        if (mPrefillRequest == null
                || mPrefillRequest.getForm().mSessionId != mRequest.getForm().mSessionId) {
            return false;
        }

        boolean bottomSheetShown = showAutofillDialog(mContainerView, focus);
        if (mNativeAutofillProvider != 0) {
            AutofillProviderJni.get()
                    .onShowBottomSheetResult(
                            mNativeAutofillProvider,
                            bottomSheetShown,
                            mStructureProvidedForPrefillRequest);
        }
        return bottomSheetShown;
    }

    /**
     * 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, /* causedByValueChange= */ true);
        } 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.
            Rect absBound = transformToWindowBounds(new RectF(x, y, x + width, y + height));
            if (!focusField.absBound.equals(absBound)) {
                notifyVirtualViewExited(mContainerView, index);
                notifyVirtualViewEntered(mContainerView, index, absBound);
                // Update focus field position.
                mRequest.setFocusField(new FocusField(focusField.fieldIndex, absBound));
            }
        }
        notifyVirtualValueChanged(index, /* forceNotify= */ false);
        mAutofillUMA.onUserChangeFieldValue(mRequest.getField(sIndex).hasPreviouslyAutofilled());
    }

    /**
     * Invoked by the native counterpart when one or more fields have changed
     * their visibility. The (Java) fields' visibility state has at that point
     * already been updated by direct calls from native to the fields.
     *
     * @param indices the indices of the fields with visibility changes.
     *
     */
    @CalledByNative
    private void onFormFieldVisibilitiesDidChange(int[] indices) {
        if (mRequest == null || indices.length == 0) return;

        mAutofillUMA.onFieldChangedVisibility();

        for (int index : indices) {
            notifyVirtualViewVisibilityChanged(
                    index, mRequest.getField((short) index).getVisible());
        }
    }

    /**
     * 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) {
        if (mRequest == null) return;

        short sIndex = (short) index;
        FormFieldData fieldData = mRequest.getField(sIndex);
        if (fieldData != null) fieldData.updateBounds(new RectF(x, y, x + width, y + 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;

        FocusField focusField = mRequest.getFocusField();
        if (focusField == null || sIndex != focusField.fieldIndex) return;

        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, index, absBound);

        // Update focus field position.
        mRequest.setFocusField(new FocusField(focusField.fieldIndex, absBound));
    }

    // Add a specific method in order to mock it in test.
    protected void initializeNativeAutofillProvider(WebContents webContents) {
        AutofillProviderJni.get().init(this, webContents);
    }

    private boolean isDatalistField(int index) {
        assert index <= Short.MAX_VALUE;
        FormFieldData field = mRequest.getField((short) index);
        return field.mControlType == FormFieldData.ControlType.DATALIST;
    }

    // `ValueChanged`, `ViewVisibilityChanged`, `ViewEntered`, and `ViewExited`
    // events are not communicated to the Android Autofill service if the focused element is a
    // datalist. This avoids UI conflicts between the datalist popup (shown by WebView) and the
    // Android Autofill UI (shown by Android).
    // On submit and on autofill a `ValueChanged` event is still sent to the Android Autofill
    // service.
    private void notifyVirtualValueChanged(int index, boolean forceNotify) {
        if (!forceNotify && isDatalistField(index)) return;
        AutofillValue autofillValue = mRequest.getFieldNewValue(index);
        if (autofillValue == null) return;
        mAutofillManager.notifyVirtualValueChanged(
                mContainerView, mRequest.getFieldVirtualId((short) index), autofillValue);
    }

    private void notifyVirtualViewVisibilityChanged(int index, boolean isVisible) {
        if (isDatalistField(index)) return;
        mAutofillManager.notifyVirtualViewVisibilityChanged(
                mContainerView, mRequest.getFieldVirtualId((short) index), isVisible);
    }

    @RequiresApi(VERSION_CODES.TIRAMISU)
    private boolean showAutofillDialog(View parent, int index) {
        // Refer to notifyVirtualValueChanged() for the reason of the datalist's special handling.
        if (isDatalistField(index)) return false;

        return mAutofillManager.showAutofillDialog(
                parent, mRequest.getFieldVirtualId((short) index));
    }

    private void notifyVirtualViewEntered(View parent, int index, Rect absBounds) {
        // Refer to notifyVirtualValueChanged() for the reason of the datalist's special handling.
        if (isDatalistField(index)) return;
        mAutofillManager.notifyVirtualViewEntered(
                parent, mRequest.getFieldVirtualId((short) index), absBounds);
    }

    private void notifyVirtualViewExited(View parent, int index) {
        // Refer to notifyVirtualValueChanged() for the reason of the datalist's special handling.
        if (isDatalistField(index)) return;
        mAutofillManager.notifyVirtualViewExited(parent, mRequest.getFieldVirtualId((short) index));
    }

    /**
     * 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 focusField 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, /* causedByValueChange= */ false);
    }

    @CalledByNative
    public void hideDatalistPopup() {
        if (mDatalistPopup == null) return;

        mDatalistPopup.dismiss();
        mDatalistPopup = null;
        mDatalistSuggestions = null;
        if (mWebContentsAccessibility != null) {
            mWebContentsAccessibility.onAutofillPopupDismissed();
        }
    }

    private void notifyViewExitBeforeDestroyRequest() {
        if (mRequest == null) return;
        FocusField focusField = mRequest.getFocusField();
        if (focusField == null) return;
        notifyVirtualViewExited(mContainerView, 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? FocusOnNonFormField 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, prev.fieldIndex);
            }

            notifyVirtualViewEntered(mContainerView, 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, 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) {
        mDatalistSuggestions = new AutofillSuggestion[datalistValues.length];
        for (int i = 0; i < mDatalistSuggestions.length; i++) {
            mDatalistSuggestions[i] =
                    new AutofillSuggestion.Builder()
                            .setLabel(datalistValues[i])
                            .setSubLabel(datalistLabels[i])
                            .setItemTag("")
                            .setSuggestionType(SuggestionType.DATALIST_ENTRY)
                            .setFeatureForIPH("")
                            .build();
        }
        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(
                                                mDatalistSuggestions[listIndex].getLabel());
                                    }

                                    @Override
                                    public void deleteSuggestion(int listIndex) {}

                                    @Override
                                    public void accessibilityFocusCleared() {
                                        mWebContentsAccessibility
                                                .onAutofillPopupAccessibilityFocusCleared();
                                    }
                                },
                                null);
            } catch (RuntimeException e) {
                // Deliberately swallowing exception because bad framework implementation can
                // throw exceptions in ListPopupWindow constructor.
                onDatalistPopupDismissed();
                return;
            }
        }
        mDatalistPopup.filterAndShow(mDatalistSuggestions, isRtl);
        if (mWebContentsAccessibility != null) {
            mWebContentsAccessibility.onAutofillPopupDisplayed(mDatalistPopup.getListView());
        }
    }

    private void onDatalistPopupDismissed() {
        ViewAndroidDelegate delegate = mWebContents.getViewAndroidDelegate();
        if (delegate != null) delegate.removeView(mAnchorView);
        mAnchorView = null;
    }

    private void onSuggestionSelected(String value) {
        if (mNativeAutofillProvider != 0) {
            acceptDataListSuggestion(mNativeAutofillProvider, value);
        }
        hideDatalistPopup();
    }

    private void setAnchorViewRect(RectF rect) {
        if (mNativeAutofillProvider != 0) {
            setAnchorViewRect(mNativeAutofillProvider, mAnchorView, rect);
        }
    }

    @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;
    }

    private void detachFromJavaAutofillProvider() {
        if (mNativeAutofillProvider == 0) return;
        // Makes sure this is the last call to mNativeAutofillProvider.
        long nativeAutofillProvider = mNativeAutofillProvider;
        mNativeAutofillProvider = 0;
        AutofillProviderJni.get().detachFromJavaAutofillProvider(nativeAutofillProvider);
    }

    public void setWebContents(WebContents webContents) {
        if (webContents == mWebContents) return;
        mAutofillUMA.recordSession();
        if (mWebContents != null) mRequest = null;
        mWebContents = webContents;
        detachFromJavaAutofillProvider();
        if (mWebContents != null) {
            initializeNativeAutofillProvider(webContents);
        }
    }

    @CalledByNative
    protected void onDidFillAutofillFormData() {
        // The changes were caused by the autofill service autofill form,
        // notified it about the result.
        forceNotifyFormValues();
    }

    @CalledByNative
    private void onServerPredictionsAvailable() {
        if (mRequest == null) return;
        mRequest.onServerPredictionsAvailable();
        mAutofillManager.onServerPredictionsAvailable();
        mAutofillUMA.onServerTypeAvailable(mRequest.getForm(), /* afterSessionStarted= */ true);
    }

    private void forceNotifyFormValues() {
        if (mRequest == null) return;
        for (int i = 0; i < mRequest.getFieldCount(); ++i) {
            notifyVirtualValueChanged(i, /* forceNotify= */ true);
        }
    }

    public AutofillPopup getDatalistPopupForTesting() {
        return mDatalistPopup;
    }

    private Rect transformToWindowBounds(RectF rect) {
        // Refer to crbug.com/1085294 for the reason of offset.
        // The current version of Mockito didn't support mock static method, adding extra method so
        // the transform can be tested.
        return transformToWindowBoundsWithOffsetY(
                rect, RenderCoordinates.fromWebContents(mWebContents).getContentOffsetYPixInt());
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    public Rect transformToWindowBoundsWithOffsetY(RectF rect, int offsetY) {
        // 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);
        location[1] += offsetY;
        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);
        }
    }

    /**
     * Inform native provider to autofill.
     *
     * @param nativeAutofillProvider the native autofill provider.
     */
    private void autofill(long nativeAutofillProvider) {
        AutofillProviderJni.get().onAutofillAvailable(nativeAutofillProvider);
    }

    private void acceptDataListSuggestion(long nativeAutofillProvider, String value) {
        AutofillProviderJni.get().onAcceptDataListSuggestion(nativeAutofillProvider, value);
    }

    private void setAnchorViewRect(long nativeAutofillProvider, View anchorView, RectF rect) {
        AutofillProviderJni.get()
                .setAnchorViewRect(
                        nativeAutofillProvider,
                        anchorView,
                        rect.left,
                        rect.top,
                        rect.width(),
                        rect.height());
    }

    @CalledByNative
    public void cancelSession() {
        mAutofillManager.cancel();
        mPrefillRequest = null;
        mRequest = null;
    }

    @CalledByNative
    public void reset() {
        hideDatalistPopup();
        mPrefillRequest = null;
        mRequest = null;
    }

    @NativeMethods
    interface Natives {
        void init(AutofillProvider caller, WebContents webContents);

        void detachFromJavaAutofillProvider(long nativeAndroidAutofillProviderBridgeImpl);

        void onAutofillAvailable(long nativeAndroidAutofillProviderBridgeImpl);

        void onAcceptDataListSuggestion(
                long nativeAndroidAutofillProviderBridgeImpl,
                @JniType("std::u16string") String value);

        void setAnchorViewRect(
                long nativeAndroidAutofillProviderBridgeImpl,
                View anchorView,
                float x,
                float y,
                float width,
                float height);

        void onShowBottomSheetResult(
                long nativeAndroidAutofillProviderBridgeImpl,
                boolean isShown,
                boolean providedAutofillStructure);
    }
}