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

// Copyright 2023 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.graphics.Rect;
import android.util.SparseArray;
import android.view.ViewStructure;
import android.view.autofill.AutofillValue;

import org.chromium.base.Log;
import org.chromium.components.autofill_public.ViewType;

import java.util.ArrayList;

/**
 * A class to translate {@link FormData} into the ViewStructure used by Android's Autofill
 * framework. It uses the {@link FormData#fillViewStructure(ViewStructure)} to translate the
 * FormData object into a ViewStructure.
 *
 * <p>The key method of this class is: - {@link #autofill}: Verifies that the autofill request by
 * the framework is valid.
 */
public class AutofillRequest {
    /** A simple class representing the field that is currently focused by the user. */
    public static class FocusField {
        public final short fieldIndex;
        public final Rect absBound;

        public FocusField(short fieldIndex, Rect absBound) {
            this.fieldIndex = fieldIndex;
            this.absBound = absBound;
        }
    }

    private static final String TAG = "AutofillRequest";

    private FormData mFormData;
    private FocusField mFocusField;
    private AutofillHintsService mAutofillHintsService;

    /**
     * @param formData the form of the AutofillRequest.
     * @param focus the currently focused field.
     * @param hasServerPrediction whether the server type of formData is valid.
     */
    public AutofillRequest(FormData formData, FocusField focus, boolean hasServerPrediction) {
        mFormData = formData;
        mFocusField = focus;
        // Don't need to create binder object if server prediction is already available.
        if (!hasServerPrediction) mAutofillHintsService = new AutofillHintsService();
    }

    /**
     * Verifies that the values of this autofill request from the framework have virtual ids that
     * match the session id and the ids of existing form fields of the selected form. If they do, it
     * updates the underlying FormFieldData objects to contain the new values, which are then used
     * by the native code to fill the form.
     *
     * @param values the autofill request by the Android Autofill framework
     * @return whether the autofill request is valid, i.e. whether the virtual ids contained in it
     *     correspond to an ongoing session with existing form fields.
     */
    public boolean autofill(final SparseArray<AutofillValue> values) {
        int filledCount = 0;
        for (int i = 0; i < values.size(); ++i) {
            int id = values.keyAt(i);
            if (toSessionId(id) != mFormData.mSessionId) continue;
            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;
            }
            filledCount++;
        }
        return filledCount != 0;
    }

    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 = FormData.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 getFieldVirtualId(short fieldIndex) {
        return FormData.toFieldVirtualId(mFormData.mSessionId, fieldIndex);
    }

    public FormData getForm() {
        return mFormData;
    }

    public FormFieldData getField(short fieldIndex) {
        return mFormData.mFields.get(fieldIndex);
    }

    private static int toSessionId(int fieldVirtualId) {
        return (fieldVirtualId & 0xffff0000) >> 16;
    }

    private static short toIndex(int fieldVirtualId) {
        return (short) (fieldVirtualId & 0xffff);
    }

    public AutofillHintsService getAutofillHintsService() {
        return mAutofillHintsService;
    }

    public void onServerPredictionsAvailable() {
        if (mAutofillHintsService == null) return;
        ArrayList<ViewType> viewTypes = new ArrayList<ViewType>();
        for (FormFieldData field : mFormData.mFields) {
            viewTypes.add(
                    new ViewType(
                            field.getAutofillId(),
                            field.getServerType(),
                            field.getComputedType(),
                            field.getServerPredictions()));
            }
        mAutofillHintsService.onViewTypeAvailable(viewTypes);
    }
}