chromium/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/minimizedcustomtab/MinimizedFeatureUtils.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.chrome.browser.customtabs.features.minimizedcustomtab;

import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.text.TextUtils;

import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.IntentUtils;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.SysUtils;
import org.chromium.base.cached_flags.IntCachedFieldTrialParameter;
import org.chromium.base.cached_flags.StringCachedFieldTrialParameter;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.customtabs.CustomTabFeatureOverridesManager;
import org.chromium.chrome.browser.flags.ChromeFeatureList;

import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

/** Utility methods for the Minimized Custom Tab feature. */
public class MinimizedFeatureUtils {
    public static final IntCachedFieldTrialParameter ICON_VARIANT =
            ChromeFeatureList.newIntCachedFieldTrialParameter(
                    ChromeFeatureList.CCT_MINIMIZED, "icon_variant", 0);

    // Devices from this OEM--and potentially others--sometimes crash when we call
    // `Activity#enterPictureInPictureMode` on Android R. So, we disable the feature on those
    // devices. See: https://crbug.com/1519164.
    public static final StringCachedFieldTrialParameter MANUFACTURER_EXCLUDE_LIST =
            ChromeFeatureList.newStringCachedFieldTrialParameter(
                    ChromeFeatureList.CCT_MINIMIZED_ENABLED_BY_DEFAULT,
                    "manufacturer_exclude_list",
                    "xiaomi");

    private static Set<String> sManufacturerExcludeList;

    // These values are persisted to logs. Entries should not be renumbered and
    // numeric values should never be reused.
    @IntDef({
        MinimizedFeatureAvailability.AVAILABLE,
        MinimizedFeatureAvailability.UNAVAILABLE_LOW_END_DEVICE,
        MinimizedFeatureAvailability.UNAVAILABLE_SYSTEM_FEATURE,
        MinimizedFeatureAvailability.UNAVAILABLE_PIP_PERMISSION,
        MinimizedFeatureAvailability.UNAVAILABLE_EXCLUDED_MANUFACTURER,
        MinimizedFeatureAvailability.NUM_ENTRIES
    })
    @VisibleForTesting
    @interface MinimizedFeatureAvailability {
        int AVAILABLE = 0;
        // (Obsolete) int UNAVAILABLE_API_LEVEL = 1;
        int UNAVAILABLE_LOW_END_DEVICE = 2;
        int UNAVAILABLE_SYSTEM_FEATURE = 3;
        int UNAVAILABLE_PIP_PERMISSION = 4;
        int UNAVAILABLE_EXCLUDED_MANUFACTURER = 5;

        int NUM_ENTRIES = 6;
    }

    private static Boolean sIsDeviceEligibleForMinimizedCustomTab;
    private static boolean sIsDeviceEligibleForMinimizedCustomTabForTesting;

    /**
     * Computes the availability of the Minimized Custom Tab feature based on multiple signals and
     * emits histograms accordingly.
     *
     * @param context The {@link Context}.
     * @param featureOverridesManager The {@link CustomTabFeatureOverridesManager} to check
     *     overridden features.
     * @return Whether the Minimized Custom Tab feature is available.
     */
    public static boolean isMinimizedCustomTabAvailable(
            Context context, @Nullable CustomTabFeatureOverridesManager featureOverridesManager) {
        if (!isDeviceEligibleForMinimizedCustomTab(context)) return false;
        if (!ChromeFeatureList.sCctIntentFeatureOverrides.isEnabled()) {
            return ChromeFeatureList.sCctMinimized.isEnabled();
        }
        if (featureOverridesManager == null) return ChromeFeatureList.sCctMinimized.isEnabled();

        Boolean override =
                featureOverridesManager.isFeatureEnabled(ChromeFeatureList.CCT_MINIMIZED);
        if (override != null) return override;
        return ChromeFeatureList.sCctMinimized.isEnabled();
    }

    /**
     * Computes the eligibility of the Minimized Custom Tab feature based on multiple signals and
     * emits histograms accordingly.
     *
     * @param context The {@link Context}.
     * @return Whether the device is eligible for Minimized Custom Tab feature.
     */
    private static boolean isDeviceEligibleForMinimizedCustomTab(Context context) {
        if (sIsDeviceEligibleForMinimizedCustomTabForTesting) return true;

        if (sIsDeviceEligibleForMinimizedCustomTab != null) {
            return sIsDeviceEligibleForMinimizedCustomTab;
        }

        @MinimizedFeatureAvailability int availability = MinimizedFeatureAvailability.AVAILABLE;
        sIsDeviceEligibleForMinimizedCustomTab = true;
        if (SysUtils.isLowEndDevice()) {
            availability = MinimizedFeatureAvailability.UNAVAILABLE_LOW_END_DEVICE;
            sIsDeviceEligibleForMinimizedCustomTab = false;
        } else if (!context.getPackageManager()
                .hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
            availability = MinimizedFeatureAvailability.UNAVAILABLE_SYSTEM_FEATURE;
            sIsDeviceEligibleForMinimizedCustomTab = false;
        } else if (!isPipAllowed(context)) {
            availability = MinimizedFeatureAvailability.UNAVAILABLE_PIP_PERMISSION;
            sIsDeviceEligibleForMinimizedCustomTab = false;
        } else if (isDeviceExcluded()) {
            availability = MinimizedFeatureAvailability.UNAVAILABLE_EXCLUDED_MANUFACTURER;
            sIsDeviceEligibleForMinimizedCustomTab = false;
        }

        RecordHistogram.recordEnumeratedHistogram(
                "CustomTabs.MinimizedFeatureAvailability",
                availability,
                MinimizedFeatureAvailability.NUM_ENTRIES);
        ResettersForTesting.register(() -> sIsDeviceEligibleForMinimizedCustomTab = null);
        return sIsDeviceEligibleForMinimizedCustomTab;
    }

    private static boolean isPipAllowed(Context context) {
        AppOpsManager appOpsManager =
                (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
        int result =
                appOpsManager.checkOpNoThrow(
                        AppOpsManager.OPSTR_PICTURE_IN_PICTURE,
                        context.getApplicationInfo().uid,
                        context.getPackageName());
        return result == AppOpsManager.MODE_ALLOWED;
    }

    private static boolean isDeviceExcluded() {
        sManufacturerExcludeList = new HashSet<>();
        String listStr = MANUFACTURER_EXCLUDE_LIST.getValue();
        if (!TextUtils.isEmpty(listStr)) {
            Collections.addAll(sManufacturerExcludeList, listStr.split(","));
        }
        return VERSION.SDK_INT == VERSION_CODES.R
                && (sManufacturerExcludeList.contains(Build.MANUFACTURER.toLowerCase(Locale.US))
                        || sManufacturerExcludeList.contains(Build.BRAND.toLowerCase(Locale.US)));
    }

    public static void setDeviceEligibleForMinimizedCustomTabForTesting(boolean eligibility) {
        sIsDeviceEligibleForMinimizedCustomTabForTesting = eligibility;
        ResettersForTesting.register(
                () -> sIsDeviceEligibleForMinimizedCustomTabForTesting = false);
    }

    public static @DrawableRes int getMinimizeIcon() {
        return ICON_VARIANT.getValue() == 1 ? R.drawable.ic_pip_24dp : R.drawable.ic_minimize;
    }

    /**
     * Returns whether Minimized Custom Tabs should be enabled based on the intent data provider.
     *
     * @param intentDataProvider The {@link BrowserServicesIntentDataProvider}.
     * @return Whether Minimized Custom Tabs should be enabled.
     */
    public static boolean shouldEnableMinimizedCustomTabs(
            BrowserServicesIntentDataProvider intentDataProvider) {
        boolean isWebApp =
                intentDataProvider.isWebappOrWebApkActivity()
                        || intentDataProvider.isTrustedWebActivity();
        if (isWebApp) return false;

        boolean isFedCmIntent =
                intentDataProvider.isTrustedIntent()
                        && IntentUtils.safeGetIntExtra(
                                        intentDataProvider.getIntent(),
                                        IntentHandler.EXTRA_FEDCM_ID,
                                        -1)
                                != -1;
        if (isFedCmIntent) return false;

        boolean isAuthTab = intentDataProvider.isAuthTab();
        if (isAuthTab) return false;

        return true;
    }
}