chromium/components/browser_ui/modaldialog/android/java/src/org/chromium/components/browser_ui/modaldialog/ModalDialogViewBinder.java

// Copyright 2018 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.browser_ui.modaldialog;

import android.text.TextUtils;
import android.view.View;

import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;

import java.util.Arrays;

/**
 * This class is responsible for binding view properties from {@link ModalDialogProperties} to a
 * {@link ModalDialogView}.
 */
public class ModalDialogViewBinder
        implements PropertyModelChangeProcessor.ViewBinder<
                PropertyModel, ModalDialogView, PropertyKey> {
    @Override
    public void bind(PropertyModel model, ModalDialogView view, PropertyKey propertyKey) {
        if (ModalDialogProperties.TITLE == propertyKey) {
            view.setTitle(model.get(ModalDialogProperties.TITLE));
        } else if (ModalDialogProperties.TITLE_MAX_LINES == propertyKey) {
            view.setTitleMaxLines(model.get(ModalDialogProperties.TITLE_MAX_LINES));
        } else if (ModalDialogProperties.TITLE_ICON == propertyKey) {
            view.setTitleIcon(model.get(ModalDialogProperties.TITLE_ICON));
        } else if (ModalDialogProperties.MESSAGE_PARAGRAPH_1 == propertyKey) {
            view.setMessageParagraph1(model.get(ModalDialogProperties.MESSAGE_PARAGRAPH_1));
        } else if (ModalDialogProperties.MESSAGE_PARAGRAPH_2 == propertyKey) {
            view.setMessageParagraph2(model.get(ModalDialogProperties.MESSAGE_PARAGRAPH_2));
        } else if (ModalDialogProperties.BUTTON_GROUP_BUTTON_SPEC_LIST == propertyKey) {
            assert checkFilterTouchConsistency(model);
            assert checkDefaultButtonsNotCombinedWithButtonGroup(model);
            view.setupButtonGroup(model.get(ModalDialogProperties.BUTTON_GROUP_BUTTON_SPEC_LIST));
        } else if (ModalDialogProperties.CUSTOM_VIEW == propertyKey) {
            view.setCustomView(model.get(ModalDialogProperties.CUSTOM_VIEW));
        } else if (ModalDialogProperties.CUSTOM_BUTTON_BAR_VIEW == propertyKey) {
            view.setCustomButtonBar(model.get(ModalDialogProperties.CUSTOM_BUTTON_BAR_VIEW));
        } else if (ModalDialogProperties.POSITIVE_BUTTON_TEXT == propertyKey) {
            assert checkFilterTouchConsistency(model);
            assert checkDefaultButtonsNotCombinedWithButtonGroup(model);
            view.setButtonText(
                    ModalDialogProperties.ButtonType.POSITIVE,
                    model.get(ModalDialogProperties.POSITIVE_BUTTON_TEXT));
        } else if (ModalDialogProperties.POSITIVE_BUTTON_CONTENT_DESCRIPTION == propertyKey) {
            view.setButtonContentDescription(
                    ModalDialogProperties.ButtonType.POSITIVE,
                    model.get(ModalDialogProperties.POSITIVE_BUTTON_CONTENT_DESCRIPTION));
        } else if (ModalDialogProperties.POSITIVE_BUTTON_DISABLED == propertyKey) {
            view.setButtonEnabled(
                    ModalDialogProperties.ButtonType.POSITIVE,
                    !model.get(ModalDialogProperties.POSITIVE_BUTTON_DISABLED));
        } else if (ModalDialogProperties.NEGATIVE_BUTTON_TEXT == propertyKey) {
            assert checkFilterTouchConsistency(model);
            assert checkFilledButtonConsistency(model);
            assert checkDefaultButtonsNotCombinedWithButtonGroup(model);
            view.setButtonText(
                    ModalDialogProperties.ButtonType.NEGATIVE,
                    model.get(ModalDialogProperties.NEGATIVE_BUTTON_TEXT));
        } else if (ModalDialogProperties.NEGATIVE_BUTTON_CONTENT_DESCRIPTION == propertyKey) {
            view.setButtonContentDescription(
                    ModalDialogProperties.ButtonType.NEGATIVE,
                    model.get(ModalDialogProperties.NEGATIVE_BUTTON_CONTENT_DESCRIPTION));
        } else if (ModalDialogProperties.NEGATIVE_BUTTON_DISABLED == propertyKey) {
            view.setButtonEnabled(
                    ModalDialogProperties.ButtonType.NEGATIVE,
                    !model.get(ModalDialogProperties.NEGATIVE_BUTTON_DISABLED));
        } else if (ModalDialogProperties.FOOTER_MESSAGE == propertyKey) {
            view.setFooterMessage(model.get(ModalDialogProperties.FOOTER_MESSAGE));
        } else if (ModalDialogProperties.TITLE_SCROLLABLE == propertyKey) {
            view.setTitleScrollable(model.get(ModalDialogProperties.TITLE_SCROLLABLE));
        } else if (ModalDialogProperties.WRAP_CUSTOM_VIEW_IN_SCROLLABLE == propertyKey) {
            assert checkCustomViewScrollConsistency(model);
            view.setWrapCustomViewInScrollable(
                    model.get(ModalDialogProperties.WRAP_CUSTOM_VIEW_IN_SCROLLABLE));
        } else if (ModalDialogProperties.CONTROLLER == propertyKey) {
            view.setOnButtonClickedCallback(
                    (buttonType) -> {
                        model.get(ModalDialogProperties.CONTROLLER).onClick(model, buttonType);
                    });
        } else if (ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE == propertyKey) {
            // Intentionally left empty since this is a property for the dialog container.
        } else if (ModalDialogProperties.FILTER_TOUCH_FOR_SECURITY == propertyKey) {
            assert checkFilterTouchConsistency(model);
            view.setFilterTouchForSecurity(
                    model.get(ModalDialogProperties.FILTER_TOUCH_FOR_SECURITY));
        } else if (ModalDialogProperties.TOUCH_FILTERED_CALLBACK == propertyKey) {
            view.setOnTouchFilteredCallback(
                    model.get(ModalDialogProperties.TOUCH_FILTERED_CALLBACK));
        } else if (ModalDialogProperties.CONTENT_DESCRIPTION == propertyKey) {
            // Intentionally left empty since this is a property used for the dialog container.
        } else if (ModalDialogProperties.BUTTON_STYLES == propertyKey) {
            assert checkFilledButtonConsistency(model);
            assert checkButtonStyleIsOnlyConfiguredWithDefaultButtons(model);
            // Intentionally left empty since this is only read once before the dialog is inflated.
        } else if (ModalDialogProperties.DIALOG_STYLES == propertyKey) {
            int dialogStyle = model.get(ModalDialogProperties.DIALOG_STYLES);
            boolean ignoreWidthConstraints =
                    dialogStyle == ModalDialogProperties.DialogStyles.FULLSCREEN_DIALOG
                            || dialogStyle
                                    == ModalDialogProperties.DialogStyles.FULLSCREEN_DARK_DIALOG
                            || dialogStyle == ModalDialogProperties.DialogStyles.DIALOG_WHEN_LARGE;
            boolean ignoreHeightConstraint =
                    dialogStyle == ModalDialogProperties.DialogStyles.FULLSCREEN_DIALOG
                            || dialogStyle
                                    == ModalDialogProperties.DialogStyles.FULLSCREEN_DARK_DIALOG
                            || dialogStyle == ModalDialogProperties.DialogStyles.DIALOG_WHEN_LARGE;
            view.setIgnoreConstraints(ignoreWidthConstraints, ignoreHeightConstraint);
        } else if (ModalDialogProperties.BUTTON_TAP_PROTECTION_PERIOD_MS == propertyKey) {
            view.setButtonTapProtectionDurationMs(
                    model.get(ModalDialogProperties.BUTTON_TAP_PROTECTION_PERIOD_MS));
        } else if (ModalDialogProperties.FOCUS_DIALOG == propertyKey) {
            // Intentionally left empty since this is a property for the dialog container.
        } else {
            assert false : "Unhandled property detected in ModalDialogViewBinder!";
        }
    }

    /**
     * Checks if FILTER_TOUCH_FOR_SECURITY flag is consistent with the set of enabled buttons. Touch
     * event filtering in ModalDialogView is only applied to standard buttons and buttons in a
     * button group. When buttons are hidden, filtering touch events doesn't have effect.
     *
     * @return false if security sensitive dialog doesn't have any standard buttons or button group
     *     buttons configured with a text.
     */
    private static boolean checkFilterTouchConsistency(PropertyModel model) {
        return !model.get(ModalDialogProperties.FILTER_TOUCH_FOR_SECURITY)
                || isAnyDefaultButtonWithTextConfigured(model)
                || isButtongroupWithTextButtonsConfigured(model);
    }

    /**
     * Checks if the BUTTON_STYLES property is consistent with the set of enabled buttons. If the
     * primary button (== positive button for dialog view) is in filled state, it could be disabled
     * while the negative button is enabled. On the contrary, if the negative button is filled, it
     * could also be disabled while the primary button is enabled. This check only applies to
     * standard buttons on a modal dialog.
     *
     * @return false if one button is in filled state while the other button doesn't present. True
     *     otherwise.
     */
    private static boolean checkFilledButtonConsistency(PropertyModel model) {
        int styles = model.get(ModalDialogProperties.BUTTON_STYLES);
        if (styles == ModalDialogProperties.ButtonStyles.PRIMARY_FILLED_NEGATIVE_OUTLINE) {
            return !TextUtils.isEmpty(model.get(ModalDialogProperties.NEGATIVE_BUTTON_TEXT));
        } else if (styles == ModalDialogProperties.ButtonStyles.PRIMARY_OUTLINE_NEGATIVE_FILLED) {
            return !TextUtils.isEmpty(model.get(ModalDialogProperties.POSITIVE_BUTTON_TEXT));
        } else if (styles == ModalDialogProperties.ButtonStyles.PRIMARY_FILLED_NO_NEGATIVE) {
            return TextUtils.isEmpty(model.get(ModalDialogProperties.NEGATIVE_BUTTON_TEXT));
        }

        return true;
    }

    /**
     * Checks that BUTTON_STYLES isn't present together with CUSTOM_BUTTON_BAR_VIEW or a button
     * group component, because neither support default positive and negative buttons as well as
     * their styling.
     */
    private static boolean checkButtonStyleIsOnlyConfiguredWithDefaultButtons(PropertyModel model) {
        int styles = model.get(ModalDialogProperties.BUTTON_STYLES);
        View customButtons = model.get(ModalDialogProperties.CUSTOM_BUTTON_BAR_VIEW);
        ModalDialogProperties.ModalDialogButtonSpec[] buttonGroup =
                model.get(ModalDialogProperties.BUTTON_GROUP_BUTTON_SPEC_LIST);
        return styles == 0 || (customButtons == null && buttonGroup == null);
    }

    /** Checks that default button configurations aren't mixed with button group configurations. */
    private static boolean checkDefaultButtonsNotCombinedWithButtonGroup(PropertyModel model) {
        boolean defaultButtonsConfigured = isAnyDefaultButtonWithTextConfigured(model);
        boolean buttonGroupConfigured = isButtongroupWithTextButtonsConfigured(model);
        return (defaultButtonsConfigured ^ buttonGroupConfigured)
                || (!defaultButtonsConfigured && !buttonGroupConfigured);
    }

    /**
     * Checks that if a custom view that should be shown in a ScrollView, it is not itself a scroll
     * container.
     */
    private static boolean checkCustomViewScrollConsistency(PropertyModel model) {
        View customView = model.get(ModalDialogProperties.CUSTOM_VIEW);
        return customView == null
                || model.get(ModalDialogProperties.WRAP_CUSTOM_VIEW_IN_SCROLLABLE)
                        != customView.isScrollContainer();
    }

    private static boolean isButtongroupWithTextButtonsConfigured(PropertyModel model) {
        return model.get(ModalDialogProperties.BUTTON_GROUP_BUTTON_SPEC_LIST) != null
                && Arrays.stream(model.get(ModalDialogProperties.BUTTON_GROUP_BUTTON_SPEC_LIST))
                        .anyMatch(buttonSpec -> !TextUtils.isEmpty(buttonSpec.getText()));
    }

    private static boolean isAnyDefaultButtonWithTextConfigured(PropertyModel model) {
        return !TextUtils.isEmpty(model.get(ModalDialogProperties.POSITIVE_BUTTON_TEXT))
                || !TextUtils.isEmpty(model.get(ModalDialogProperties.NEGATIVE_BUTTON_TEXT));
    }
}