chromium/ui/android/java/src/org/chromium/ui/display/DisplayUtil.java

// Copyright 2018 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.ui.display;

import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.WindowInsets;
import android.view.WindowManager;

import androidx.annotation.Nullable;

/**
 * Helper functions relevant to working with displays, but have no parallel in the native
 * DisplayAndroid class.
 */
public abstract class DisplayUtil {
    private static final float UI_SCALING_FACTOR_FOR_AUTOMOTIVE = 1.34f;
    private static @Nullable Float sUiScalingFactorForAutomotiveOverride;

    /** Change the UI scaling factor on automotive devices for testing. */
    public static void setUiScalingFactorForAutomotiveForTesting(float scalingFactor) {
        sUiScalingFactorForAutomotiveOverride = scalingFactor;
    }

    /** Reset the UI scaling factor on automotive devices to the default value. */
    public static void resetUiScalingFactorForAutomotiveForTesting() {
        sUiScalingFactorForAutomotiveOverride = null;
    }

    /**
     * Retrieves the UI scaling factor on automotive devices.
     * TODO: Remove this method and replace usages with getUiDensityForAutomotive.
     */
    @Deprecated
    public static float getUiScalingFactorForAutomotive() {
        return sUiScalingFactorForAutomotiveOverride;
    }

    public static int getUiDensityForAutomotive(Context context, int baseDensity) {
        TypedValue automotiveUiScaleFactor = new TypedValue();
        context.getResources()
                .getValue(
                        org.chromium.ui.R.dimen.automotive_ui_scale_factor,
                        automotiveUiScaleFactor,
                        true);
        float uiScalingFactor =
                sUiScalingFactorForAutomotiveOverride != null
                        ? sUiScalingFactorForAutomotiveOverride
                        : automotiveUiScaleFactor.getFloat();
        int rawScaledDensity = (int) (baseDensity * uiScalingFactor);
        // Round up to the nearest 20 to align with DisplayMetrics defined densities.
        return ((int) Math.ceil(rawScaledDensity / 20.0f)) * 20;
    }

    /** Returns the smaller of getDisplayWidth(), getDisplayHeight(). */
    public static int getSmallestWidth(DisplayAndroid display) {
        int width = display.getDisplayWidth();
        int height = display.getDisplayHeight();
        return width < height ? width : height;
    }

    /** Returns the given value converted from px to dp. */
    public static int pxToDp(DisplayAndroid display, int value) {
        // Adding .5 is what Android does when doing this conversion.
        return (int) (value / display.getDipScale() + 0.5f);
    }

    /** Returns the given value converted from dp to px. */
    public static int dpToPx(DisplayAndroid display, int value) {
        // Adding .5 is what Android does when doing this conversion.
        return (int) (value * display.getDipScale() + 0.5f);
    }

    /**
     * Scales up the UI for the {@link DisplayMetrics} by the scaling factor for automotive devices.
     *
     * @param displayMetrics The DisplayMetrics to scale up density for.
     * @return The DisplayMetrics that was scaled up.
     */
    public static DisplayMetrics scaleUpDisplayMetricsForAutomotive(
            Context context, DisplayMetrics displayMetrics) {
        int adjustedDensity = getUiDensityForAutomotive(context, displayMetrics.densityDpi);
        float scaling = (float) adjustedDensity / (float) displayMetrics.densityDpi;
        displayMetrics.density *= scaling;
        displayMetrics.densityDpi = adjustedDensity;
        displayMetrics.xdpi *= scaling;
        displayMetrics.ydpi *= scaling;
        return displayMetrics;
    }

    /**
     * Scales up the UI for the {@link DisplayMetrics} by the scaling factor for automotive devices.
     *
     * @param context The context used to retrieve the system {@link WindowManager}.
     * @param configuration The Configuration to scale up UI for.
     */
    public static void scaleUpConfigurationForAutomotive(
            Context context, Configuration configuration) {
        DisplayMetrics displayMetrics = new DisplayMetrics();
        WindowManager windowManager =
                (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        assert windowManager != null;
        windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);

        int adjustedDensity = getUiDensityForAutomotive(context, displayMetrics.densityDpi);
        float scaling = (float) adjustedDensity / (float) displayMetrics.densityDpi;

        int screenWidthDp = displayMetrics.widthPixels;
        int screenHeightDp = displayMetrics.heightPixels;

        // Configuration.screenWidthDp and Configuration.screenHeightDp are not supposed to take
        // into account system bars. Since we are scaling up the UI in automotive during a time when
        // we cannot access the default Configuration (CompatActivity#attachBaseContext), we need
        // to manually subtract the system bar insets ourselves.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            Insets systemBarInsets =
                    windowManager
                            .getCurrentWindowMetrics()
                            .getWindowInsets()
                            .getInsets(WindowInsets.Type.systemBars());
            screenHeightDp = screenHeightDp - systemBarInsets.top - systemBarInsets.bottom;
            screenWidthDp = screenWidthDp - systemBarInsets.left - systemBarInsets.right;
        }

        configuration.densityDpi = adjustedDensity;
        configuration.screenWidthDp =
                Math.round(screenWidthDp / (displayMetrics.density * scaling));
        configuration.screenHeightDp =
                Math.round(screenHeightDp / (displayMetrics.density * scaling));
        configuration.smallestScreenWidthDp =
                Math.min(configuration.screenWidthDp, configuration.screenHeightDp);
    }

    /**
     * Get current smallest screen width in dp. This method uses {@link WindowManager} on
     * Android R and above; otherwise, {@link DisplayUtil#getSmallestWidth(DisplayAndroid)}.
     *
     * @param context {@link Context} used to get system service and target display.
     * @return Smallest screen width in dp.
     */
    public static int getCurrentSmallestScreenWidth(Context context) {
        DisplayAndroid display = DisplayAndroid.getNonMultiDisplay(context);
        // Android T does not receive updated width upon foldable unfold from window context.
        // Continue to rely on context on this case.
        Context windowManagerContext =
                (VERSION.SDK_INT >= VERSION_CODES.R && VERSION.SDK_INT < VERSION_CODES.TIRAMISU)
                        ? (display.getWindowContext() != null
                                ? display.getWindowContext()
                                : context)
                        : context;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            // Context#getSystemService(Context.WINDOW_SERVICE) is preferred over
            // Activity#getWindowManager, because during #attachBaseContext, #getWindowManager
            // is not ready yet and always returns null. See crbug.com/1252150.
            WindowManager manager =
                    (WindowManager) windowManagerContext.getSystemService(Context.WINDOW_SERVICE);
            assert manager != null;
            Rect bounds = manager.getMaximumWindowMetrics().getBounds();
            return DisplayUtil.pxToDp(
                    display, Math.min(bounds.right - bounds.left, bounds.bottom - bounds.top));
        }
        return DisplayUtil.pxToDp(display, DisplayUtil.getSmallestWidth(display));
    }
}