chromium/chrome/android/java/src/org/chromium/chrome/browser/autofill/AutofillSaveCardPromptBase.java

// Copyright 2021 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;

import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewStub;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.DrawableRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.Nullable;

import org.chromium.chrome.R;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.components.autofill.payments.LegalMessageLine;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyModel;

/**
 * Base class for creating autofill save card prompts that support displaying legal message line.
 */
public abstract class AutofillSaveCardPromptBase implements ModalDialogProperties.Controller {
    private final AutofillSaveCardPromptBaseDelegate mBaseDelegate;

    protected PropertyModel mDialogModel;
    protected ModalDialogManager mModalDialogManager;
    protected Context mContext;
    protected View mDialogView;
    private SpannableStringBuilder mSpannableStringBuilder;

    interface AutofillSaveCardPromptBaseDelegate {
        /** Called when a link is clicked. */
        void onLinkClicked(String url);

        /** Called whenever the dialog is dismissed. */
        void onPromptDismissed();

        /**
         * Called when the dialog is dismissed neither because the user accepted/confirmed the
         * prompt or it was dismissed by native code.
         */
        void onUserDismiss();
    }

    /**
     * @param context The {@link Context} to inflate layout xml.
     * @param delegate A {@link AutofillSaveCardPromptBaseDelegate} to handle events.
     * @param contentLayoutId The content of the prompt dialog. Set 0 to make content empty.
     * @param customTitleLayoutId Layout id for a custom title and icon view.
     * @param title Title of the prompt dialog.
     * @param titleIcon Icon near the title. Set 0 to ignore this icon.
     * @param confirmButtonLabel The text of confirm button.
     * @param filledConfirmButton Whether to use a button of filled style.
     */
    protected AutofillSaveCardPromptBase(
            Context context,
            AutofillSaveCardPromptBaseDelegate delegate,
            @LayoutRes int contentLayoutId,
            @LayoutRes int customTitleLayoutId,
            String title,
            @DrawableRes int titleIcon,
            String confirmButtonLabel,
            boolean filledConfirmButton) {
        mBaseDelegate = delegate;
        LayoutInflater inflater = LayoutInflater.from(context);
        mDialogView = inflater.inflate(R.layout.autofill_save_card_base_layout, null);
        boolean useCustomTitleView =
                ChromeFeatureList.isEnabled(
                                ChromeFeatureList
                                        .AUTOFILL_ENABLE_MOVING_GPAY_LOGO_TO_THE_RIGHT_ON_CLANK)
                        && customTitleLayoutId != Resources.ID_NULL;

        if (useCustomTitleView) {
            ViewStub stub = mDialogView.findViewById(R.id.title_with_icon_stub);
            stub.setLayoutResource(customTitleLayoutId);
            stub.inflate();
        }

        if (contentLayoutId != 0) {
            ViewStub stub = mDialogView.findViewById(R.id.autofill_save_card_content_stub);
            stub.setLayoutResource(contentLayoutId);
            stub.inflate();
        }

        PropertyModel.Builder builder =
                new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
                        .with(ModalDialogProperties.CONTROLLER, this)
                        .with(ModalDialogProperties.CUSTOM_VIEW, mDialogView)
                        .with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, confirmButtonLabel)
                        .with(
                                ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
                                context.getResources(),
                                R.string.cancel)
                        .with(ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE, false)
                        .with(ModalDialogProperties.POSITIVE_BUTTON_DISABLED, true)
                        .with(
                                ModalDialogProperties.BUTTON_STYLES,
                                filledConfirmButton
                                        ? ModalDialogProperties.ButtonStyles
                                                .PRIMARY_FILLED_NEGATIVE_OUTLINE
                                        : ModalDialogProperties.ButtonStyles
                                                .PRIMARY_OUTLINE_NEGATIVE_OUTLINE);

        updateTitleView(useCustomTitleView, title, titleIcon, builder, context);
        mDialogModel = builder.build();
        mContext = context;
    }

    /**
     * Show the dialog.
     *
     * @param activity The current activity, used for context. When null, the method does nothing.
     * @param modalDialogManager Used to display modal dialogs. When null, the method does nothing.
     */
    public final void show(
            @Nullable Activity activity, @Nullable ModalDialogManager modalDialogManager) {
        if (activity == null || modalDialogManager == null) return;

        if (mSpannableStringBuilder != null) {
            TextView legalMessage = mDialogView.findViewById(R.id.legal_message);
            legalMessage.setText(mSpannableStringBuilder);
            legalMessage.setVisibility(View.VISIBLE);
            legalMessage.setMovementMethod(LinkMovementMethod.getInstance());
        }

        mContext = activity;
        mModalDialogManager = modalDialogManager;
        mModalDialogManager.showDialog(mDialogModel, ModalDialogManager.ModalDialogType.APP);
    }

    /**
     * Updates the title and icon view. If AUTOFILL_ENABLE_MOVING_GPAY_LOGO_TO_THE_RIGHT_ON_CLANK
     * feature is enabled, sets title and icon in the customView otherwise uses
     * PropertyModel.Builder for title and icon.
     *
     * @param useCustomTitleView Indicates true/false to use custom title view.
     * @param title Title of the prompt dialog.
     * @param titleIcon Icon near the title.
     * @param builder The PropertyModel.Builder instance.
     * @param context The {@link Context} to inflate layout xml.
     */
    private void updateTitleView(
            boolean useCustomTitleView,
            String title,
            @DrawableRes int titleIcon,
            PropertyModel.Builder builder,
            Context context) {
        if (useCustomTitleView) {
            TextView titleView = mDialogView.findViewById(R.id.title);
            titleView.setText(title);

            if (titleIcon != Resources.ID_NULL) {
                ImageView iconView = mDialogView.findViewById(R.id.title_icon);
                iconView.setImageResource(titleIcon);
            } else {
                mDialogView.findViewById(R.id.title_icon).setVisibility(View.GONE);
            }
        } else {
            builder.with(ModalDialogProperties.TITLE, title);
            if (titleIcon != Resources.ID_NULL) {
                builder.with(ModalDialogProperties.TITLE_ICON, context, titleIcon);
            }
        }
    }

    public void addLegalMessageLine(LegalMessageLine line) {
        if (mSpannableStringBuilder == null) {
            mSpannableStringBuilder = new SpannableStringBuilder();
        } else {
            // If this isn't the first line, append a new line before the legal message.
            mSpannableStringBuilder.append("\n\n");
        }
        int offset = mSpannableStringBuilder.length();
        mSpannableStringBuilder.append(line.text);
        for (final LegalMessageLine.Link link : line.links) {
            String url = link.url;
            mSpannableStringBuilder.setSpan(
                    new ClickableSpan() {
                        @Override
                        public void onClick(View view) {
                            mBaseDelegate.onLinkClicked(url);
                        }
                    },
                    link.start + offset,
                    link.end + offset,
                    Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        }
    }

    public void dismiss(@DialogDismissalCause int dismissalCause) {
        mModalDialogManager.dismissDialog(mDialogModel, dismissalCause);
    }
}