1 /*
<lambda>null2  * Copyright (C) 2021 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 package com.android.wallpaper.util
17 
18 import android.content.Context
19 import android.graphics.Point
20 import android.view.Display
21 import android.view.DisplayInfo
22 import android.view.Surface.ROTATION_270
23 import android.view.Surface.ROTATION_90
24 import com.android.systemui.shared.recents.utilities.Utilities
25 import com.android.wallpaper.model.wallpaper.DeviceDisplayType
26 import dagger.hilt.android.qualifiers.ApplicationContext
27 import javax.inject.Inject
28 import javax.inject.Singleton
29 import kotlin.math.min
30 
31 /**
32  * Utility class to provide methods to find and obtain information about displays via {@link
33  * DisplayManager}
34  *
35  * Always pass [Context] or [Display] for the current display, instead of using the context in this
36  * class, which is fine for stateless info.
37  */
38 @Singleton
39 class DisplayUtils
40 @Inject
41 constructor(
42     @ApplicationContext private val appContext: Context,
43     private val displaysProvider: DisplaysProvider
44 ) {
45     companion object {
46         private val ROTATION_HORIZONTAL_HINGE = setOf(ROTATION_90, ROTATION_270)
47         private const val TABLET_MIN_DPS = 600f // See Sysui's Utilities.TABLET_MIN_DPS
48     }
49 
50     fun hasMultiInternalDisplays(): Boolean {
51         return displaysProvider.getInternalDisplays().size > 1
52     }
53 
54     /**
55      * Returns the internal {@link Display} with the largest area to be used to calculate wallpaper
56      * size and cropping.
57      */
58     fun getWallpaperDisplay(): Display {
59         val internalDisplays = displaysProvider.getInternalDisplays()
60         return internalDisplays.maxWithOrNull { a, b -> getRealArea(a) - getRealArea(b) }
61             ?: internalDisplays[0]
62     }
63 
64     /**
65      * Checks if the device only has one display or unfolded screen in horizontal hinge orientation.
66      *
67      * @param context Must be a context that is associated with a display, such as an Activity or a
68      *   context created via createDisplayContext(android.view.Display).
69      */
70     fun isSingleDisplayOrUnfoldedHorizontalHinge(context: Context): Boolean {
71         return !hasMultiInternalDisplays() || isUnfoldedHorizontalHinge(context)
72     }
73 
74     /**
75      * Checks if the device is a foldable and it's unfolded and in horizontal hinge orientation
76      * (portrait).
77      *
78      * @param context Must be a context that is associated with a display, such as an Activity or a
79      *   context created via createDisplayContext(android.view.Display).
80      */
81     fun isUnfoldedHorizontalHinge(context: Context): Boolean {
82         return context.display.rotation in ROTATION_HORIZONTAL_HINGE &&
83             isOnWallpaperDisplay(context) &&
84             hasMultiInternalDisplays()
85     }
86 
87     fun getMaxDisplaysDimension(): Point {
88         val dimen = Point()
89         displaysProvider.getInternalDisplays().let { displays ->
90             dimen.x = displays.maxOf { getRealSize(it).x }
91             dimen.y = displays.maxOf { getRealSize(it).y }
92         }
93         return dimen
94     }
95 
96     /**
97      * This flag returns true if the display is:
98      * 1. a large screen device display, e.g. tablet
99      * 2. an unfolded display from a foldable device
100      *
101      * This flag returns false the display is:
102      * 1. a handheld device display
103      * 2. a folded display from a foldable device
104      *
105      * @param context Must be a context that is associated with a display, such as an Activity or a
106      *   context created via createDisplayContext(android.view.Display).
107      */
108     // TODO (b/338247922): This function is not tested due to testing blocker isOnWallpaperDisplay
109     fun isLargeScreenOrUnfoldedDisplay(context: Context): Boolean {
110         // Note that a foldable is a large screen device if the largest display is large screen.
111         // Ths flag is true if it is a large screen device, e.g. tablet, or a foldable device.
112         val isLargeScreenOrFoldable = isLargeScreenDevice()
113         // For a single display device, this flag is always true.
114         // For a multi-display device, it is only true when the current display is the largest
115         // display. For the case of foldable, it is true when the display is the unfolded one, and
116         // false when it is folded.
117         val isSingleDisplayOrUnfolded = isOnWallpaperDisplay(context)
118         return isLargeScreenOrFoldable && isSingleDisplayOrUnfolded
119     }
120 
121     /**
122      * Returns true if this device's screen (or largest screen in case of multiple screen devices)
123      * is considered a "Large screen"
124      */
125     fun isLargeScreenDevice(): Boolean {
126         // We need to use MaxDisplay's dimensions because if we're in embedded mode, our window
127         // will only be the size of the embedded Activity.
128         val maxDisplaysDimension = getRealSize(getWallpaperDisplay())
129         val smallestWidth = min(maxDisplaysDimension.x, maxDisplaysDimension.y)
130         return Utilities.dpiFromPx(
131             smallestWidth.toFloat(),
132             appContext.resources.configuration.densityDpi
133         ) >= TABLET_MIN_DPS
134     }
135 
136     /**
137      * Returns `true` if the current display is the wallpaper display on a multi-display device.
138      *
139      * On a multi-display device the wallpaper display is the largest display while on a single
140      * display device the only display is both the wallpaper display and the current display.
141      *
142      * For single display device, this is always true.
143      *
144      * @param context Must be a context that is associated with a display, such as an Activity or a
145      *   context created via createDisplayContext(android.view.Display).
146      */
147     // TODO (b/338247922): This function is not tested due to fake Display limitations with uniqueId
148     fun isOnWallpaperDisplay(context: Context): Boolean {
149         return context.display.uniqueId == getWallpaperDisplay().uniqueId
150     }
151 
152     /** Gets the real width and height of the display. */
153     fun getRealSize(display: Display): Point {
154         val displayInfo = DisplayInfo()
155         display.getDisplayInfo(displayInfo)
156         return Point(displayInfo.logicalWidth, displayInfo.logicalHeight)
157     }
158 
159     /**
160      * Returns the smallest display on a device
161      *
162      * For foldable devices, this method will return the outer display or the primary display when
163      * the device is folded. This is always the smallest display in foldable devices.
164      */
165     fun getSmallerDisplay(): Display {
166         val internalDisplays = displaysProvider.getInternalDisplays()
167         val largestDisplay = getWallpaperDisplay()
168         val smallestDisplay = internalDisplays.firstOrNull { it != largestDisplay }
169         return smallestDisplay ?: largestDisplay
170     }
171 
172     /**
173      * @param context Must be a context that is associated with a display, such as an Activity or a
174      *   context created via createDisplayContext(android.view.Display).
175      */
176     // TODO (b/338247922): This function is not tested due to testing blocker isOnWallpaperDisplay
177     fun getCurrentDisplayType(context: Context): DeviceDisplayType {
178         if (!hasMultiInternalDisplays()) {
179             return DeviceDisplayType.SINGLE
180         }
181         return if (isOnWallpaperDisplay(context)) {
182             DeviceDisplayType.UNFOLDED
183         } else {
184             DeviceDisplayType.FOLDED
185         }
186     }
187 
188     private fun getRealArea(display: Display): Int {
189         val displayInfo = DisplayInfo()
190         display.getDisplayInfo(displayInfo)
191         return displayInfo.logicalHeight * displayInfo.logicalWidth
192     }
193 
194     fun getInternalDisplaySizes(
195         allDimensions: Boolean = false,
196     ): List<Point> {
197         return displaysProvider
198             .getInternalDisplays()
199             .map { getRealSize(it) }
200             .let {
201                 if (allDimensions) {
202                     it + it.map { size -> Point(size.y, size.x) }
203                 } else {
204                     it
205                 }
206             }
207     }
208 }
209