chromium/components/android_autofill/browser/java/src/org/chromium/components/autofill/FormData.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.graphics.RectF;
import android.view.View;
import android.view.ViewStructure;
import android.view.autofill.AutofillValue;

import androidx.annotation.VisibleForTesting;

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

import java.util.List;

/**
 * The wrapper class of the native autofill::FormDataAndroid.
 *
 * <p>{@link #fillViewStructure(ViewStructure)} is used by other classes (i.e {@link
 * AutofillRequest} to translate the FormData object into a ViewStructure.
 */
@JNINamespace("autofill")
public class FormData {
    public final int mSessionId;
    public final String mName;
    public final String mHost;
    public final List<FormFieldData> mFields;
    // Every node must have an Autofill id. We (arbitrarily, but consistently) choose the
    // maximum value for the form node.
    private static final short FORM_NODE_ID = Short.MAX_VALUE;

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    @CalledByNative
    static FormData createFormData(
            int sessionId,
            @JniType("std::u16string") String name,
            @JniType("std::string") String origin,
            @JniType("std::vector") List<FormFieldData> fields) {
        return new FormData(sessionId, name, origin, fields);
    }

    public FormData(int sessionId, String name, String host, List<FormFieldData> fields) {
        mSessionId = sessionId;
        mName = name;
        mHost = host;
        mFields = fields;
    }

    /**
     * Translates the current form into a ViewStructure processed by Android's Autofill framework.
     *
     * @param structure out parameter, the structure passed to the framework.
     * @param focusFieldIndex the index of the field that is currently focused. -1 if unknown.
     */
    public void fillViewStructure(ViewStructure structure, short focusFieldIndex) {
        structure.setWebDomain(mHost);
        structure.setHtmlInfo(
                structure.newHtmlInfoBuilder("form").addAttribute("name", mName).build());
        int index = structure.addChildCount(mFields.size());
        short fieldIndex = 0;
        for (FormFieldData field : mFields) {
            ViewStructure child = structure.newChild(index++);
            if (focusFieldIndex == fieldIndex) {
                child.setFocused(true);
            }
            int virtualId = toFieldVirtualId(mSessionId, fieldIndex++);
            child.setAutofillId(structure.getAutofillId(), virtualId);
            field.setAutofillId(child.getAutofillId());
            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,
                    /* scrollX= */ 0,
                    /* scrollY= */ 0,
                    (int) bounds.width(),
                    (int) bounds.height());
            child.setVisibility(field.getVisible() ? View.VISIBLE : View.INVISIBLE);

            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);
            builder.addAttribute("crowdsourcing-autofill-hints", field.getServerType());
            builder.addAttribute("computed-autofill-hints", field.getComputedType());
            // Compose multiple predictions to a string separated by ','.
            String[] predictions = field.getServerPredictions();
            if (predictions != null && predictions.length > 0) {
                builder.addAttribute(
                        "crowdsourcing-predictions-autofill-hints", String.join(",", predictions));
            }
            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());
        }
    }

    static int toFieldVirtualId(int sessionId, short index) {
        return (sessionId << 16) | index;
    }

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