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