// 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.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.content_settings.ContentSettingsType;
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.COOKIES, 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.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 COOKIES = 8;
        int DEVICE_LOCATION = 9;
        int IDLE_DETECTION = 10;
        int JAVASCRIPT = 11;
        int MICROPHONE = 12;
        int NFC = 13;
        int NOTIFICATIONS = 14;
        int POPUPS = 15;
        int PROTECTED_MEDIA = 16;
        int SENSORS = 17;
        int SOUND = 18;
        int USB = 19;
        int BLUETOOTH = 20;
        int VIRTUAL_REALITY = 21;
        int USE_STORAGE = 22;
        int AUTO_DARK_WEB_CONTENT = 23;
        int REQUEST_DESKTOP_SITE = 24;
        int FEDERATED_IDENTITY_API = 25;
        int THIRD_PARTY_COOKIES = 26;
        int SITE_DATA = 27;
        int ANTI_ABUSE = 28;
        int ZOOM = 29;
        /**
         * Number of handled categories used for calculating array sizes.
         */
        int NUM_ENTRIES = 30;
    }

    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 {
            permission = "";
        }
        return new SiteSettingsCategory(browserContextHandle, type, permission);
    }

    public static SiteSettingsCategory createFromContentSettingsType(
            BrowserContextHandle browserContextHandle,
            @ContentSettingsType 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 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.COOKIES:
            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.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.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:
                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 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.COOKIES:
                return "cookies";
            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.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.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.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 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 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.COOKIES || 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.COOKIES || mCategory == Type.DEVICE_LOCATION
                || mCategory == Type.CAMERA || mCategory == Type.MICROPHONE) {
            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
        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.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());
    }
}
