chromium/chrome/android/java/src/org/chromium/chrome/browser/privacy_sandbox/TrackingProtectionSnackbarController.java

// Copyright 2024 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.privacy_sandbox;

import android.content.Context;

import androidx.annotation.Nullable;

import org.chromium.base.ContextUtils;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.flags.ActivityType;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.ui.messages.snackbar.Snackbar;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager.SnackbarController;
import org.chromium.components.content_settings.CookieBlocking3pcdStatus;
import org.chromium.components.content_settings.CookieControlsBridge;
import org.chromium.components.content_settings.CookieControlsEnforcement;
import org.chromium.components.content_settings.CookieControlsObserver;
import org.chromium.content_public.browser.BrowserContextHandle;
import org.chromium.content_public.browser.WebContents;

import java.util.concurrent.locks.ReentrantLock;

/**
 * Displays the {@link Snackbar} with provided actions for WebApk (aka PWA - Progressive Web Apps).
 *
 * <p>It checks whether the Tracking Protection {@link Snackbar} logic should be executed by looking
 * into {@link ActivityType}. If the provided {@link ActivityType} does not identify the caller as
 * WebApk, the logic won't be executed.
 */
public class TrackingProtectionSnackbarController implements CookieControlsObserver {
    private static final long MINIMUM_DELAY_BETWEEN_CONSECUTIVE_SNACKBARS_MS = 5 * 60 * 1000;

    private final Supplier<SnackbarManager> mSnackbarManagerSupplier;
    private final Runnable mSnakcbarOnAction;
    private final CookieControlsBridge mCookieControlsBridge;
    private final @ActivityType int mActivityType;
    private final ReentrantLock mLock = new ReentrantLock();
    private SnackbarController mSnackbarController =
            new SnackbarController() {
                @Override
                public void onDismissNoAction(Object actionData) {}

                @Override
                public void onAction(Object actionData) {
                    mSnakcbarOnAction.run();
                }
            };
    private boolean mTrackingProtectionControlsVisible;
    private boolean mTrackingProtectionBlocked;
    private int mBlockingStatus3pcd;
    private TrackingProtectionSnackbarLimiter mTrackingProtectionLimiter;
    private WebContents mWebContents;

    /**
     * Creates the {@link TrackingProtectionSnackbarController} object.
     *
     * @param snackbarOnAction logic to be executed when action button was clicked.
     * @param snackbarManagerSupplier supplier of {@link SnackbarManager} used to generate {@link
     *     Snackbar} object when requested.
     * @param webContents The WebContents instance to observe.
     * @param originalBrowserContext The "original" browser context. In Chrome, this corresponds to
     *     the regular profile when webContents is incognito.
     */
    public TrackingProtectionSnackbarController(
            Runnable snackbarOnAction,
            Supplier<SnackbarManager> snackbarManagerSupplier,
            WebContents webContents,
            @Nullable BrowserContextHandle originalBrowserContext,
            @ActivityType int activityType) {
        mSnakcbarOnAction = snackbarOnAction;
        mSnackbarManagerSupplier = snackbarManagerSupplier;
        mCookieControlsBridge = new CookieControlsBridge(this, webContents, originalBrowserContext);
        mActivityType = activityType;
        mWebContents = webContents;
        mTrackingProtectionLimiter = new TrackingProtectionSnackbarLimiter();
    }

    @Override
    public void onHighlightCookieControl(boolean shouldHighlight) {
        if (mBlockingStatus3pcd == CookieBlocking3pcdStatus.NOT_IN3PCD) {
            return;
        }

        if (mTrackingProtectionControlsVisible && !mTrackingProtectionBlocked && shouldHighlight) {
            showSnackbar();
        }
    }

    @Override
    public void onHighlightPwaCookieControl() {
        maybeTriggerSnackbar();
    }

    @Override
    public void onStatusChanged(
            boolean controlsVisible,
            boolean protectionsOn,
            @CookieControlsEnforcement int enforcement,
            @CookieBlocking3pcdStatus int blockingStatus,
            long expiration) {
        mTrackingProtectionControlsVisible = controlsVisible;
        mTrackingProtectionBlocked = protectionsOn;
        mBlockingStatus3pcd = blockingStatus;
    }

    /**
     * Checks PWA {@link Snackbar} eligibility criteria and triggers it if needed.
     *
     * <p>It takes into account both rate limiting and test / feature triggers.
     */
    public void maybeTriggerSnackbar() {
        if (!ChromeFeatureList.isEnabled(ChromeFeatureList.TRACKING_PROTECTION_USER_BYPASS_PWA)) {
            return;
        }

        boolean forceTriggerEnabled =
                ChromeFeatureList.isEnabled(
                        ChromeFeatureList.TRACKING_PROTECTION_USER_BYPASS_PWA_TRIGGER);

        String host = "";
        if (mWebContents != null && mWebContents.getLastCommittedUrl() != null) {
            host = mWebContents.getLastCommittedUrl().getHost();
        }

        if (!forceTriggerEnabled
                && (!mTrackingProtectionLimiter.shouldAllowRequest(host)
                        || !mTrackingProtectionControlsVisible
                        || mTrackingProtectionBlocked)) {
            return;
        }

        showSnackbar();
    }

    /**
     * Shows {@link Snackbar} for TrackingProtection if the provided {@link ActivityType} is
     * correct.
     */
    private void showSnackbar() {
        boolean locked = mLock.tryLock();
        try {
            if (!locked) {
                return;
            }
            if (!ChromeFeatureList.isEnabled(ChromeFeatureList.TRACKING_PROTECTION_USER_BYPASS_PWA)
                    || mActivityType != ActivityType.WEB_APK) {
                return;
            }

            Context context = ContextUtils.getApplicationContext();
            Snackbar snackbar =
                    Snackbar.make(
                            context.getString(
                                    R.string.privacy_sandbox_tracking_protection_snackbar_title),
                            mSnackbarController,
                            Snackbar.TYPE_NOTIFICATION,
                            Snackbar.UMA_SPECIAL_LOCALE);
            snackbar.setDuration(SnackbarManager.DEFAULT_SNACKBAR_DURATION_LONG_MS);
            snackbar.setAction(
                    context.getString(R.string.privacy_sandbox_tracking_protection_snackbar_action),
                    null);
            mSnackbarManagerSupplier.get().showSnackbar(snackbar);
            mCookieControlsBridge.onEntryPointAnimated();
        } finally {
            mLock.unlock();
        }
    }
}