chromium/components/privacy_sandbox/android/java/src/org/chromium/components/privacy_sandbox/TrackingProtectionSettings.java

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

import static org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridge.SITE_WILDCARD;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Browser;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.view.View;

import androidx.annotation.ColorInt;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.preference.Preference;
import androidx.preference.Preference.OnPreferenceClickListener;
import androidx.preference.PreferenceFragmentCompat;

import org.chromium.base.IntentUtils;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.components.browser_ui.settings.ChromeSwitchPreference;
import org.chromium.components.browser_ui.settings.CustomDividerFragment;
import org.chromium.components.browser_ui.settings.ExpandablePreferenceGroup;
import org.chromium.components.browser_ui.settings.SettingsPage;
import org.chromium.components.browser_ui.settings.SettingsUtils;
import org.chromium.components.browser_ui.settings.TextMessagePreference;
import org.chromium.components.browser_ui.site_settings.AddExceptionPreference;
import org.chromium.components.browser_ui.site_settings.AddExceptionPreference.SiteAddedCallback;
import org.chromium.components.browser_ui.site_settings.SiteSettingsCategory;
import org.chromium.components.browser_ui.site_settings.Website;
import org.chromium.components.browser_ui.site_settings.WebsitePermissionsFetcher;
import org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridge;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.content_settings.ContentSettingValues;
import org.chromium.components.content_settings.ContentSettingsType;
import org.chromium.ui.text.NoUnderlineClickableSpan;
import org.chromium.ui.text.SpanApplier;
import org.chromium.ui.widget.Toast;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;

/** Fragment to manage settings for tracking protection. */
public class TrackingProtectionSettings extends PreferenceFragmentCompat
        implements CustomDividerFragment,
                OnPreferenceClickListener,
                SiteAddedCallback,
                SettingsPage {
    // Must match keys in tracking_protection_preferences.xml.
    private static final String OFFBOARDING_NOTICE = "offboarding_notice";
    private static final String PREF_BLOCK_ALL_TOGGLE = "block_all_3pcd_toggle";
    private static final String PREF_IP_PROTECTION_TOGGLE = "ip_protection_toggle";
    private static final String PREF_IP_PROTECTION_LEARN_MORE = "ip_protection_learn_more";
    private static final String PREF_FINGERPRINTING_PROTECTION_TOGGLE =
            "fingerprinting_protection_toggle";
    private static final String PREF_FINGERPRINTING_PROTECTION_LEARN_MORE =
            "fingerprinting_protection_learn_more";
    private static final String PREF_DNT_TOGGLE = "dnt_toggle";
    private static final String PREF_BULLET_TWO = "bullet_point_two";
    private static final String ALLOWED_GROUP = "allowed_group";
    public static final String ADD_EXCEPTION_KEY = "add_exception";

    public static final String LEARN_MORE_URL =
            "https://support.google.com/chrome/?p=tracking_protection";

    // The number of sites that are on the Allowed list.
    private int mAllowedSiteCount;

    // Whether the Allowed list should be shown expanded.
    private boolean mAllowListExpanded = true;

    private TrackingProtectionDelegate mDelegate;

    private CustomTabIntentHelper mCustomTabIntentHelper;

    private final ObservableSupplierImpl<String> mPageTitle = new ObservableSupplierImpl<>();

    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        SettingsUtils.addPreferencesFromResource(this, R.xml.tracking_protection_preferences);
        mPageTitle.set(getString(R.string.privacy_sandbox_tracking_protection_title));

        // Format the Learn More link in the second bullet point.
        TextMessagePreference bulletTwo = (TextMessagePreference) findPreference(PREF_BULLET_TWO);
        bulletTwo.setSummary(
                SpanApplier.applySpans(
                        getResources()
                                .getString(
                                        R.string
                                                .privacy_sandbox_tracking_protection_bullet_two_description),
                        new SpanApplier.SpanInfo(
                                "<link>",
                                "</link>",
                                new NoUnderlineClickableSpan(
                                        getContext(), this::onLearnMoreClicked))));

        ChromeSwitchPreference blockAll3PCookiesSwitch =
                (ChromeSwitchPreference) findPreference(PREF_BLOCK_ALL_TOGGLE);
        ChromeSwitchPreference ipProtectionSwitch =
                (ChromeSwitchPreference) findPreference(PREF_IP_PROTECTION_TOGGLE);
        TextMessagePreference ipProtectionLearnMore =
                (TextMessagePreference) findPreference(PREF_IP_PROTECTION_LEARN_MORE);
        ChromeSwitchPreference fingerprintingProtectionSwitch =
                (ChromeSwitchPreference) findPreference(PREF_FINGERPRINTING_PROTECTION_TOGGLE);
        TextMessagePreference fingerprintingProtectionLearnMore =
                (TextMessagePreference) findPreference(PREF_FINGERPRINTING_PROTECTION_LEARN_MORE);
        ChromeSwitchPreference doNotTrackSwitch =
                (ChromeSwitchPreference) findPreference(PREF_DNT_TOGGLE);

        // Block all 3PCD switch.
        blockAll3PCookiesSwitch.setChecked(mDelegate.isBlockAll3PCDEnabled());
        blockAll3PCookiesSwitch.setOnPreferenceChangeListener(
                (preference, newValue) -> {
                    mDelegate.setBlockAll3PCD((boolean) newValue);
                    return true;
                });

        // IP protection switch.
        if (mDelegate.shouldDisplayIpProtection()) {
            ipProtectionSwitch.setVisible(true);
            ipProtectionSwitch.setChecked(mDelegate.isIpProtectionEnabled());
            ipProtectionSwitch.setOnPreferenceChangeListener(
                    (preference, newValue) -> {
                        mDelegate.setIpProtection((boolean) newValue);
                        return true;
                    });
            ipProtectionLearnMore.setVisible(true);
            // TODO(b/330745124): Update the learn more action.
            ipProtectionLearnMore.setSummary(
                    SpanApplier.applySpans(
                            getResources()
                                    .getString(
                                            R.string.tracking_protection_ip_protection_learn_more),
                            new SpanApplier.SpanInfo(
                                    "<link>",
                                    "</link>",
                                    new NoUnderlineClickableSpan(
                                            getContext(), this::onLearnMoreClicked))));
        }

        // Fingerprinting protection switch.
        if (mDelegate.shouldDisplayFingerprintingProtection()) {
            fingerprintingProtectionSwitch.setVisible(true);
            fingerprintingProtectionSwitch.setChecked(
                    mDelegate.isFingerprintingProtectionEnabled());
            fingerprintingProtectionSwitch.setOnPreferenceChangeListener(
                    (preference, newValue) -> {
                        mDelegate.setFingerprintingProtection((boolean) newValue);
                        return true;
                    });
            fingerprintingProtectionLearnMore.setVisible(true);
            // TODO(b/330745124): Update the learn more action.
            fingerprintingProtectionLearnMore.setSummary(
                    SpanApplier.applySpans(
                            getResources()
                                    .getString(
                                            R.string
                                                    .tracking_protection_fingerprinting_protection_learn_more),
                            new SpanApplier.SpanInfo(
                                    "<link>",
                                    "</link>",
                                    new NoUnderlineClickableSpan(
                                            getContext(), this::onLearnMoreClicked))));
        }

        // Do not track switch.
        doNotTrackSwitch.setChecked(mDelegate.isDoNotTrackEnabled());
        doNotTrackSwitch.setOnPreferenceChangeListener(
                (preference, newValue) -> {
                    mDelegate.setDoNotTrack((boolean) newValue);
                    return true;
                });

        mAllowListExpanded = true;
        mAllowedSiteCount = 0;
        ExpandablePreferenceGroup allowedGroup =
                getPreferenceScreen().findPreference(ALLOWED_GROUP);
        allowedGroup.setOnPreferenceClickListener(this);
        refreshBlockingExceptions();

        // Add the exceptions button.
        SiteSettingsCategory cookiesCategory =
                SiteSettingsCategory.createFromType(
                        mDelegate.getBrowserContext(),
                        SiteSettingsCategory.Type.THIRD_PARTY_COOKIES);
        getPreferenceScreen()
                .addPreference(
                        new AddExceptionPreference(
                                getContext(),
                                ADD_EXCEPTION_KEY,
                                getString(
                                        R.string
                                                .website_settings_third_party_cookies_page_add_allow_exception_description),
                                cookiesCategory,
                                this));
    }

    @Override
    public ObservableSupplier<String> getPageTitle() {
        return mPageTitle;
    }

    @Override
    public boolean hasDivider() {
        // Remove dividers between preferences.
        return false;
    }

    // OnPreferenceClickListener:
    @Override
    public boolean onPreferenceClick(Preference preference) {
        if (ALLOWED_GROUP.equals(preference.getKey())) {
            mAllowListExpanded = !mAllowListExpanded;
        }
        refreshBlockingExceptions();
        return true;
    }

    // AddExceptionPreference.SiteAddedCallback:
    @Override
    public void onAddSite(String primaryPattern, String secondaryPattern) {
        WebsitePreferenceBridge.setContentSettingCustomScope(
                mDelegate.getBrowserContext(),
                ContentSettingsType.COOKIES,
                primaryPattern,
                secondaryPattern,
                ContentSettingValues.ALLOW);

        String hostname = primaryPattern.equals(SITE_WILDCARD) ? secondaryPattern : primaryPattern;
        Toast.makeText(
                        getContext(),
                        getContext().getString(R.string.website_settings_add_site_toast, hostname),
                        Toast.LENGTH_SHORT)
                .show();

        refreshBlockingExceptions();
    }

    public void setTrackingProtectionDelegate(TrackingProtectionDelegate delegate) {
        mDelegate = delegate;
    }

    private void refreshBlockingExceptions() {
        SiteSettingsCategory cookiesCategory =
                SiteSettingsCategory.createFromType(
                        mDelegate.getBrowserContext(),
                        SiteSettingsCategory.Type.THIRD_PARTY_COOKIES);
        new WebsitePermissionsFetcher(mDelegate.getSiteSettingsDelegate(getContext()))
                .fetchPreferencesForCategory(cookiesCategory, this::onExceptionsFetched);
    }

    private void onExceptionsFetched(Collection<Website> sites) {
        List<WebsiteExceptionRowPreference> websites = new ArrayList<>();
        for (Website site : sites) {
            WebsiteExceptionRowPreference preference =
                    new WebsiteExceptionRowPreference(
                            getContext(), site, mDelegate, this::refreshBlockingExceptions);
            websites.add(preference);
        }

        ExpandablePreferenceGroup allowedGroup =
                getPreferenceScreen().findPreference(ALLOWED_GROUP);
        allowedGroup.removeAll();
        // Add the description preference.
        var description = new TextMessagePreference(getContext(), null);
        description.setSummary(getString(R.string.tracking_protection_allowed_group_description));
        allowedGroup.addPreference(description);
        mAllowedSiteCount = 0;
        for (WebsiteExceptionRowPreference website : websites) {
            allowedGroup.addPreference(website);
            mAllowedSiteCount++;
        }
        if (!mAllowListExpanded) allowedGroup.removeAll();
        updateExceptionsHeader();
    }

    private void updateExceptionsHeader() {
        ExpandablePreferenceGroup allowedGroup =
                getPreferenceScreen().findPreference(ALLOWED_GROUP);
        SpannableStringBuilder spannable =
                new SpannableStringBuilder(
                        getString(R.string.tracking_protection_allowed_group_title));
        String prefCount = String.format(Locale.getDefault(), " - %d", mAllowedSiteCount);
        spannable.append(prefCount);

        // Color the first part of the title blue.
        ForegroundColorSpan blueSpan =
                new ForegroundColorSpan(
                        SemanticColorUtils.getDefaultTextColorAccent1(getContext()));
        spannable.setSpan(
                blueSpan,
                0,
                spannable.length() - prefCount.length(),
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        // Gray out the total count of items.
        final @ColorInt int gray = SemanticColorUtils.getDefaultTextColorSecondary(getContext());
        spannable.setSpan(
                new ForegroundColorSpan(gray),
                spannable.length() - prefCount.length(),
                spannable.length(),
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        // Configure the preference group.
        allowedGroup.setTitle(spannable);
        allowedGroup.setExpanded(mAllowListExpanded);
    }

    private void onLearnMoreClicked(View view) {
        openUrlInCct(LEARN_MORE_URL);
    }

    public void setCustomTabIntentHelper(CustomTabIntentHelper helper) {
        mCustomTabIntentHelper = helper;
    }

    private void openUrlInCct(String url) {
        assert (mCustomTabIntentHelper != null)
                : "CCT helpers must be set on TrackingProtectionSettings before opening a link";
        CustomTabsIntent customTabIntent =
                new CustomTabsIntent.Builder().setShowTitle(true).build();
        customTabIntent.intent.setData(Uri.parse(url));
        Intent intent =
                mCustomTabIntentHelper.createCustomTabActivityIntent(
                        getContext(), customTabIntent.intent);
        intent.setPackage(getContext().getPackageName());
        intent.putExtra(Browser.EXTRA_APPLICATION_ID, getContext().getPackageName());
        IntentUtils.addTrustedIntentExtras(intent);
        IntentUtils.safeStartActivity(getContext(), intent);
    }
}