chromium/components/subresource_filter/android/java/src/org/chromium/components/subresource_filter/AdsBlockedDialog.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.components.subresource_filter;

import android.content.Context;
import android.content.res.Resources;
import android.os.Handler;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.ClickableSpan;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

import org.jni_zero.CalledByNative;
import org.jni_zero.NativeMethods;

import org.chromium.base.ThreadUtils;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogManager.ModalDialogType;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modaldialog.ModalDialogProperties.ButtonType;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.text.NoUnderlineClickableSpan;

/**
 * Java part of AdsBlockedDialog pair providing communication between native ads blocked
 * delegate code and Java ads blocked dialog UI components.
 */
public class AdsBlockedDialog implements ModalDialogProperties.Controller {
    private long mNativeDialog;
    private final Context mContext;
    private final ModalDialogManager mModalDialogManager;
    private PropertyModel mDialogModel;
    private ClickableSpan mClickableSpan;
    private Handler mDialogHandler;

    @CalledByNative
    static AdsBlockedDialog create(long nativeDialog, @NonNull WindowAndroid windowAndroid) {
        return new AdsBlockedDialog(nativeDialog, windowAndroid);
    }

    AdsBlockedDialog(long nativeDialog, @NonNull WindowAndroid windowAndroid) {
        mNativeDialog = nativeDialog;
        mContext = windowAndroid.getContext().get();
        mModalDialogManager = windowAndroid.getModalDialogManager();
        mDialogHandler = new Handler(ThreadUtils.getUiThreadLooper());
    }

    /**
     * Internal constructor for {@link AdsBlockedDialog}. Used by tests to inject
     * parameters. External code should use AdsBlockedDialog#create.
     *
     * @param nativeDialog The pointer to the dialog instance created by native code.
     * @param context The context for accessing resources.
     * @param modalDialogManager The ModalDialogManager to display the dialog.
     * @param dialogHandler The {@link Handler} used to post the call to show the dialog.
     */
    @VisibleForTesting
    AdsBlockedDialog(
            long nativeDialog,
            @NonNull Context context,
            @NonNull ModalDialogManager modalDialogManager,
            Handler dialogHandler) {
        mNativeDialog = nativeDialog;
        mContext = context;
        mModalDialogManager = modalDialogManager;
        mDialogHandler = dialogHandler;
    }

    PropertyModel getDialogModelForTesting() {
        return mDialogModel;
    }

    ClickableSpan getMessageClickableSpanForTesting() {
        return mClickableSpan;
    }

    @CalledByNative
    void show(boolean shouldPostDialog) {
        Resources resources = mContext.getResources();
        mClickableSpan =
                new NoUnderlineClickableSpan(
                        mContext,
                        (view) -> AdsBlockedDialogJni.get().onLearnMoreClicked(mNativeDialog));
        mDialogModel =
                new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
                        .with(ModalDialogProperties.CONTROLLER, this)
                        .with(
                                ModalDialogProperties.TITLE,
                                resources,
                                R.string.blocked_ads_dialog_title)
                        .with(ModalDialogProperties.MESSAGE_PARAGRAPH_1, getFormattedMessageText())
                        .with(
                                ModalDialogProperties.POSITIVE_BUTTON_TEXT,
                                resources,
                                R.string.blocked_ads_dialog_always_allow)
                        .with(
                                ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
                                resources,
                                R.string.cancel)
                        .with(ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE, true)
                        .with(ModalDialogProperties.FOCUS_DIALOG, true)
                        .build();

        // shouldPostDialog determines if ModalDialogManager#showDialog should be invoked directly
        // or using Handler#post.
        // The dialog should be re-shown on the original tab on navigation back from the redirected
        // support link tab. This redirection is observed by
        // WebContentsObserver#OnWebContentsFocused and
        // TabModelSelectorTabModelObserver#didSelectTab. The sequence of invocation of these
        // methods is not consistent on phones and tablets. Using #post will delay the
        // #showDialog request reliably to after both tab events are handled on the UI thread,
        // guaranteeing that the dialog will be re-shown as expected. See crbug.com/1261967 for
        // details.
        // TODO (crbug.com/1272049): Investigate tab event observer ordering and tab modal
        // suspension logic as a follow up.
        if (shouldPostDialog) {
            mDialogHandler.post(
                    () -> mModalDialogManager.showDialog(mDialogModel, ModalDialogType.TAB));
        } else {
            mModalDialogManager.showDialog(mDialogModel, ModalDialogType.TAB);
        }
    }

    @CalledByNative
    void dismiss() {
        mModalDialogManager.dismissDialog(mDialogModel, DialogDismissalCause.DISMISSED_BY_NATIVE);
    }

    // Returns link-formatted message text for the ads blocked dialog.
    @VisibleForTesting
    CharSequence getFormattedMessageText() {
        Resources resources = mContext.getResources();
        String messageText = resources.getString(R.string.blocked_ads_dialog_message);
        String learnMoreLinkText = resources.getString(R.string.blocked_ads_dialog_learn_more);
        final SpannableString formattedLinkText = new SpannableString(learnMoreLinkText);
        formattedLinkText.setSpan(
                mClickableSpan, 0, learnMoreLinkText.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
        return TextUtils.expandTemplate(messageText, formattedLinkText);
    }

    // ModalDialogProperties.Controller implementation.
    @Override
    public void onClick(PropertyModel model, @ButtonType int buttonType) {
        assert mNativeDialog != 0;
        if (buttonType == ButtonType.POSITIVE) {
            AdsBlockedDialogJni.get().onAllowAdsClicked(mNativeDialog);
        }
        mModalDialogManager.dismissDialog(
                model,
                buttonType == ButtonType.POSITIVE
                        ? DialogDismissalCause.POSITIVE_BUTTON_CLICKED
                        : DialogDismissalCause.NEGATIVE_BUTTON_CLICKED);
    }

    @Override
    public void onDismiss(PropertyModel model, @DialogDismissalCause int dismissalCause) {
        mDialogHandler.removeCallbacksAndMessages(null);
        AdsBlockedDialogJni.get().onDismissed(mNativeDialog);
        mNativeDialog = 0;
    }

    @NativeMethods
    interface Natives {
        void onAllowAdsClicked(long nativeAdsBlockedDialog);

        void onLearnMoreClicked(long nativeAdsBlockedDialog);

        void onDismissed(long nativeAdsBlockedDialog);
    }
}