1 /* 2 * Copyright 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.core.view; 18 19 import static android.content.Context.UI_MODE_SERVICE; 20 21 import android.annotation.SuppressLint; 22 import android.app.UiModeManager; 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.graphics.Point; 26 import android.os.Build; 27 import android.text.TextUtils; 28 import android.view.Display; 29 30 import androidx.annotation.RequiresApi; 31 import androidx.core.util.Preconditions; 32 33 import org.jspecify.annotations.NonNull; 34 import org.jspecify.annotations.Nullable; 35 36 import java.lang.reflect.Method; 37 38 /** 39 * A class for retrieving accurate display modes for a display. 40 * <p> 41 * On many Android TV devices, Display.Mode may not report the accurate width and height because 42 * these devices do not have powerful enough graphics pipelines to run framework code at the same 43 * resolutions supported by their video pipelines. For these devices, there is no way for an app 44 * to determine, for example, whether or not the current display mode is 4k, or that the display 45 * supports switching to other 4k modes. This class offers a workaround for this problem. 46 */ 47 public final class DisplayCompat { 48 private static final int DISPLAY_SIZE_4K_WIDTH = 3840; 49 private static final int DISPLAY_SIZE_4K_HEIGHT = 2160; 50 DisplayCompat()51 private DisplayCompat() { 52 // This class is non-instantiable. 53 } 54 55 /** 56 * Gets the current display mode of the given display, where the size can be relied on to 57 * determine support for 4k on Android TV devices. 58 */ getMode(@onNull Context context, @NonNull Display display)59 public static @NonNull ModeCompat getMode(@NonNull Context context, @NonNull Display display) { 60 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 61 return Api23Impl.getMode(context, display); 62 } 63 // Prior to display modes, the best we can do is return the display size as the display 64 // mode. 65 return new ModeCompat(getDisplaySize(context, display)); 66 } 67 getDisplaySize(@onNull Context context, @NonNull Display display)68 private static @NonNull Point getDisplaySize(@NonNull Context context, 69 @NonNull Display display) { 70 // If a workaround for the display size is present, use it. 71 Point displaySize = getCurrentDisplaySizeFromWorkarounds(context, display); 72 if (displaySize != null) { 73 return displaySize; 74 } 75 76 displaySize = new Point(); 77 display.getRealSize(displaySize); 78 return displaySize; 79 } 80 81 /** 82 * Gets the supported modes of the given display where any mode with the same size as the 83 * current mode can be relied on to determine support for 4k on Android TV devices. 84 */ 85 @SuppressLint("ArrayReturn") getSupportedModes( @onNull Context context, @NonNull Display display)86 public static ModeCompat @NonNull [] getSupportedModes( 87 @NonNull Context context, @NonNull Display display) { 88 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 89 return Api23Impl.getSupportedModes(context, display); 90 } 91 // Prior to display modes, the best we can do is return the current mode - the 92 // current display size wrapped in a ModeCompat object. 93 return new ModeCompat[] { getMode(context, display) }; 94 } 95 96 /** 97 * Parses a string which represents the display-size which contains 'x' as a delimiter 98 * between two integers representing the display's width and height and returns the 99 * display size as a Point object. 100 * 101 * @param displaySize a string 102 * @return a Point object containing the size in x and y direction in pixels 103 * @throws NumberFormatException in case the integers cannot be parsed 104 */ parseDisplaySize(@onNull String displaySize)105 private static Point parseDisplaySize(@NonNull String displaySize) 106 throws NumberFormatException { 107 String[] displaySizeParts = displaySize.trim().split("x", -1); 108 if (displaySizeParts.length == 2) { 109 int width = Integer.parseInt(displaySizeParts[0]); 110 int height = Integer.parseInt(displaySizeParts[1]); 111 if (width > 0 && height > 0) { 112 return new Point(width, height); 113 } 114 } 115 throw new NumberFormatException(); 116 } 117 118 /** 119 * Reads a system property and returns its string value. 120 * 121 * @param name the name of the system property 122 * @return the result string or null if an exception occurred 123 */ getSystemProperty(String name)124 private static @Nullable String getSystemProperty(String name) { 125 try { 126 @SuppressLint("PrivateApi") 127 Class<?> systemProperties = Class.forName("android.os.SystemProperties"); 128 Method getMethod = systemProperties.getMethod("get", String.class); 129 return (String) getMethod.invoke(systemProperties, name); 130 } catch (Exception e) { 131 return null; 132 } 133 } 134 135 /** 136 * Returns whether the app is running on a TV device 137 */ isTv(@onNull Context context)138 private static boolean isTv(@NonNull Context context) { 139 // See https://developer.android.com/training/tv/start/hardware.html#runtime-check. 140 UiModeManager uiModeManager = (UiModeManager) context.getSystemService(UI_MODE_SERVICE); 141 return uiModeManager != null 142 && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; 143 } 144 145 /** 146 * Helper function to determine the physical display size from the system properties only. On 147 * Android TVs it is common for the UI to be configured for a lower resolution than SurfaceViews 148 * can output. Before API 26 the Display object does not provide a way to identify this case, 149 * and up to and including API 28 many devices still do not correctly set their hardware 150 * composer output size. 151 * 152 * @return the physical display size, in pixels or null if the information is not available 153 */ parsePhysicalDisplaySizeFromSystemProperties( @onNull String property, @NonNull Display display)154 private static @Nullable Point parsePhysicalDisplaySizeFromSystemProperties( 155 @NonNull String property, @NonNull Display display) { 156 // System properties are only relevant for the default display. 157 if (display.getDisplayId() != Display.DEFAULT_DISPLAY) { 158 return null; 159 } 160 161 // Check the system property for display size. 162 String displaySize = getSystemProperty(property); 163 if (TextUtils.isEmpty(displaySize) || displaySize == null) { 164 return null; 165 } 166 167 try { 168 return parseDisplaySize(displaySize); 169 } catch (NumberFormatException e) { 170 // Ignore invalid display sizes. 171 return null; 172 } 173 } 174 175 /** 176 * Gets the current physical size of the given display in pixels from a variety of vendor 177 * workarounds. 178 */ getCurrentDisplaySizeFromWorkarounds( @onNull Context context, @NonNull Display display)179 static Point getCurrentDisplaySizeFromWorkarounds( 180 @NonNull Context context, 181 @NonNull Display display) { 182 // From API 28 treble may prevent the system from writing sys.display-size so we check 183 // vendor.display-size instead. 184 Point displaySize = Build.VERSION.SDK_INT < Build.VERSION_CODES.P 185 ? parsePhysicalDisplaySizeFromSystemProperties("sys.display-size", display) 186 : parsePhysicalDisplaySizeFromSystemProperties("vendor.display-size", display); 187 if (displaySize != null) { 188 return displaySize; 189 } else if (isSonyBravia4kTv(context)) { 190 // Sony Android TVs advertise support for 4k output via a system feature. 191 // The TV may or may not be currently in the 4k display mode. Instead, we can only 192 // assume that if the current display mode is the highest display mode, then we are 193 // in a 4k mode. 194 return isCurrentModeTheLargestMode(display) 195 ? new Point(DISPLAY_SIZE_4K_WIDTH, DISPLAY_SIZE_4K_HEIGHT) 196 : null; 197 } 198 return null; 199 } 200 201 /** 202 * Is the connected display is a 4k capable Sony TV? 203 */ 204 private static boolean isSonyBravia4kTv(@NonNull Context context) { 205 return isTv(context) 206 && "Sony".equals(Build.MANUFACTURER) 207 && Build.MODEL.startsWith("BRAVIA") 208 && context.getPackageManager().hasSystemFeature( 209 "com.sony.dtv.hardware.panel.qfhd"); 210 } 211 212 /** 213 * Does the current display mode have the largest physical size of all supported modes? 214 */ 215 static boolean isCurrentModeTheLargestMode(@NonNull Display display) { 216 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 217 return Api23Impl.isCurrentModeTheLargestMode(display); 218 } else { 219 // Prior to modes, the current mode is always the largest display mode. 220 return true; 221 } 222 } 223 224 @RequiresApi(Build.VERSION_CODES.M) 225 static class Api23Impl { 226 private Api23Impl() {} 227 228 static @NonNull ModeCompat getMode(@NonNull Context context, @NonNull Display display) { 229 Display.Mode currentMode = display.getMode(); 230 Point workaroundSize = getCurrentDisplaySizeFromWorkarounds(context, display); 231 // If the current mode has the wrong physical size, then correct it with the 232 // workaround. 233 return workaroundSize == null || physicalSizeEquals(currentMode, workaroundSize) 234 ? new ModeCompat(currentMode, /* isNative= */ true) 235 : new ModeCompat(currentMode, workaroundSize); 236 } 237 238 @SuppressLint("ArrayReturn") 239 public static ModeCompat @NonNull [] getSupportedModes( 240 @NonNull Context context, @NonNull Display display) { 241 Display.Mode[] supportedModes = display.getSupportedModes(); 242 ModeCompat[] supportedModesCompat = new ModeCompat[supportedModes.length]; 243 244 Display.Mode currentMode = display.getMode(); 245 Point workaroundSize = getCurrentDisplaySizeFromWorkarounds(context, display); 246 // The workaround size not matching the current mode indicates that the Android TV 247 // reports mode sizes inaccurately. 248 if (workaroundSize == null || physicalSizeEquals(currentMode, workaroundSize)) { 249 // This Android TV device reports display mode sizes accurately. 250 for (int i = 0; i < supportedModes.length; ++i) { 251 boolean isNative = physicalSizeEquals(supportedModes[i], currentMode); 252 supportedModesCompat[i] = new ModeCompat(supportedModes[i], isNative); 253 } 254 } else { 255 // This Android TV device does NOT report display mode sizes accurately. 256 for (int i = 0; i < supportedModes.length; ++i) { 257 // A mode with the same size as the current mode should use the workaround size. 258 supportedModesCompat[i] = physicalSizeEquals(supportedModes[i], currentMode) 259 ? new ModeCompat(supportedModes[i], workaroundSize) 260 : new ModeCompat(supportedModes[i], /* isNative= */ false); 261 } 262 } 263 return supportedModesCompat; 264 } 265 266 static boolean isCurrentModeTheLargestMode(@NonNull Display display) { 267 Display.Mode currentMode = display.getMode(); 268 Display.Mode[] supportedModes = display.getSupportedModes(); 269 for (Display.Mode supportedMode : supportedModes) { 270 if (currentMode.getPhysicalHeight() < supportedMode.getPhysicalHeight() 271 || currentMode.getPhysicalWidth() < supportedMode.getPhysicalWidth()) { 272 return false; 273 } 274 } 275 return true; 276 } 277 278 /** 279 * Returns true if mode.getPhysicalWidth and mode.getPhysicalHeight are equal to the given 280 * size. 281 */ 282 static boolean physicalSizeEquals(Display.Mode mode, Point size) { 283 return (mode.getPhysicalWidth() == size.x && mode.getPhysicalHeight() == size.y) 284 || (mode.getPhysicalWidth() == size.y && mode.getPhysicalHeight() == size.x); 285 } 286 287 /** 288 * Returns true if mode.getPhysicalWidth and mode.getPhysicalHeight are equal to the size 289 * of another mode. 290 */ 291 static boolean physicalSizeEquals(Display.Mode mode, Display.Mode otherMode) { 292 return mode.getPhysicalWidth() == otherMode.getPhysicalWidth() 293 && mode.getPhysicalHeight() == otherMode.getPhysicalHeight(); 294 } 295 } 296 297 /** 298 * Compat class which provides access to the underlying display mode, if there is one, and 299 * a more reliable display mode size. 300 */ 301 public static final class ModeCompat { 302 private final Display.Mode mMode; 303 private final Point mPhysicalSize; 304 private final boolean mIsNative; 305 306 /** 307 * Create a ModeCompat object that does not wrap any Display.Mode object, but only 308 * contains the display mode size. 309 * 310 * @param physicalSize the physical size of the display mode 311 */ 312 ModeCompat(@NonNull Point physicalSize) { 313 Preconditions.checkNotNull(physicalSize, "physicalSize == null"); 314 mPhysicalSize = physicalSize; 315 mMode = null; 316 mIsNative = true; 317 } 318 319 /** 320 * Create a ModeCompat object that wraps a Display.Mode that has an accurate physical size. 321 * 322 * @param mode the wrapped Display.Mode object 323 */ 324 @RequiresApi(Build.VERSION_CODES.M) 325 ModeCompat(Display.@NonNull Mode mode, boolean isNative) { 326 Preconditions.checkNotNull(mode, "mode == null, can't wrap a null reference"); 327 // This simplifies the getPhysicalWidth() / getPhysicalHeight functions below 328 mPhysicalSize = new Point(Api23Impl.getPhysicalWidth(mode), 329 Api23Impl.getPhysicalHeight(mode)); 330 mMode = mode; 331 mIsNative = isNative; 332 } 333 334 /** 335 * Create a ModeCompat object that wraps a Display.Mode, but with a more accurate 336 * display mode size. 337 * 338 * @param mode the wrapped Display.Mode object 339 * @param physicalSize the true physical size of the display mode 340 * 341 */ 342 @RequiresApi(Build.VERSION_CODES.M) 343 ModeCompat(Display.@NonNull Mode mode, @NonNull Point physicalSize) { 344 Preconditions.checkNotNull(mode, "mode == null, can't wrap a null reference"); 345 Preconditions.checkNotNull(physicalSize, "physicalSize == null"); 346 mPhysicalSize = physicalSize; 347 mMode = mode; 348 mIsNative = true; 349 } 350 351 /** 352 * Returns the physical width of the given display when configured in this mode. 353 */ 354 public int getPhysicalWidth() { 355 return mPhysicalSize.x; 356 } 357 358 /** 359 * Returns the physical height of the given display when configured in this mode. 360 */ 361 public int getPhysicalHeight() { 362 return mPhysicalSize.y; 363 } 364 365 /** 366 * This field indicates whether a mode has the same resolution as the current display mode. 367 * <p> 368 * This field does *not* indicate the native resolution of the display. 369 * 370 * @return true if this mode is the same resolution as the current display mode. 371 * @deprecated Use {@link DisplayCompat#getMode} to retrieve the resolution of the current 372 * display mode. 373 */ 374 @Deprecated 375 public boolean isNative() { 376 return mIsNative; 377 } 378 379 /** 380 * Returns the wrapped object Display.Mode, which may be null if no mode is available. 381 */ 382 @RequiresApi(Build.VERSION_CODES.M) 383 public Display.@Nullable Mode toMode() { 384 return mMode; 385 } 386 387 @RequiresApi(23) 388 static class Api23Impl { 389 private Api23Impl() { 390 // This class is not instantiable. 391 } 392 393 static int getPhysicalWidth(Display.Mode mode) { 394 return mode.getPhysicalWidth(); 395 } 396 397 static int getPhysicalHeight(Display.Mode mode) { 398 return mode.getPhysicalHeight(); 399 } 400 } 401 } 402 } 403