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