chromium/chrome/browser/autofill/android/java/src/org/chromium/chrome/browser/autofill/editors/DropdownFieldView.java

// Copyright 2016 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.FieldProperties.ERROR_MESSAGE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.FOCUSED;
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.getDropdownKeyByValue;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.getDropdownValueByKey;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.setDropdownKey;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;

import androidx.annotation.Nullable;

import org.chromium.base.ResettersForTesting;
import org.chromium.chrome.browser.autofill.R;
import org.chromium.components.browser_ui.util.TraceEventVectorDrawableCompat;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.modelutil.PropertyModel;

import java.util.List;

/** Helper class for creating a dropdown view with a label. */
class DropdownFieldView implements FieldView {
    @Nullable private static EditorObserverForTest sObserverForTest;

    private final Context mContext;
    private final PropertyModel mFieldModel;
    private final View mLayout;
    private final TextView mLabel;
    private final Spinner mDropdown;
    private final View mUnderline;
    private final TextView mErrorLabel;
    private int mSelectedIndex;
    private ArrayAdapter<String> mAdapter;
    @Nullable private String mHint;
    @Nullable private EditorFieldValidator mValidator;
    private boolean mShowRequiredIndicator;

    /**
     * Builds a dropdown view.
     *
     * @param context              The application context to use when creating widgets.
     * @param root                 The object that provides a set of LayoutParams values for
     *                             the view.
     * @param fieldModel           The data model of the dropdown.
     * @param hasRequiredIndicator Whether the required (*) indicator is visible.
     */
    public DropdownFieldView(Context context, ViewGroup root, final PropertyModel fieldModel) {
        mContext = context;
        mFieldModel = fieldModel;

        mLayout =
                LayoutInflater.from(context)
                        .inflate(R.layout.payment_request_editor_dropdown, root, false);

        mLabel = (TextView) mLayout.findViewById(R.id.spinner_label);
        setShowRequiredIndicator(/* showRequiredIndicator= */ false);

        mUnderline = mLayout.findViewById(R.id.spinner_underline);

        mErrorLabel = mLayout.findViewById(R.id.spinner_error);

        mDropdown = (Spinner) mLayout.findViewById(R.id.spinner);
        mDropdown.setTag(this);
        mDropdown.setOnItemSelectedListener(
                new OnItemSelectedListener() {
                    @Override
                    public void onItemSelected(
                            AdapterView<?> parent, View view, int position, long id) {
                        if (mSelectedIndex != position) {
                            String key =
                                    getDropdownKeyByValue(mFieldModel, mAdapter.getItem(position));
                            // If the hint is selected, it means that no value is entered by the
                            // user.
                            if (mHint != null && position == 0) {
                                key = null;
                            }
                            mSelectedIndex = position;
                            setDropdownKey(mFieldModel, key);

                            if (mValidator != null) {
                                mValidator.onUserEditedField();
                                mValidator.validate(mFieldModel);
                            }
                        }
                        if (sObserverForTest != null) {
                            sObserverForTest.onEditorTextUpdate();
                        }
                    }

                    @Override
                    public void onNothingSelected(AdapterView<?> parent) {}
                });
        mDropdown.setOnTouchListener(
                new View.OnTouchListener() {
                    @SuppressLint("ClickableViewAccessibility")
                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        if (event.getAction() == MotionEvent.ACTION_DOWN) {
                            requestFocusAndHideKeyboard();
                        }

                        return false;
                    }
                });
    }

    void setLabel(String label, boolean isRequired) {
        mLabel.setText(
                isRequired && mShowRequiredIndicator
                        ? label + EditorDialogView.REQUIRED_FIELD_INDICATOR
                        : label);
    }

    void setDropdownValues(List<String> values, @Nullable String hint) {
        mHint = hint;
        if (mHint != null) {
            mAdapter =
                    new HintedDropDownAdapter<String>(
                            mContext,
                            R.layout.multiline_spinner_item,
                            R.id.spinner_item,
                            values,
                            mHint);
            // Wrap the TextView in the dropdown popup around with a FrameLayout to display the text
            // in multiple lines.
            // Note that the TextView in the dropdown popup is displayed in a DropDownListView for
            // the dropdown style Spinner and the DropDownListView sets to display TextView instance
            // in a single line.
            mAdapter.setDropDownViewResource(R.layout.payment_request_dropdown_item);
        } else {
            mAdapter =
                    new DropdownFieldAdapter<String>(
                            mContext, R.layout.multiline_spinner_item, values);
            mAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        }
        mDropdown.setAdapter(mAdapter);
    }

    void setValue(@Nullable String value) {
        if (mAdapter == null) {
            // Can't set value when adapter hasn't been initialized.
            return;
        }
        // If no value is selected or the value previously entered is not valid, we'll  select the
        // hint, which is the first item on the dropdown adapter. We also need to check for both key
        // and value, because the saved value could take both forms (NY or New York).
        mSelectedIndex = TextUtils.isEmpty(value) ? 0 : mAdapter.getPosition(value);
        if (mSelectedIndex < 0) {
            // Assuming that value is the value (NY).
            mSelectedIndex = mAdapter.getPosition(getDropdownValueByKey(mFieldModel, value));
        }
        // Invalid value in the mFieldModel
        if (mSelectedIndex < 0) mSelectedIndex = 0;
        mDropdown.setSelection(mSelectedIndex);
    }

    void setErrorMessage(@Nullable String errorMessage) {
        View view = mDropdown.getSelectedView();
        if (errorMessage == null) {
            // {@link Spinner#getSelectedView()} is null in JUnit tests.
            if (view != null && view instanceof TextView) {
                ((TextView) view).setError(null);
            }
            mUnderline.setBackgroundColor(mContext.getColor(R.color.modern_grey_600));
            mErrorLabel.setText(null);
            mErrorLabel.setVisibility(View.GONE);
            return;
        }

        Drawable drawable =
                TraceEventVectorDrawableCompat.create(
                        mContext.getResources(), R.drawable.ic_error, mContext.getTheme());
        drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
        if (view != null && view instanceof TextView) {
            ((TextView) view).setError(errorMessage, drawable);
        }
        mUnderline.setBackgroundColor(mContext.getColor(R.color.default_text_color_error));
        mErrorLabel.setText(errorMessage);
        mErrorLabel.setVisibility(View.VISIBLE);

        if (sObserverForTest != null) {
            sObserverForTest.onEditorValidationError();
        }
    }

    void setValidator(@Nullable EditorFieldValidator validator) {
        mValidator = validator;
    }

    @Override
    public void setShowRequiredIndicator(boolean showRequiredIndicator) {
        mShowRequiredIndicator = showRequiredIndicator;
        setLabel(mFieldModel.get(LABEL), mFieldModel.get(IS_REQUIRED));
    }

    /** @return The View containing everything. */
    public View getLayout() {
        return mLayout;
    }

    /** @return The PropertyModel that the DropdownFieldView represents. */
    public PropertyModel getFieldModel() {
        return mFieldModel;
    }

    /** @return The label view for the spinner. */
    public View getLabel() {
        return mLabel;
    }

    /** @return The dropdown view itself. */
    public Spinner getDropdown() {
        return mDropdown;
    }

    public TextView getErrorLabelForTests() {
        return mErrorLabel;
    }

    @Override
    public boolean validate() {
        if (mValidator != null) {
            mValidator.validate(mFieldModel);
        }
        return mFieldModel.get(ERROR_MESSAGE) == null;
    }

    @Override
    public boolean isRequired() {
        return mFieldModel.get(IS_REQUIRED);
    }

    @Override
    public void scrollToAndFocus() {
        requestFocusAndHideKeyboard();
    }

    private void requestFocusAndHideKeyboard() {
        // Clear 'focused' bit because dropdown is not focusable. (crbug.com/1474419)
        mFieldModel.set(FOCUSED, false);

        KeyboardVisibilityDelegate.getInstance().hideKeyboard(mDropdown);
        ViewGroup parent = (ViewGroup) mDropdown.getParent();
        if (parent != null) parent.requestChildFocus(mDropdown, mDropdown);
        mDropdown.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
    }

    public static void setEditorObserverForTest(EditorObserverForTest observerForTest) {
        sObserverForTest = observerForTest;
        ResettersForTesting.register(() -> sObserverForTest = null);
    }
}