1 // Copyright 2020 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.media; 6 7 import static android.content.Context.UI_MODE_SERVICE; 8 9 import android.annotation.SuppressLint; 10 import android.annotation.TargetApi; 11 import android.app.UiModeManager; 12 import android.content.Context; 13 import android.content.res.Configuration; 14 import android.graphics.Point; 15 import android.os.Build; 16 import android.text.TextUtils; 17 import android.view.Display; 18 19 import androidx.annotation.NonNull; 20 import androidx.annotation.Nullable; 21 import androidx.annotation.RequiresApi; 22 23 import java.lang.reflect.Method; 24 import java.util.ArrayList; 25 26 /** 27 * A class for retrieving the physical display size from a device. This is necessary because 28 * Display.Mode.getPhysicalDisplaySize might not report the real physical display size 29 * because most ATV devices don't report all available modes correctly. In this case there is no 30 * way to find out whether a device is capable to display 4k content. This class offers a 31 * workaround for this problem. 32 * Note: This code is copied from androidx.core.view.DisplayCompat. 33 */ 34 public final class DisplayCompat { 35 private static final int DISPLAY_SIZE_4K_WIDTH = 3840; 36 private static final int DISPLAY_SIZE_4K_HEIGHT = 2160; 37 DisplayCompat()38 private DisplayCompat() { 39 // This class is non-instantiable. 40 } 41 42 /** 43 * Gets the supported modes of the given display where at least one of the modes is flagged 44 * as isNative(). Note that a native mode might not wrap any Display.Mode object in case 45 * the display returns no mode with the physical display size. 46 * 47 * @return an array of supported modes where at least one of the modes is native which 48 * contains the physical display size 49 */ 50 @NonNull 51 @SuppressLint("ArrayReturn") getSupportedModes( @onNull Context context, @NonNull Display display)52 public static ModeCompat[] getSupportedModes( 53 @NonNull Context context, @NonNull Display display) { 54 Point physicalDisplaySize = getPhysicalDisplaySize(context, display); 55 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 56 // Display.Mode class and display.getSupportedModes() exist 57 Display.Mode[] supportedModes = display.getSupportedModes(); 58 ArrayList<ModeCompat> supportedModesCompat = new ArrayList<>(supportedModes.length); 59 boolean nativeModeExists = false; 60 for (int i = 0; i < supportedModes.length; i++) { 61 if (physicalSizeEquals(supportedModes[i], physicalDisplaySize)) { 62 // Current mode has native resolution, flag it accordingly 63 supportedModesCompat.add(i, new ModeCompat(supportedModes[i], true)); 64 nativeModeExists = true; 65 } else { 66 supportedModesCompat.add(i, new ModeCompat(supportedModes[i], false)); 67 } 68 } 69 if (!nativeModeExists) { 70 // If no mode with physicalDisplaySize dimension exists, add the mode with the 71 // native display resolution 72 supportedModesCompat.add(new ModeCompat(physicalDisplaySize)); 73 } 74 return supportedModesCompat.toArray(new ModeCompat[0]); 75 } else { 76 // previous to Android M Display.Mode and Display.getSupportedModes() did not exist, 77 // hence the only supported mode is the native display resolution 78 return new ModeCompat[] {new ModeCompat(physicalDisplaySize)}; 79 } 80 } 81 82 /** 83 * Returns whether the app is running on a TV device 84 * 85 * @return true iff the app is running on a TV device 86 */ isTv(@onNull Context context)87 public static boolean isTv(@NonNull Context context) { 88 // See https://developer.android.com/training/tv/start/hardware.html#runtime-check. 89 UiModeManager uiModeManager = (UiModeManager) context.getSystemService(UI_MODE_SERVICE); 90 return uiModeManager != null 91 && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; 92 } 93 94 /** 95 * Parses a string which represents the display-size which contains 'x' as a delimiter 96 * between two integers representing the display's width and height and returns the 97 * display size as a Point object. 98 * 99 * @param displaySize a string 100 * @return a Point object containing the size in x and y direction in pixels 101 * @throws NumberFormatException in case the integers cannot be parsed 102 */ parseDisplaySize(@onNull String displaySize)103 private static Point parseDisplaySize(@NonNull String displaySize) 104 throws NumberFormatException { 105 String[] displaySizeParts = displaySize.trim().split("x", -1); 106 if (displaySizeParts.length == 2) { 107 int width = Integer.parseInt(displaySizeParts[0]); 108 int height = Integer.parseInt(displaySizeParts[1]); 109 if (width > 0 && height > 0) { 110 return new Point(width, height); 111 } 112 } 113 throw new NumberFormatException(); 114 } 115 116 /** 117 * Reads a system property and returns its string value. 118 * 119 * @param name the name of the system property 120 * @return the result string or null if an exception occurred 121 */ 122 @Nullable getSystemProperty(String name)123 private static String getSystemProperty(String name) { 124 try { 125 @SuppressLint("PrivateApi") 126 Class<?> systemProperties = Class.forName("android.os.SystemProperties"); 127 Method getMethod = systemProperties.getMethod("get", String.class); 128 return (String) getMethod.invoke(systemProperties, name); 129 } catch (Exception e) { 130 return null; 131 } 132 } 133 134 /** 135 * Returns true if mode.getPhysicalWidth and mode.getPhysicalHeight are equal to the given size 136 * 137 * @param mode a Display.Mode object 138 * @param size a Point object representing the size in horizontal and vertical direction 139 */ 140 @RequiresApi(Build.VERSION_CODES.M) 141 @TargetApi(Build.VERSION_CODES.M) physicalSizeEquals(Display.Mode mode, Point size)142 private static boolean physicalSizeEquals(Display.Mode mode, Point size) { 143 return (mode.getPhysicalWidth() == size.x && mode.getPhysicalHeight() == size.y) 144 || (mode.getPhysicalWidth() == size.y && mode.getPhysicalHeight() == size.x); 145 } 146 147 /** 148 * Helper function to determine the physical display size from the system properties only. On 149 * Android TVs it is common for the UI to be configured for a lower resolution than SurfaceViews 150 * can output. Before API 26 the Display object does not provide a way to identify this case, 151 * and up to and including API 28 many devices still do not correctly set their hardware 152 * composer output size. 153 * 154 * @return the physical display size, in pixels or null if the information is not available 155 */ 156 @Nullable parsePhysicalDisplaySizeFromSystemProperties( @onNull String property, @NonNull Display display)157 private static Point parsePhysicalDisplaySizeFromSystemProperties( 158 @NonNull String property, @NonNull Display display) { 159 if (display.getDisplayId() == Display.DEFAULT_DISPLAY) { 160 // Check the system property for display size. From API 28 treble may prevent the 161 // system from writing sys.display-size so we check vendor.display-size instead. 162 String displaySize = getSystemProperty(property); 163 // If we managed to read the display size, attempt to parse it. 164 if (!TextUtils.isEmpty(displaySize)) { 165 try { 166 return parseDisplaySize(displaySize); 167 } catch (NumberFormatException e) { 168 // Do nothing for now, null is returned in the end 169 } 170 } 171 } 172 // Unable to determine display size from system properties 173 return null; 174 } 175 176 /** 177 * Gets the physical size of the given display in pixels. The size is collected in the 178 * following order: 179 * 1) sys.display-size if API < 28 (P) and the system-property is set 180 * 2) vendor.display-size if API >= 28 (P) and the system-property is set 181 * 3) physical width and height from display.getMode() for API >= 23 182 * 4) display.getRealSize() for API >= 17 183 * 5) display.getSize() 184 * 185 * @return the physical display size, in pixels 186 */ getPhysicalDisplaySize( @onNull Context context, @NonNull Display display)187 private static Point getPhysicalDisplaySize( 188 @NonNull Context context, @NonNull Display display) { 189 Point displaySize = Build.VERSION.SDK_INT < Build.VERSION_CODES.P 190 ? parsePhysicalDisplaySizeFromSystemProperties("sys.display-size", display) 191 : parsePhysicalDisplaySizeFromSystemProperties("vendor.display-size", display); 192 if (displaySize != null) { 193 return displaySize; 194 } else if (isSonyBravia4kTv(context)) { 195 // Sony Android TVs advertise support for 4k output via a system feature. 196 return new Point(DISPLAY_SIZE_4K_WIDTH, DISPLAY_SIZE_4K_HEIGHT); 197 } else { 198 // Unable to retrieve the physical display size from system properties, get display 199 // size from the framework API. Note that this might not be the actual physical 200 // display size but the, possibly down-scaled, UI size. 201 displaySize = new Point(); 202 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 203 Display.Mode mode = display.getMode(); 204 displaySize.x = mode.getPhysicalWidth(); 205 displaySize.y = mode.getPhysicalHeight(); 206 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 207 display.getRealSize(displaySize); 208 } else { 209 display.getSize(displaySize); 210 } 211 } 212 return displaySize; 213 } 214 215 /** 216 * Determines whether the connected display is a 4k capable Sony TV. 217 * 218 * @return true if the display is a Sony BRAVIA TV that supports 4k 219 */ isSonyBravia4kTv(@onNull Context context)220 private static boolean isSonyBravia4kTv(@NonNull Context context) { 221 return isTv(context) && "Sony".equals(Build.MANUFACTURER) 222 && Build.MODEL.startsWith("BRAVIA") 223 && context.getPackageManager().hasSystemFeature("com.sony.dtv.hardware.panel.qfhd"); 224 } 225 226 /** 227 * Compat class which provides an additional isNative() field. This field indicates whether a 228 * mode is native, which is important when searching for the highest possible native 229 * resolution of a display. 230 */ 231 public static final class ModeCompat { 232 private final Display.Mode mMode; 233 private final Point mPhysicalDisplaySize; 234 private final boolean mIsNative; 235 236 /** 237 * Package private constructor which creates a native ModeCompat object that does not 238 * wrap any Display.Mode object but only contains the given display size 239 * 240 * @param physicalDisplaySize a Point object representing the display size in pixels 241 * (Point.x horizontal and Point.y vertical size) 242 */ ModeCompat(@onNull Point physicalDisplaySize)243 ModeCompat(@NonNull Point physicalDisplaySize) { 244 if (physicalDisplaySize == null) { 245 throw new NullPointerException("physicalDisplaySize == null"); 246 } 247 248 mIsNative = true; 249 mPhysicalDisplaySize = physicalDisplaySize; 250 mMode = null; 251 } 252 253 /** 254 * Package private constructor which creates a non-native ModeCompat and wraps the given 255 * Mode object 256 * 257 * @param mode a Display.Mode object 258 */ 259 @RequiresApi(Build.VERSION_CODES.M) 260 @TargetApi(Build.VERSION_CODES.M) ModeCompat(@onNull Display.Mode mode, boolean isNative)261 ModeCompat(@NonNull Display.Mode mode, boolean isNative) { 262 if (mode == null) { 263 throw new NullPointerException("Display.Mode == null, can't wrap a null reference"); 264 } 265 266 mIsNative = isNative; 267 // This simplifies the getPhysicalWidth() / getPhysicalHeight functions below 268 mPhysicalDisplaySize = new Point(mode.getPhysicalWidth(), mode.getPhysicalHeight()); 269 mMode = mode; 270 } 271 272 /** 273 * Returns the physical width of the given display when configured in this mode 274 * 275 * @return the physical screen width in pixels 276 */ getPhysicalWidth()277 public int getPhysicalWidth() { 278 return mPhysicalDisplaySize.x; 279 } 280 281 /** 282 * Returns the physical height of the given display when configured in this mode 283 * 284 * @return the physical screen height in pixels 285 */ getPhysicalHeight()286 public int getPhysicalHeight() { 287 return mPhysicalDisplaySize.y; 288 } 289 290 /** 291 * Function to get the wrapped object 292 * 293 * @return the wrapped Display.Mode object or null if there was no matching mode for the 294 * native resolution. 295 */ 296 @RequiresApi(Build.VERSION_CODES.M) 297 @Nullable toMode()298 public Display.Mode toMode() { 299 return mMode; 300 } 301 302 /** 303 * This field indicates whether a mode is native, which is important when searching for 304 * the highest possible native resolution of a display. 305 * 306 * @return true if this is a native mode of the wrapped display 307 */ isNative()308 public boolean isNative() { 309 return mIsNative; 310 } 311 } 312 } 313