chromium/chrome/browser/autofill/android/java/src/org/chromium/chrome/browser/autofill/editors/EditorProperties.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.chrome.browser.autofill.editors;

import static org.chromium.chrome.browser.autofill.editors.EditorProperties.DropdownFieldProperties.DROPDOWN_HINT;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.DropdownFieldProperties.DROPDOWN_KEY_VALUE_LIST;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.IS_REQUIRED;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.LABEL;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.VALIDATOR;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.VALUE;

import android.text.TextWatcher;
import android.util.Pair;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;

import org.chromium.base.Callback;
import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModel.ReadableBooleanPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.ReadableIntPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.ReadableObjectPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableBooleanPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableObjectPropertyKey;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

/** Properties defined here reflect the visible state of the {@link EditorDialog}. */
public class EditorProperties {
    /** Contains information needed by {@link EditorDialogView} to display fields. */
    public static class FieldItem extends ListItem {
        public final boolean isFullLine;

        public FieldItem(int type, PropertyModel model) {
            this(type, model, /* isFullLine= */ false);
        }

        public FieldItem(int type, PropertyModel model, boolean isFullLine) {
            super(type, model);
            this.isFullLine = isFullLine;
        }
    }

    public static final ReadableObjectPropertyKey<String> EDITOR_TITLE =
            new ReadableObjectPropertyKey<>("editor_title");
    public static final ReadableObjectPropertyKey<String> CUSTOM_DONE_BUTTON_TEXT =
            new ReadableObjectPropertyKey<String>("custom_done_button_text");
    public static final ReadableObjectPropertyKey<String> FOOTER_MESSAGE =
            new ReadableObjectPropertyKey<>("footer_message");
    public static final ReadableObjectPropertyKey<String> DELETE_CONFIRMATION_TITLE =
            new ReadableObjectPropertyKey<>("delete_confirmation_title");
    public static final ReadableObjectPropertyKey<String> DELETE_CONFIRMATION_TEXT =
            new ReadableObjectPropertyKey<>("delete_confirmation_text");
    public static final ReadableBooleanPropertyKey SHOW_REQUIRED_INDICATOR =
            new ReadableBooleanPropertyKey("show_required_indicator");

    public static final WritableObjectPropertyKey<ListModel<FieldItem>> EDITOR_FIELDS =
            new WritableObjectPropertyKey<>("editor_fields");

    public static final ReadableObjectPropertyKey<Runnable> DONE_RUNNABLE =
            new ReadableObjectPropertyKey<>("done_callback");
    public static final ReadableObjectPropertyKey<Runnable> CANCEL_RUNNABLE =
            new ReadableObjectPropertyKey<>("cancel_callback");

    public static final ReadableBooleanPropertyKey ALLOW_DELETE =
            new ReadableBooleanPropertyKey("allow_delete");
    public static final ReadableObjectPropertyKey<Runnable> DELETE_RUNNABLE =
            new ReadableObjectPropertyKey<>("delete_callback");

    public static final WritableBooleanPropertyKey VALIDATE_ON_SHOW =
            new WritableBooleanPropertyKey("validate_on_show");

    public static final WritableBooleanPropertyKey VISIBLE =
            new WritableBooleanPropertyKey("visible");

    public static final PropertyKey[] ALL_KEYS = {
        EDITOR_TITLE,
        CUSTOM_DONE_BUTTON_TEXT,
        FOOTER_MESSAGE,
        DELETE_CONFIRMATION_TITLE,
        DELETE_CONFIRMATION_TEXT,
        SHOW_REQUIRED_INDICATOR,
        EDITOR_FIELDS,
        DONE_RUNNABLE,
        CANCEL_RUNNABLE,
        ALLOW_DELETE,
        DELETE_RUNNABLE,
        VALIDATE_ON_SHOW,
        VISIBLE
    };

    private EditorProperties() {}

    /*
     * Types of fields this editor model supports.
     */
    @IntDef({ItemType.DROPDOWN, ItemType.TEXT_INPUT})
    @Retention(RetentionPolicy.SOURCE)
    public @interface ItemType {
        // A fixed list of values, only 1 of which can be selected.
        int DROPDOWN = 1;
        // User can fill in a sequence of characters subject to input type restrictions.
        int TEXT_INPUT = 2;
    }

    /** A convenience class for displaying keyed values in a dropdown. */
    public static class DropdownKeyValue extends Pair<String, String> {
        public DropdownKeyValue(String key, String value) {
            super(key, value);
        }

        /** @return The key identifier. */
        public String getKey() {
            return super.first;
        }

        /** @return The human-readable localized display value. */
        public String getValue() {
            return super.second;
        }

        @Override
        public String toString() {
            return super.second.toString();
        }
    }

    /** Field properties common to every field. */
    public static class FieldProperties {
        public static final WritableObjectPropertyKey<String> LABEL =
                new WritableObjectPropertyKey<>("label");
        public static final PropertyModel.WritableObjectPropertyKey<EditorFieldValidator>
                VALIDATOR = new WritableObjectPropertyKey<>("validator");
        public static final WritableObjectPropertyKey<String> ERROR_MESSAGE =
                new WritableObjectPropertyKey<>("error_message");
        // TODO(crbug.com/40265078): make this field read-only.
        public static final WritableBooleanPropertyKey IS_REQUIRED =
                new WritableBooleanPropertyKey("is_required");
        public static final WritableBooleanPropertyKey FOCUSED =
                new WritableBooleanPropertyKey("focused");
        // TODO(crbug.com/40265078): make this field read-only.
        public static final WritableObjectPropertyKey<String> VALUE =
                new WritableObjectPropertyKey<>("value");

        public static final PropertyKey[] FIELD_ALL_KEYS = {
            LABEL, VALIDATOR, IS_REQUIRED, ERROR_MESSAGE, FOCUSED, VALUE
        };
    }

    /** Properties specific for the dropdown fields. */
    public static class DropdownFieldProperties {
        public static final ReadableObjectPropertyKey<List<DropdownKeyValue>>
                DROPDOWN_KEY_VALUE_LIST = new ReadableObjectPropertyKey<>("key_value_list");
        public static final WritableObjectPropertyKey<Callback<String>> DROPDOWN_CALLBACK =
                new WritableObjectPropertyKey<>("callback");
        public static final ReadableObjectPropertyKey<String> DROPDOWN_HINT =
                new ReadableObjectPropertyKey<>("hint");

        public static final PropertyKey[] DROPDOWN_SPECIFIC_KEYS = {
            DROPDOWN_KEY_VALUE_LIST, DROPDOWN_CALLBACK, DROPDOWN_HINT
        };

        public static final PropertyKey[] DROPDOWN_ALL_KEYS =
                Stream.concat(
                                Arrays.stream(FieldProperties.FIELD_ALL_KEYS),
                                Arrays.stream(DROPDOWN_SPECIFIC_KEYS))
                        .toArray(PropertyKey[]::new);
    }

    /** Properties specific for the text fields. */
    public static class TextFieldProperties {
        public static final ReadableIntPropertyKey TEXT_FIELD_TYPE =
                new ReadableIntPropertyKey("field_type");
        public static final WritableObjectPropertyKey<List<String>> TEXT_SUGGESTIONS =
                new WritableObjectPropertyKey<>("suggestions");
        public static final ReadableObjectPropertyKey<TextWatcher> TEXT_FORMATTER =
                new ReadableObjectPropertyKey<>("formatter");

        public static final PropertyKey[] TEXT_SPECIFIC_KEYS = {
            TEXT_FIELD_TYPE, TEXT_SUGGESTIONS, TEXT_FORMATTER,
        };

        public static final PropertyKey[] TEXT_ALL_KEYS =
                Stream.concat(
                                Arrays.stream(FieldProperties.FIELD_ALL_KEYS),
                                Arrays.stream(TEXT_SPECIFIC_KEYS))
                        .toArray(PropertyKey[]::new);
    }

    public static boolean isDropdownField(ListItem fieldItem) {
        return fieldItem.type == ItemType.DROPDOWN;
    }

    public static @Nullable String getDropdownKeyByValue(
            PropertyModel dropdownField, String value) {
        return dropdownField.get(DropdownFieldProperties.DROPDOWN_KEY_VALUE_LIST).stream()
                .filter(
                        keyValue -> {
                            return keyValue.getValue().equals(value);
                        })
                .map(DropdownKeyValue::getKey)
                .findAny()
                .orElse(null);
    }

    public static @Nullable String getDropdownValueByKey(PropertyModel dropdownField, String key) {
        return dropdownField.get(DropdownFieldProperties.DROPDOWN_KEY_VALUE_LIST).stream()
                .filter(
                        keyValue -> {
                            return keyValue.getKey().equals(key);
                        })
                .map(DropdownKeyValue::getValue)
                .findAny()
                .orElse(null);
    }

    public static void setDropdownKey(PropertyModel dropdownField, @Nullable String key) {
        // The mValue can only be set to null if there is a hint.
        if (key == null && dropdownField.get(DropdownFieldProperties.DROPDOWN_HINT) == null) {
            return;
        }
        dropdownField.set(FieldProperties.VALUE, key);
        Callback<String> fieldCallback =
                dropdownField.get(DropdownFieldProperties.DROPDOWN_CALLBACK);
        if (fieldCallback != null) {
            fieldCallback.onResult(key);
        }
    }

    public static boolean validateForm(PropertyModel editorModel) {
        boolean isValid = true;
        for (ListItem item : editorModel.get(EditorProperties.EDITOR_FIELDS)) {
            if (item.model.get(FieldProperties.VALIDATOR) == null) {
                continue;
            }
            item.model.get(FieldProperties.VALIDATOR).validate(item.model);
            isValid &= item.model.get(FieldProperties.ERROR_MESSAGE) == null;
        }
        return isValid;
    }

    public static void scrollToFieldWithErrorMessage(PropertyModel editorModel) {
        // Check if a field with an error is already focused.
        ListModel<FieldItem> fields = editorModel.get(EditorProperties.EDITOR_FIELDS);
        for (FieldItem item : fields) {
            if (item.model.get(FieldProperties.FOCUSED)
                    && item.model.get(FieldProperties.ERROR_MESSAGE) != null) {
                // Hack: Although the field is focused, it may be off screen. Toggle FOCUSED in
                // order to scroll the field into view.
                item.model.set(FieldProperties.FOCUSED, false);
                item.model.set(FieldProperties.FOCUSED, true);
                return;
            }
        }

        // Focus first field with an error.
        for (FieldItem item : fields) {
            if (item.model.get(FieldProperties.ERROR_MESSAGE) != null) {
                item.model.set(FieldProperties.FOCUSED, true);
                break;
            }
            // The field (ex {@link TextFieldView}) is responsible for clearing FOCUSED property
            // when the field loses focus.
        }
    }
}