blob: 1700d33b257092f12fc53ad6ac2e5a72845d2f4e [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.media;
import static android.content.Context.UI_MODE_SERVICE;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.UiModeManager;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.os.Build;
import android.text.TextUtils;
import android.view.Display;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.lang.reflect.Method;
import java.util.ArrayList;
/**
* A class for retrieving the physical display size from a device. This is necessary because
* Display.Mode.getPhysicalDisplaySize might not report the real physical display size
* because most ATV devices don't report all available modes correctly. In this case there is no
* way to find out whether a device is capable to display 4k content. This class offers a
* workaround for this problem.
* Note: This code is copied from androidx.core.view.DisplayCompat.
*/
public final class DisplayCompat {
private static final int DISPLAY_SIZE_4K_WIDTH = 3840;
private static final int DISPLAY_SIZE_4K_HEIGHT = 2160;
private DisplayCompat() {
// This class is non-instantiable.
}
/**
* Gets the supported modes of the given display where at least one of the modes is flagged
* as isNative(). Note that a native mode might not wrap any Display.Mode object in case
* the display returns no mode with the physical display size.
*
* @return an array of supported modes where at least one of the modes is native which
* contains the physical display size
*/
@NonNull
@SuppressLint("ArrayReturn")
public static ModeCompat[] getSupportedModes(
@NonNull Context context, @NonNull Display display) {
Point physicalDisplaySize = getPhysicalDisplaySize(context, display);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Display.Mode class and display.getSupportedModes() exist
Display.Mode[] supportedModes = display.getSupportedModes();
ArrayList<ModeCompat> supportedModesCompat = new ArrayList<>(supportedModes.length);
boolean nativeModeExists = false;
for (int i = 0; i < supportedModes.length; i++) {
if (physicalSizeEquals(supportedModes[i], physicalDisplaySize)) {
// Current mode has native resolution, flag it accordingly
supportedModesCompat.add(i, new ModeCompat(supportedModes[i], true));
nativeModeExists = true;
} else {
supportedModesCompat.add(i, new ModeCompat(supportedModes[i], false));
}
}
if (!nativeModeExists) {
// If no mode with physicalDisplaySize dimension exists, add the mode with the
// native display resolution
supportedModesCompat.add(new ModeCompat(physicalDisplaySize));
}
return supportedModesCompat.toArray(new ModeCompat[0]);
} else {
// previous to Android M Display.Mode and Display.getSupportedModes() did not exist,
// hence the only supported mode is the native display resolution
return new ModeCompat[] {new ModeCompat(physicalDisplaySize)};
}
}
/**
* Returns whether the app is running on a TV device
*
* @return true iff the app is running on a TV device
*/
public static boolean isTv(@NonNull Context context) {
// See https://developer.android.com/training/tv/start/hardware.html#runtime-check.
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(UI_MODE_SERVICE);
return uiModeManager != null
&& uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
}
/**
* Parses a string which represents the display-size which contains 'x' as a delimiter
* between two integers representing the display's width and height and returns the
* display size as a Point object.
*
* @param displaySize a string
* @return a Point object containing the size in x and y direction in pixels
* @throws NumberFormatException in case the integers cannot be parsed
*/
private static Point parseDisplaySize(@NonNull String displaySize)
throws NumberFormatException {
String[] displaySizeParts = displaySize.trim().split("x", -1);
if (displaySizeParts.length == 2) {
int width = Integer.parseInt(displaySizeParts[0]);
int height = Integer.parseInt(displaySizeParts[1]);
if (width > 0 && height > 0) {
return new Point(width, height);
}
}
throw new NumberFormatException();
}
/**
* Reads a system property and returns its string value.
*
* @param name the name of the system property
* @return the result string or null if an exception occurred
*/
@Nullable
private static String getSystemProperty(String name) {
try {
@SuppressLint("PrivateApi")
Class<?> systemProperties = Class.forName("android.os.SystemProperties");
Method getMethod = systemProperties.getMethod("get", String.class);
return (String) getMethod.invoke(systemProperties, name);
} catch (Exception e) {
return null;
}
}
/**
* Returns true if mode.getPhysicalWidth and mode.getPhysicalHeight are equal to the given size
*
* @param mode a Display.Mode object
* @param size a Point object representing the size in horizontal and vertical direction
*/
@RequiresApi(Build.VERSION_CODES.M)
@TargetApi(Build.VERSION_CODES.M)
private static boolean physicalSizeEquals(Display.Mode mode, Point size) {
return (mode.getPhysicalWidth() == size.x && mode.getPhysicalHeight() == size.y)
|| (mode.getPhysicalWidth() == size.y && mode.getPhysicalHeight() == size.x);
}
/**
* Helper function to determine the physical display size from the system properties only. On
* Android TVs it is common for the UI to be configured for a lower resolution than SurfaceViews
* can output. Before API 26 the Display object does not provide a way to identify this case,
* and up to and including API 28 many devices still do not correctly set their hardware
* composer output size.
*
* @return the physical display size, in pixels or null if the information is not available
*/
@Nullable
private static Point parsePhysicalDisplaySizeFromSystemProperties(
@NonNull String property, @NonNull Display display) {
if (display.getDisplayId() == Display.DEFAULT_DISPLAY) {
// Check the system property for display size. From API 28 treble may prevent the
// system from writing sys.display-size so we check vendor.display-size instead.
String displaySize = getSystemProperty(property);
// If we managed to read the display size, attempt to parse it.
if (!TextUtils.isEmpty(displaySize)) {
try {
return parseDisplaySize(displaySize);
} catch (NumberFormatException e) {
// Do nothing for now, null is returned in the end
}
}
}
// Unable to determine display size from system properties
return null;
}
/**
* Gets the physical size of the given display in pixels. The size is collected in the
* following order:
* 1) sys.display-size if API < 28 (P) and the system-property is set
* 2) vendor.display-size if API >= 28 (P) and the system-property is set
* 3) physical width and height from display.getMode() for API >= 23
* 4) display.getRealSize() for API >= 17
* 5) display.getSize()
*
* @return the physical display size, in pixels
*/
private static Point getPhysicalDisplaySize(
@NonNull Context context, @NonNull Display display) {
Point displaySize = Build.VERSION.SDK_INT < Build.VERSION_CODES.P
? parsePhysicalDisplaySizeFromSystemProperties("sys.display-size", display)
: parsePhysicalDisplaySizeFromSystemProperties("vendor.display-size", display);
if (displaySize != null) {
return displaySize;
} else if (isSonyBravia4kTv(context)) {
// Sony Android TVs advertise support for 4k output via a system feature.
return new Point(DISPLAY_SIZE_4K_WIDTH, DISPLAY_SIZE_4K_HEIGHT);
} else {
// Unable to retrieve the physical display size from system properties, get display
// size from the framework API. Note that this might not be the actual physical
// display size but the, possibly down-scaled, UI size.
displaySize = new Point();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Display.Mode mode = display.getMode();
displaySize.x = mode.getPhysicalWidth();
displaySize.y = mode.getPhysicalHeight();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
display.getRealSize(displaySize);
} else {
display.getSize(displaySize);
}
}
return displaySize;
}
/**
* Determines whether the connected display is a 4k capable Sony TV.
*
* @return true if the display is a Sony BRAVIA TV that supports 4k
*/
private static boolean isSonyBravia4kTv(@NonNull Context context) {
return isTv(context) && "Sony".equals(Build.MANUFACTURER)
&& Build.MODEL.startsWith("BRAVIA")
&& context.getPackageManager().hasSystemFeature("com.sony.dtv.hardware.panel.qfhd");
}
/**
* Compat class which provides an additional isNative() field. This field indicates whether a
* mode is native, which is important when searching for the highest possible native
* resolution of a display.
*/
public static final class ModeCompat {
private final Display.Mode mMode;
private final Point mPhysicalDisplaySize;
private final boolean mIsNative;
/**
* Package private constructor which creates a native ModeCompat object that does not
* wrap any Display.Mode object but only contains the given display size
*
* @param physicalDisplaySize a Point object representing the display size in pixels
* (Point.x horizontal and Point.y vertical size)
*/
ModeCompat(@NonNull Point physicalDisplaySize) {
if (physicalDisplaySize == null) {
throw new NullPointerException("physicalDisplaySize == null");
}
mIsNative = true;
mPhysicalDisplaySize = physicalDisplaySize;
mMode = null;
}
/**
* Package private constructor which creates a non-native ModeCompat and wraps the given
* Mode object
*
* @param mode a Display.Mode object
*/
@RequiresApi(Build.VERSION_CODES.M)
@TargetApi(Build.VERSION_CODES.M)
ModeCompat(@NonNull Display.Mode mode, boolean isNative) {
if (mode == null) {
throw new NullPointerException("Display.Mode == null, can't wrap a null reference");
}
mIsNative = isNative;
// This simplifies the getPhysicalWidth() / getPhysicalHeight functions below
mPhysicalDisplaySize = new Point(mode.getPhysicalWidth(), mode.getPhysicalHeight());
mMode = mode;
}
/**
* Returns the physical width of the given display when configured in this mode
*
* @return the physical screen width in pixels
*/
public int getPhysicalWidth() {
return mPhysicalDisplaySize.x;
}
/**
* Returns the physical height of the given display when configured in this mode
*
* @return the physical screen height in pixels
*/
public int getPhysicalHeight() {
return mPhysicalDisplaySize.y;
}
/**
* Function to get the wrapped object
*
* @return the wrapped Display.Mode object or null if there was no matching mode for the
* native resolution.
*/
@RequiresApi(Build.VERSION_CODES.M)
@Nullable
public Display.Mode toMode() {
return mMode;
}
/**
* This field indicates whether a mode is native, which is important when searching for
* the highest possible native resolution of a display.
*
* @return true if this is a native mode of the wrapped display
*/
public boolean isNative() {
return mIsNative;
}
}
}