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

// Copyright 2016 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.ComponentCallbacks;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.WindowManager;

import androidx.annotation.RequiresApi;

import org.chromium.base.BuildInfo;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.StrictModeContext;
import org.chromium.base.ThreadUtils;

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

/** A DisplayAndroid implementation tied to a physical Display. */
/* package */ class PhysicalDisplayAndroid extends DisplayAndroid {
    private static final String TAG = "DisplayAndroid";

    // The behavior of observing window configuration changes using ComponentCallbacks is new in S.
    private static final boolean USE_CONFIGURATION = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S;

    // When this object exists, a positive value means that the forced DIP scale is set and
    // the zero means it is not. The non existing object (i.e. null reference) means that
    // the existence and value of the forced DIP scale has not yet been determined.
    private static Float sForcedDIPScale;

    private static Float getHdrSdrRatio(Display display) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return null;
        return display.getHdrSdrRatio();
    }

    private static boolean isHdr(Display display) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return false;
        return display.isHdr() && display.isHdrSdrRatioAvailable();
    }

    private static boolean hasForcedDIPScale() {
        if (sForcedDIPScale == null) {
            String forcedScaleAsString =
                    CommandLine.getInstance()
                            .getSwitchValue(DisplaySwitches.FORCE_DEVICE_SCALE_FACTOR);
            if (forcedScaleAsString == null) {
                sForcedDIPScale = Float.valueOf(0.0f);
            } else {
                boolean isInvalid = false;
                try {
                    sForcedDIPScale = Float.valueOf(forcedScaleAsString);
                    // Negative values are discarded.
                    if (sForcedDIPScale.floatValue() <= 0.0f) isInvalid = true;
                } catch (NumberFormatException e) {
                    // Strings that do not represent numbers are discarded.
                    isInvalid = true;
                }

                if (isInvalid) {
                    Log.w(TAG, "Ignoring invalid forced DIP scale '" + forcedScaleAsString + "'");
                    sForcedDIPScale = Float.valueOf(0.0f);
                }
            }
        }
        return sForcedDIPScale.floatValue() > 0;
    }

    /**
     * This method returns the bitsPerPixel without the alpha channel, as this is the value expected
     * by Chrome and the CSS media queries.
     */
    @SuppressWarnings("deprecation")
    private static int bitsPerPixel(int pixelFormatId) {
        // For JB-MR1 and above, this is the only value, so we can hard-code the result.
        if (pixelFormatId == PixelFormat.RGBA_8888) return 24;

        PixelFormat pixelFormat = new PixelFormat();
        PixelFormat.getPixelFormatInfo(pixelFormatId, pixelFormat);
        if (!PixelFormat.formatHasAlpha(pixelFormatId)) return pixelFormat.bitsPerPixel;

        switch (pixelFormatId) {
            case PixelFormat.RGBA_1010102:
                return 30;

            case PixelFormat.RGBA_4444:
                return 12;

            case PixelFormat.RGBA_5551:
                return 15;

            case PixelFormat.RGBA_8888:
                assert false;
                // fall through RGBX_8888 does not have an alpha channel even if it has 8 reserved
                // bits at the end.
            case PixelFormat.RGBX_8888:
            case PixelFormat.RGB_888:
            default:
                return 24;
        }
    }

    @SuppressWarnings("deprecation")
    private static int bitsPerComponent(int pixelFormatId) {
        switch (pixelFormatId) {
            case PixelFormat.RGBA_4444:
                return 4;

            case PixelFormat.RGBA_5551:
                return 5;

            case PixelFormat.RGBA_8888:
            case PixelFormat.RGBX_8888:
            case PixelFormat.RGB_888:
                return 8;

            case PixelFormat.RGB_332:
                return 2;

            case PixelFormat.RGB_565:
                return 5;

                // Non-RGB formats.
            case PixelFormat.A_8:
            case PixelFormat.LA_88:
            case PixelFormat.L_8:
                return 0;

                // Unknown format. Use 8 as a sensible default.
            default:
                return 8;
        }
    }

    private final Context mWindowContext;
    private final ComponentCallbacks mComponentCallbacks;
    private final Display mDisplay;
    private Consumer<Display> mHdrSdrRatioCallback;

    /* package */ PhysicalDisplayAndroid(Display display, boolean disableHdrSdkRatioCallback) {
        super(display.getDisplayId());
        if (USE_CONFIGURATION) {
            Context appContext = ContextUtils.getApplicationContext();
            // `createWindowContext` on some devices writes to disk. See crbug.com/1408587.
            try (StrictModeContext ignored = StrictModeContext.allowAllThreadPolicies()) {
                mWindowContext =
                        appContext.createWindowContext(
                                display, WindowManager.LayoutParams.TYPE_APPLICATION, null);
            }
            assert display.getDisplayId() == mWindowContext.getDisplay().getDisplayId();
            mComponentCallbacks =
                    new ComponentCallbacks() {
                        @Override
                        public void onLowMemory() {}

                        @Override
                        public void onConfigurationChanged(Configuration newConfig) {
                            updateFromConfiguration();
                        }
                    };
            mWindowContext.registerComponentCallbacks(mComponentCallbacks);
            mDisplay = mWindowContext.getDisplay();
            updateFromConfiguration();
        } else {
            mWindowContext = null;
            mComponentCallbacks = null;
            mDisplay = display;
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
                && mDisplay.isHdrSdrRatioAvailable()
                && !disableHdrSdkRatioCallback) {
            mHdrSdrRatioCallback = this::hdrSdrRatioChanged;
            mDisplay.registerHdrSdrRatioChangedListener(
                    (Runnable runnable) -> {
                        ThreadUtils.getUiThreadHandler().post(runnable);
                    },
                    mHdrSdrRatioCallback);
        } else {
            mHdrSdrRatioCallback = null;
        }
    }

    @Override
    public Context getWindowContext() {
        return mWindowContext;
    }

    @RequiresApi(api = VERSION_CODES.R)
    private void updateFromConfiguration() {
        Point size = new Point();
        WindowManager windowManager = mWindowContext.getSystemService(WindowManager.class);
        Rect rect = windowManager.getMaximumWindowMetrics().getBounds();
        size.set(rect.width(), rect.height());
        DisplayMetrics displayMetrics = mWindowContext.getResources().getDisplayMetrics();

        if (BuildInfo.getInstance().isAutomotive
                && CommandLine.getInstance()
                        .hasSwitch(DisplaySwitches.AUTOMOTIVE_WEB_UI_SCALE_UP_ENABLED)) {
            mDisplay.getRealMetrics(displayMetrics);
            DisplayUtil.scaleUpDisplayMetricsForAutomotive(mWindowContext, displayMetrics);
        }
        updateCommon(
                size,
                displayMetrics.density,
                displayMetrics.xdpi,
                displayMetrics.ydpi,
                mWindowContext.getDisplay());
    }

    /* package */ void onDisplayRemoved() {
        if (USE_CONFIGURATION) {
            mWindowContext.unregisterComponentCallbacks(mComponentCallbacks);
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
                && mHdrSdrRatioCallback != null) {
            mDisplay.unregisterHdrSdrRatioChangedListener(mHdrSdrRatioCallback);
            mHdrSdrRatioCallback = null;
        }
    }

    @SuppressWarnings("deprecation")
    /* package */ void updateFromDisplay(Display display) {
        if (USE_CONFIGURATION) {
            assert display.getDisplayId() == mDisplay.getDisplayId();
            // Needed to update non-configuration info such as refresh rate.
            updateFromConfiguration();
            return;
        }
        Point size = new Point();
        DisplayMetrics displayMetrics = new DisplayMetrics();
        display.getRealSize(size);
        display.getRealMetrics(displayMetrics);

        if (BuildInfo.getInstance().isAutomotive
                && CommandLine.getInstance()
                        .hasSwitch(DisplaySwitches.AUTOMOTIVE_WEB_UI_SCALE_UP_ENABLED)) {
            DisplayUtil.scaleUpDisplayMetricsForAutomotive(
                    ContextUtils.getApplicationContext(), displayMetrics);
        }
        updateCommon(
                size, displayMetrics.density, displayMetrics.xdpi, displayMetrics.ydpi, display);
    }

    private void hdrSdrRatioChanged(Display display) {
        assert display.getDisplayId() == mDisplay.getDisplayId();
        super.update(
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                isHdr(mDisplay),
                getHdrSdrRatio(mDisplay));
    }

    private void updateCommon(Point size, float density, float xdpi, float ydpi, Display display) {
        if (hasForcedDIPScale()) density = sForcedDIPScale.floatValue();
        boolean isWideColorGamut = false;
        // Although this API was added in Android O, it was buggy.
        // Restrict to Android Q, where it was fixed.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            isWideColorGamut = display.isWideColorGamut();
        }

        int pixelFormatId = PixelFormat.RGBA_8888;

        // Note: getMode() and getSupportedModes() can return null in some situations - see
        // crbug.com/1401322.
        Display.Mode currentMode = display.getMode();
        Display.Mode[] modes = display.getSupportedModes();
        List<Display.Mode> supportedModes = null;
        if (modes != null && modes.length > 0) {
            supportedModes = Arrays.asList(modes);
        }

        super.update(
                size,
                density,
                xdpi,
                ydpi,
                bitsPerPixel(pixelFormatId),
                bitsPerComponent(pixelFormatId),
                display.getRotation(),
                isWideColorGamut,
                null,
                display.getRefreshRate(),
                currentMode,
                supportedModes,
                isHdr(display),
                getHdrSdrRatio(display));
    }
}