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