chromium/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SiteSettingsCategory.java

// Copyright 2015 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.site_settings;

import static org.chromium.components.content_settings.PrefNames.COOKIE_CONTROLS_MODE;

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Process;
import android.provider.Settings;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.preference.Preference;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.PackageManagerUtils;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.content_settings.ContentSettingsType;
import org.chromium.components.permissions.PermissionUtil;
import org.chromium.components.prefs.PrefService;
import org.chromium.components.subresource_filter.SubresourceFilterFeatureMap;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.content_public.browser.BrowserContextHandle;
import org.chromium.ui.text.SpanApplier;
import org.chromium.ui.text.SpanApplier.SpanInfo;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/** A base class for dealing with website settings categories. */
public class SiteSettingsCategory {
    @IntDef({
        Type.ALL_SITES,
        Type.ADS,
        Type.AUGMENTED_REALITY,
        Type.AUTOMATIC_DOWNLOADS,
        Type.BACKGROUND_SYNC,
        Type.BLUETOOTH,
        Type.BLUETOOTH_SCANNING,
        Type.CAMERA,
        Type.CLIPBOARD,
        Type.HAND_TRACKING,
        Type.IDLE_DETECTION,
        Type.DEVICE_LOCATION,
        Type.JAVASCRIPT,
        Type.MICROPHONE,
        Type.NFC,
        Type.NOTIFICATIONS,
        Type.POPUPS,
        Type.PROTECTED_MEDIA,
        Type.SENSORS,
        Type.SOUND,
        Type.USB,
        Type.VIRTUAL_REALITY,
        Type.USE_STORAGE,
        Type.AUTO_DARK_WEB_CONTENT,
        Type.REQUEST_DESKTOP_SITE,
        Type.FEDERATED_IDENTITY_API,
        Type.THIRD_PARTY_COOKIES,
        Type.SITE_DATA,
        Type.ANTI_ABUSE,
        Type.ZOOM,
        Type.STORAGE_ACCESS,
        Type.TRACKING_PROTECTION,
        Type.NUM_ENTRIES
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Type {
        // All updates here must also be reflected in {@link #preferenceKey(int)
        // preferenceKey} and {@link #contentSettingsType(int) contentSettingsType}.
        int ALL_SITES = 0;
        int ADS = 1;
        int AUGMENTED_REALITY = 2;
        int AUTOMATIC_DOWNLOADS = 3;
        int BACKGROUND_SYNC = 4;
        int BLUETOOTH_SCANNING = 5;
        int CAMERA = 6;
        int CLIPBOARD = 7;
        int DEVICE_LOCATION = 8;
        int IDLE_DETECTION = 9;
        int JAVASCRIPT = 10;
        int MICROPHONE = 11;
        int NFC = 12;
        int NOTIFICATIONS = 13;
        int POPUPS = 14;
        int PROTECTED_MEDIA = 15;
        int SENSORS = 16;
        int SOUND = 17;
        int USB = 18;
        int BLUETOOTH = 19;
        int VIRTUAL_REALITY = 20;
        int USE_STORAGE = 21;
        int AUTO_DARK_WEB_CONTENT = 22;
        int REQUEST_DESKTOP_SITE = 23;
        int FEDERATED_IDENTITY_API = 24;
        int THIRD_PARTY_COOKIES = 25;
        int SITE_DATA = 26;
        int ANTI_ABUSE = 27;
        int ZOOM = 28;
        int STORAGE_ACCESS = 29;
        int TRACKING_PROTECTION = 30;
        int HAND_TRACKING = 31;

        /** Number of handled categories used for calculating array sizes. */
        int NUM_ENTRIES = 32;
    }

    private final BrowserContextHandle mBrowserContextHandle;

    // The id of this category.
    private @Type int mCategory;

    // The id of a permission in Android M that governs this category. Can be blank if Android has
    // no equivalent permission for the category.
    private String mAndroidPermission;

    /**
     * Construct a SiteSettingsCategory.
     *
     * @param category The string id of the category to construct.
     * @param androidPermission A string containing the id of a toggle-able permission in Android
     *     that this category represents (or blank, if Android does not expose that permission).
     */
    protected SiteSettingsCategory(
            BrowserContextHandle browserContextHandle,
            @Type int category,
            String androidPermission) {
        mBrowserContextHandle = browserContextHandle;
        mCategory = category;
        mAndroidPermission = androidPermission;
    }

    /** Construct a SiteSettingsCategory from a type. */
    public static SiteSettingsCategory createFromType(
            BrowserContextHandle browserContextHandle, @Type int type) {
        if (type == Type.DEVICE_LOCATION) return new LocationCategory(browserContextHandle);
        if (type == Type.NFC) return new NfcCategory(browserContextHandle);
        if (type == Type.NOTIFICATIONS) return new NotificationCategory(browserContextHandle);

        final String permission;
        if (type == Type.CAMERA) {
            permission = android.Manifest.permission.CAMERA;
        } else if (type == Type.MICROPHONE) {
            permission = android.Manifest.permission.RECORD_AUDIO;
        } else if (type == Type.AUGMENTED_REALITY) {
            permission = android.Manifest.permission.CAMERA;
        } else if (type == Type.HAND_TRACKING
                && PackageManagerUtils.hasSystemFeature(
                        PackageManagerUtils.XR_IMMERSIVE_FEATURE_NAME)) {
            permission = PermissionUtil.ANDROID_PERMISSION_HAND_TRACKING;
        } else {
            permission = "";
        }
        return new SiteSettingsCategory(browserContextHandle, type, permission);
    }

    public static SiteSettingsCategory createFromContentSettingsType(
            BrowserContextHandle browserContextHandle,
            @ContentSettingsType.EnumType int contentSettingsType) {
        assert contentSettingsType != -1;
        assert Type.ALL_SITES == 0;
        for (@Type int i = Type.ALL_SITES; i < Type.NUM_ENTRIES; i++) {
            if (contentSettingsType(i) == contentSettingsType) {
                return createFromType(browserContextHandle, i);
            }
        }
        return null;
    }

    public static SiteSettingsCategory createFromPreferenceKey(
            BrowserContextHandle browserContextHandle, String preferenceKey) {
        assert Type.ALL_SITES == 0;
        for (@Type int i = Type.ALL_SITES; i < Type.NUM_ENTRIES; i++) {
            if (preferenceKey(i).equals(preferenceKey)) {
                return createFromType(browserContextHandle, i);
            }
        }
        return null;
    }

    /** Convert Type into {@link ContentSettingsType}. */
    public static @ContentSettingsType.EnumType int contentSettingsType(@Type int type) {
        // This switch statement is ordered by types alphabetically.
        switch (type) {
            case Type.ADS:
                return ContentSettingsType.ADS;
            case Type.ANTI_ABUSE:
                return ContentSettingsType.ANTI_ABUSE;
            case Type.AUGMENTED_REALITY:
                return ContentSettingsType.AR;
            case Type.AUTO_DARK_WEB_CONTENT:
                return ContentSettingsType.AUTO_DARK_WEB_CONTENT;
            case Type.AUTOMATIC_DOWNLOADS:
                return ContentSettingsType.AUTOMATIC_DOWNLOADS;
            case Type.BACKGROUND_SYNC:
                return ContentSettingsType.BACKGROUND_SYNC;
            case Type.BLUETOOTH:
                return ContentSettingsType.BLUETOOTH_GUARD;
            case Type.BLUETOOTH_SCANNING:
                return ContentSettingsType.BLUETOOTH_SCANNING;
            case Type.CAMERA:
                return ContentSettingsType.MEDIASTREAM_CAMERA;
            case Type.CLIPBOARD:
                return ContentSettingsType.CLIPBOARD_READ_WRITE;
            case Type.SITE_DATA:
            case Type.THIRD_PARTY_COOKIES:
                return ContentSettingsType.COOKIES;
            case Type.REQUEST_DESKTOP_SITE:
                return ContentSettingsType.REQUEST_DESKTOP_SITE;
            case Type.DEVICE_LOCATION:
                return ContentSettingsType.GEOLOCATION;
            case Type.FEDERATED_IDENTITY_API:
                return ContentSettingsType.FEDERATED_IDENTITY_API;
            case Type.HAND_TRACKING:
                return ContentSettingsType.HAND_TRACKING;
            case Type.IDLE_DETECTION:
                return ContentSettingsType.IDLE_DETECTION;
            case Type.JAVASCRIPT:
                return ContentSettingsType.JAVASCRIPT;
            case Type.MICROPHONE:
                return ContentSettingsType.MEDIASTREAM_MIC;
            case Type.NFC:
                return ContentSettingsType.NFC;
            case Type.NOTIFICATIONS:
                return ContentSettingsType.NOTIFICATIONS;
            case Type.POPUPS:
                return ContentSettingsType.POPUPS;
            case Type.PROTECTED_MEDIA:
                return ContentSettingsType.PROTECTED_MEDIA_IDENTIFIER;
            case Type.SENSORS:
                return ContentSettingsType.SENSORS;
            case Type.STORAGE_ACCESS:
                return ContentSettingsType.STORAGE_ACCESS;
            case Type.SOUND:
                return ContentSettingsType.SOUND;
            case Type.USB:
                return ContentSettingsType.USB_GUARD;
            case Type.VIRTUAL_REALITY:
                return ContentSettingsType.VR;
            case Type.ALL_SITES:
            case Type.USE_STORAGE:
            case Type.ZOOM:
            case Type.TRACKING_PROTECTION:
                return ContentSettingsType.DEFAULT; // Conversion unavailable.
        }
        assert false;
        return ContentSettingsType.DEFAULT;
    }

    /**
     * Get the chooser data type {@link ContentSettingsType} corresponding to the given {@link
     * ContentSettingsType}.
     */
    public static int objectChooserDataTypeFromGuard(@ContentSettingsType.EnumType int type) {
        switch (type) {
            case ContentSettingsType.USB_GUARD:
                return ContentSettingsType.USB_CHOOSER_DATA;
            case ContentSettingsType.BLUETOOTH_GUARD:
                return ContentSettingsType.BLUETOOTH_CHOOSER_DATA;
            default:
                return -1; // Conversion unavailable.
        }
    }

    /** Convert Type into preference String */
    public static String preferenceKey(@Type int type) {
        // This switch statement is ordered by types alphabetically.
        switch (type) {
            case Type.ADS:
                return "ads";
            case Type.ANTI_ABUSE:
                return "anti_abuse";
            case Type.AUGMENTED_REALITY:
                return "augmented_reality";
            case Type.AUTO_DARK_WEB_CONTENT:
                return "auto_dark_web_content";
            case Type.ALL_SITES:
                return "all_sites";
            case Type.AUTOMATIC_DOWNLOADS:
                return "automatic_downloads";
            case Type.BACKGROUND_SYNC:
                return "background_sync";
            case Type.BLUETOOTH:
                return "bluetooth";
            case Type.BLUETOOTH_SCANNING:
                return "bluetooth_scanning";
            case Type.CAMERA:
                return "camera";
            case Type.CLIPBOARD:
                return "clipboard";
            case Type.REQUEST_DESKTOP_SITE:
                return "request_desktop_site";
            case Type.DEVICE_LOCATION:
                return "device_location";
            case Type.FEDERATED_IDENTITY_API:
                return "federated_identity_api";
            case Type.HAND_TRACKING:
                return "hand_tracking";
            case Type.IDLE_DETECTION:
                return "idle_detection";
            case Type.JAVASCRIPT:
                return "javascript";
            case Type.MICROPHONE:
                return "microphone";
            case Type.NFC:
                return "nfc";
            case Type.NOTIFICATIONS:
                return "notifications";
            case Type.POPUPS:
                return "popups";
            case Type.PROTECTED_MEDIA:
                return "protected_content";
            case Type.SENSORS:
                return "sensors";
            case Type.STORAGE_ACCESS:
                return "storage_access";
            case Type.SOUND:
                return "sound";
            case Type.USB:
                return "usb";
            case Type.USE_STORAGE:
                return "use_storage";
            case Type.VIRTUAL_REALITY:
                return "virtual_reality";
            case Type.SITE_DATA:
                return "site_data";
            case Type.THIRD_PARTY_COOKIES:
                return "third_party_cookies";
            case Type.TRACKING_PROTECTION:
                return "tracking_protection";
            case Type.ZOOM:
                return "zoom";
            default:
                assert false;
                return "";
        }
    }

    /** Returns the {@link SiteSettingsCategory.Type} for this category. */
    public @Type int getType() {
        return mCategory;
    }

    /** Returns the {@link ContentSettingsType} for this category, or -1 if no such type exists. */
    public @ContentSettingsType.EnumType int getContentSettingsType() {
        return contentSettingsType(mCategory);
    }

    /**
     * Returns the {@link ContentSettingsType} representing the chooser data type for this category,
     * or -1 if this category does not have a chooser data type.
     */
    public @ContentSettingsType.EnumType int getObjectChooserDataType() {
        return objectChooserDataTypeFromGuard(contentSettingsType(mCategory));
    }

    /** Returns whether the Ads category is enabled via an experiment flag. */
    public static boolean adsCategoryEnabled() {
        return SubresourceFilterFeatureMap.isSubresourceFilterEnabled();
    }

    /**
     * Returns whether the current category is managed either by enterprise policy or by the
     * custodian of a supervised account.
     */
    public boolean isManaged() {
        // TODO(dullweber): Why do we check some permissions for managed state and some for user
        // modifiability and some not at all?
        if (mCategory == Type.AUTOMATIC_DOWNLOADS
                || mCategory == Type.BACKGROUND_SYNC
                || mCategory == Type.JAVASCRIPT
                || mCategory == Type.POPUPS) {
            return WebsitePreferenceBridge.isContentSettingManaged(
                    getBrowserContextHandle(), getContentSettingsType());
        } else if (mCategory == Type.DEVICE_LOCATION
                || mCategory == Type.CAMERA
                || mCategory == Type.MICROPHONE) {
            return !WebsitePreferenceBridge.isContentSettingUserModifiable(
                    getBrowserContextHandle(), getContentSettingsType());
        } else if (mCategory == Type.THIRD_PARTY_COOKIES) {
            PrefService prefService = UserPrefs.get(getBrowserContextHandle());
            return prefService.isManagedPreference(COOKIE_CONTROLS_MODE);
        }
        return false;
    }

    /**
     * Returns whether the current category is managed by the custodian (e.g. parent, not an
     * enterprise admin) of the account if the account is supervised.
     */
    public boolean isManagedByCustodian() {
        // TODO(dullweber): Why do we only check these types?
        if (mCategory == Type.DEVICE_LOCATION
                || mCategory == Type.CAMERA
                || mCategory == Type.MICROPHONE
                || mCategory == Type.SITE_DATA) {
            return WebsitePreferenceBridge.isContentSettingManagedByCustodian(
                    getBrowserContextHandle(), getContentSettingsType());
        }
        return false;
    }

    /**
     * Configure a preference to show when when the Android permission for this category is
     * disabled.
     *
     * @param osWarning A preference to hold the first permission warning. After calling this
     *     method, if osWarning has no title, the preference should not be added to the preference
     *     screen.
     * @param osWarningExtra A preference to hold any additional permission warning (if any). After
     *     calling this method, if osWarningExtra has no title, the preference should not be added
     *     to the preference screen.
     * @param context The current context.
     * @param specificCategory Whether the warnings refer to a single category or is an aggregate
     *     for many permissions.
     * @param appName The name of the app to use in warning strings.
     */
    public void configurePermissionIsOffPreferences(
            Preference osWarning,
            Preference osWarningExtra,
            Context context,
            boolean specificCategory,
            String appName) {
        Intent perAppIntent = getIntentToEnableOsPerAppPermission(context);
        Intent globalIntent = getIntentToEnableOsGlobalPermission(context);
        String perAppMessage =
                getMessageForEnablingOsPerAppPermission(context, !specificCategory, appName);
        String globalMessage = getMessageForEnablingOsGlobalPermission(context);
        String unsupportedMessage = getMessageIfNotSupported(context);

        int color = SemanticColorUtils.getDefaultControlColorActive(context);
        ForegroundColorSpan linkSpan = new ForegroundColorSpan(color);

        if (perAppIntent != null) {
            SpannableString messageWithLink =
                    SpanApplier.applySpans(
                            perAppMessage, new SpanInfo("<link>", "</link>", linkSpan));
            osWarning.setTitle(messageWithLink);
            osWarning.setIntent(perAppIntent);

            if (!specificCategory) {
                osWarning.setIcon(getDisabledInAndroidIcon(context));
            }
        }

        if (!supportedGlobally()) {
            osWarningExtra.setTitle(unsupportedMessage);
            osWarningExtra.setIcon(getDisabledInAndroidIcon(context));
        } else if (globalIntent != null) {
            SpannableString messageWithLink =
                    SpanApplier.applySpans(
                            globalMessage, new SpanInfo("<link>", "</link>", linkSpan));
            osWarningExtra.setTitle(messageWithLink);
            osWarningExtra.setIntent(globalIntent);

            if (!specificCategory) {
                if (perAppIntent == null) {
                    osWarningExtra.setIcon(getDisabledInAndroidIcon(context));
                } else {
                    Drawable transparent = new ColorDrawable(Color.TRANSPARENT);
                    osWarningExtra.setIcon(transparent);
                }
            }
        }
    }

    /** Returns the icon for permissions that have been disabled by Android. */
    Drawable getDisabledInAndroidIcon(Context context) {
        Drawable icon =
                ApiCompatibilityUtils.getDrawable(
                        context.getResources(), R.drawable.exclamation_triangle);
        icon.mutate();
        int disabledColor = SemanticColorUtils.getDefaultControlColorActive(context);
        icon.setColorFilter(disabledColor, PorterDuff.Mode.SRC_IN);
        return icon;
    }

    /** Returns the BrowserContextHandle we're showing the Site Settings UI for. */
    protected BrowserContextHandle getBrowserContextHandle() {
        return mBrowserContextHandle;
    }

    /**
     * Returns whether the permission is supported on this device. Some permissions like NFC are
     * backed up by hardware support and may not be available.
     */
    protected boolean supportedGlobally() {
        return true;
    }

    /** Returns the message to display when permission is not supported. */
    @Nullable
    protected String getMessageIfNotSupported(Context context) {
        return null;
    }

    /**
     * Returns whether the permission is enabled in Android, both globally and per-app. If the
     * permission does not have a per-app setting or a global setting, true is assumed for either
     * that is missing (or both).
     */
    boolean enabledInAndroid(Context context) {
        return enabledGlobally() && enabledForChrome(context);
    }

    /**
     * Returns whether a permission is enabled across Android. Not all permissions can be disabled
     * globally, so the default is true, but can be overwritten in sub-classes.
     */
    protected boolean enabledGlobally() {
        return true;
    }

    /** Returns whether a permission is enabled for Chrome specifically. */
    protected boolean enabledForChrome(Context context) {
        if (mAndroidPermission.isEmpty()) return true;
        return permissionOnInAndroid(mAndroidPermission, context);
    }

    /**
     * Returns whether to show the 'permission blocked' message. Majority of the time, that is
     * warranted when the permission is either blocked per app or globally. But there are exceptions
     * to this, so the sub-classes can overwrite.
     */
    boolean showPermissionBlockedMessage(Context context) {
        return !enabledForChrome(context) || !enabledGlobally();
    }

    /**
     * Returns the OS Intent to use to enable a per-app permission, or null if the permission is
     * already enabled. Android M and above provides two ways of doing this for some permissions,
     * most notably Location, one that is per-app and another that is global.
     */
    private Intent getIntentToEnableOsPerAppPermission(Context context) {
        if (enabledForChrome(context)) return null;
        return getAppInfoIntent(context);
    }

    /**
     * Returns the OS Intent to use to enable a permission globally, or null if there is no global
     * permission. Android M and above provides two ways of doing this for some permissions, most
     * notably Location, one that is per-app and another that is global.
     */
    protected Intent getIntentToEnableOsGlobalPermission(Context context) {
        return null;
    }

    /**
     * Returns the message to display when per-app permission is blocked.
     *
     * @param plural Whether it applies to one per-app permission or multiple.
     */
    protected String getMessageForEnablingOsPerAppPermission(
            Context context, boolean plural, String appName) {
        @ContentSettingsType.EnumType int type = this.getContentSettingsType();
        int permission_string = R.string.android_permission_off;
        if (type == ContentSettingsType.GEOLOCATION) {
            permission_string = R.string.android_location_permission_off;
        } else if (type == ContentSettingsType.MEDIASTREAM_MIC) {
            permission_string = R.string.android_microphone_permission_off;
        } else if (type == ContentSettingsType.MEDIASTREAM_CAMERA) {
            permission_string = R.string.android_camera_permission_off;
        } else if (type == ContentSettingsType.AR) {
            permission_string = R.string.android_ar_camera_permission_off;
        } else if (type == ContentSettingsType.HAND_TRACKING) {
            permission_string = R.string.android_hand_tracking_permission_off;
        } else if (type == ContentSettingsType.NOTIFICATIONS) {
            permission_string = R.string.android_notifications_permission_off;
        }
        return context.getResources()
                .getString(
                        plural ? R.string.android_permission_off_plural : permission_string,
                        appName);
    }

    /** Returns the message to display when per-app permission is blocked. */
    protected String getMessageForEnablingOsGlobalPermission(Context context) {
        return null;
    }

    /** Returns an Intent to show the App Info page for the current app. */
    private Intent getAppInfoIntent(Context context) {
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        intent.setData(
                new Uri.Builder().scheme("package").opaquePart(context.getPackageName()).build());
        return intent;
    }

    /**
     * Returns whether a per-app permission is enabled.
     *
     * @param permission The string of the permission to check.
     */
    private boolean permissionOnInAndroid(String permission, Context context) {
        return PackageManager.PERMISSION_GRANTED
                == ApiCompatibilityUtils.checkPermission(
                        context, permission, Process.myPid(), Process.myUid());
    }
}