chromium/chrome/browser/ui/android/edge_to_edge/java/src/org/chromium/chrome/browser/ui/edge_to_edge/EdgeToEdgeUtils.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.ui.edge_to_edge;

import android.app.Activity;
import android.os.Build.VERSION_CODES;
import android.view.Window;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.core.view.WindowInsetsCompat;

import org.chromium.base.BuildInfo;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.blink.mojom.ViewportFit;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.components.browser_ui.display_cutout.DisplayCutoutController;
import org.chromium.components.browser_ui.display_cutout.DisplayCutoutController.SafeAreaInsetsTracker;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.ui.base.DeviceFormFactor;

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

/**
 * A util helper class to know if e2e is on and eligible for current session and to record metrics
 * when necessary.
 */
public class EdgeToEdgeUtils {

    private static final String ELIGIBLE_HISTOGRAM = "Android.EdgeToEdge.Eligible";
    private static final String INELIGIBLE_REASON_HISTOGRAM =
            "Android.EdgeToEdge.IneligibilityReason";

    /** The reason of why the current session is not eligible for edge to edge. */
    @IntDef({
        IneligibilityReason.OS_VERSION,
        IneligibilityReason.FORM_FACTOR,
        IneligibilityReason.NAVIGATION_MODE,
        IneligibilityReason.DEVICE_TYPE,
        IneligibilityReason.NUM_TYPES
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface IneligibilityReason {
        int OS_VERSION = 0;
        int FORM_FACTOR = 1;
        int NAVIGATION_MODE = 2;
        int DEVICE_TYPE = 3;
        int NUM_TYPES = 4;
    }

    /**
     * Whether the draw edge to edge infrastructure is on. When this is enabled, Chrome will start
     * drawing edge to edge on start up.
     */
    public static boolean isEnabled() {
        return isLegacyWebsiteOptInEnabled()
                || isEdgeToEdgeBottomChinEnabled()
                || isFullWebEdgeToEdgeOptInEnabled();
    }

    /**
     * Whether drawing website opt-in is enabled.
     *
     * <p>When enabled, Chrome will add bottom padding to the root view if the current tab / UI is
     * not a tab with `viewport-fit=cover`. Additionally, bottom attached UI will be padded to avoid
     * drawing into the bottom navigation bar region.
     *
     * @deprecated This method will be removed. External references should use {@link #isEnabled()}.
     */
    public static boolean isLegacyWebsiteOptInEnabled() {
        return ChromeFeatureList.sDrawEdgeToEdge.isEnabled();
    }

    /**
     * Whether the edge-to-edge bottom chin is enabled.
     *
     * <p>When enabled, Chrome will replace the OS navigation bar with a thin "Chin" layer in the
     * browser controls and can be scrolled off the screen on web pages.
     */
    public static boolean isEdgeToEdgeBottomChinEnabled() {
        return ChromeFeatureList.sEdgeToEdgeBottomChin.isEnabled();
    }

    /**
     * Whether drawing the website that has `viewport-fit=cover` fully edge to edge, removing the
     * bottom chin.
     */
    public static boolean isFullWebEdgeToEdgeOptInEnabled() {
        return ChromeFeatureList.sDrawWebEdgeToEdge.isEnabled();
    }

    /**
     * Record if the current activity is eligible for edge to edge. If not, also record the reason
     * why it is ineligible.
     *
     * @param activity The current active activity.
     * @return Whether the activity is eligible for edge to edge based on device configuration.
     */
    public static boolean recordEligibility(@NonNull Activity activity) {
        boolean eligible = true;

        if (hasTappableBottomBar(activity.getWindow())) {
            eligible = false;
            RecordHistogram.recordEnumeratedHistogram(
                    INELIGIBLE_REASON_HISTOGRAM,
                    IneligibilityReason.NAVIGATION_MODE,
                    IneligibilityReason.NUM_TYPES);
        }

        if (DeviceFormFactor.isNonMultiDisplayContextOnTablet(activity)) {
            eligible = false;
            RecordHistogram.recordEnumeratedHistogram(
                    INELIGIBLE_REASON_HISTOGRAM,
                    IneligibilityReason.FORM_FACTOR,
                    IneligibilityReason.NUM_TYPES);
        }

        if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.R) {
            eligible = false;
            RecordHistogram.recordEnumeratedHistogram(
                    INELIGIBLE_REASON_HISTOGRAM,
                    IneligibilityReason.OS_VERSION,
                    IneligibilityReason.NUM_TYPES);
        }

        if (BuildInfo.getInstance().isAutomotive) {
            eligible = false;
            RecordHistogram.recordEnumeratedHistogram(
                    INELIGIBLE_REASON_HISTOGRAM,
                    IneligibilityReason.DEVICE_TYPE,
                    IneligibilityReason.NUM_TYPES);
        }
        RecordHistogram.recordBooleanHistogram(ELIGIBLE_HISTOGRAM, eligible);

        return eligible;
    }

    /**
     * @param isPageOptedIntoEdgeToEdge Whether the page has opted into edge-to-edge.
     * @param layoutType The active layout type being shown.
     * @param bottomInset The bottom inset representing the height of the bottom OS navbar.
     * @return whether we should draw ToEdge based only on the given Tab and the viewport-fit value
     *     from the tracking data of the Display Cutout Controller.
     */
    static boolean shouldDrawToEdge(
            boolean isPageOptedIntoEdgeToEdge, @LayoutType int layoutType, int bottomInset) {
        return (isLegacyWebsiteOptInEnabled() && isPageOptedIntoEdgeToEdge)
                || (isEdgeToEdgeBottomChinEnabled()
                        && isBottomChinAllowed(layoutType, bottomInset));
    }

    /**
     * @param layoutType The active layout type being shown.
     * @param bottomInset The bottom inset representing the height of the bottom OS navbar.
     * @return Whether the bottom chin is allowed to be shown.
     */
    static boolean isBottomChinAllowed(@LayoutType int layoutType, int bottomInset) {
        boolean supportedLayoutType =
                layoutType == LayoutType.BROWSING
                        || layoutType == LayoutType.TOOLBAR_SWIPE
                        || layoutType == LayoutType.SIMPLE_ANIMATION;

        // Check that the bottom inset is greater than zero, otherwise there is no space to show the
        // bottom chin. A zero inset indicates a lack of "dismissable" bottom bar (e.g. fullscreen
        // mode, 3-button nav).
        boolean nonZeroEdgeToEdgeBottomInset = bottomInset > 0;

        return supportedLayoutType && nonZeroEdgeToEdgeBottomInset;
    }

    /**
     * @return whether the page is opted into edge-to-edge based on the given Tab
     */
    public static boolean isPageOptedIntoEdgeToEdge(Tab tab) {
        if (tab == null || tab.isNativePage()) {
            return ChromeFeatureList.sDrawNativeEdgeToEdge.isEnabled();
        }
        if (ChromeFeatureList.sDrawWebEdgeToEdge.isEnabled()) {
            return true;
        }
        // TODO (crbug.com/353724310) Refactor flag check to the E2E web opt-in flag
        return isLegacyWebsiteOptInEnabled() && getWasViewportFitCover(tab);
    }

    /**
     * @return whether the page is opted into edge-to-edge based on the given Tab and the given new
     *     viewport-fit value.
     */
    static boolean isPageOptedIntoEdgeToEdge(
            Tab tab, @WebContentsObserver.ViewportFitType int value) {
        if (tab == null || tab.isNativePage()) {
            return ChromeFeatureList.sDrawNativeEdgeToEdge.isEnabled();
        }
        if (!isLegacyWebsiteOptInEnabled()) {
            return false;
        }
        return value == ViewportFit.COVER || value == ViewportFit.COVER_FORCED_BY_USER_AGENT;
    }

    /**
     * @return whether the given window's insets indicate a tappable bottom bar.
     */
    static boolean hasTappableBottomBar(Window window) {
        var rootInsets = window.getDecorView().getRootWindowInsets();
        assert rootInsets != null;
        return WindowInsetsCompat.toWindowInsetsCompat(rootInsets)
                        .getInsets(WindowInsetsCompat.Type.tappableElement())
                        .bottom
                != 0;
    }

    /**
     * Returns whether the given Tab has a web page that was already rendered with
     * viewport-fit=cover.
     */
    static boolean getWasViewportFitCover(@NonNull Tab tab) {
        assert tab != null;
        SafeAreaInsetsTracker safeAreaInsetsTracker =
                DisplayCutoutController.getSafeAreaInsetsTracker(tab);
        return safeAreaInsetsTracker == null ? false : safeAreaInsetsTracker.isViewportFitCover();
    }
}