1 /* 2 * Copyright (C) 2022 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.systemui.shared.regionsampling 17 18 import android.app.WallpaperColors 19 import android.app.WallpaperManager 20 import android.graphics.Color 21 import android.graphics.Point 22 import android.graphics.Rect 23 import android.graphics.RectF 24 import android.util.Log 25 import android.view.View 26 import androidx.annotation.VisibleForTesting 27 import java.io.PrintWriter 28 import java.util.concurrent.Executor 29 30 /** Class for instance of RegionSamplingHelper */ 31 open class RegionSampler 32 @JvmOverloads 33 constructor( 34 val sampledView: View, 35 val mainExecutor: Executor?, 36 val bgExecutor: Executor?, 37 val regionSamplingEnabled: Boolean, 38 val isLockscreen: Boolean = false, 39 val wallpaperManager: WallpaperManager? = WallpaperManager.getInstance(sampledView.context), 40 val updateForegroundColor: UpdateColorCallback, 41 ) : WallpaperManager.LocalWallpaperColorConsumer { 42 private var regionDarkness = RegionDarkness.DEFAULT 43 private var samplingBounds = Rect() 44 private val tmpScreenLocation = IntArray(2) 45 private var lightForegroundColor = Color.WHITE 46 private var darkForegroundColor = Color.BLACK 47 @VisibleForTesting val displaySize = Point() 48 private var initialSampling: WallpaperColors? = null 49 50 /** 51 * Sets the colors to be used for Dark and Light Foreground. 52 * 53 * @param lightColor The color used for Light Foreground. 54 * @param darkColor The color used for Dark Foreground. 55 */ setForegroundColorsnull56 fun setForegroundColors(lightColor: Int, darkColor: Int) { 57 lightForegroundColor = lightColor 58 darkForegroundColor = darkColor 59 } 60 61 private val layoutChangedListener = 62 object : View.OnLayoutChangeListener { 63 onLayoutChangenull64 override fun onLayoutChange( 65 view: View?, 66 left: Int, 67 top: Int, 68 right: Int, 69 bottom: Int, 70 oldLeft: Int, 71 oldTop: Int, 72 oldRight: Int, 73 oldBottom: Int 74 ) { 75 76 // don't pass in negative bounds when region is in transition state 77 if (sampledView.locationOnScreen[0] < 0 || sampledView.locationOnScreen[1] < 0) { 78 return 79 } 80 81 val currentViewRect = Rect(left, top, right, bottom) 82 val oldViewRect = Rect(oldLeft, oldTop, oldRight, oldBottom) 83 84 if (currentViewRect != oldViewRect) { 85 stopRegionSampler() 86 startRegionSampler() 87 } 88 } 89 } 90 91 /** 92 * Determines which foreground color to use based on region darkness. 93 * 94 * @return the determined foreground color 95 */ currentForegroundColornull96 fun currentForegroundColor(): Int { 97 return if (regionDarkness.isDark) { 98 lightForegroundColor 99 } else { 100 darkForegroundColor 101 } 102 } 103 getRegionDarknessnull104 private fun getRegionDarkness(isRegionDark: Boolean): RegionDarkness { 105 return if (isRegionDark) { 106 RegionDarkness.DARK 107 } else { 108 RegionDarkness.LIGHT 109 } 110 } 111 currentRegionDarknessnull112 fun currentRegionDarkness(): RegionDarkness { 113 return regionDarkness 114 } 115 116 /** Start region sampler */ startRegionSamplernull117 fun startRegionSampler() { 118 119 if (!regionSamplingEnabled) { 120 if (DEBUG) Log.d(TAG, "startRegionSampler() | RegionSampling flag not enabled") 121 return 122 } 123 124 sampledView.addOnLayoutChangeListener(layoutChangedListener) 125 126 val screenLocationBounds = calculateScreenLocation(sampledView) 127 if (screenLocationBounds == null) { 128 if (DEBUG) Log.d(TAG, "startRegionSampler() | passed in null region") 129 return 130 } 131 if (screenLocationBounds.isEmpty) { 132 if (DEBUG) Log.d(TAG, "startRegionSampler() | passed in empty region") 133 return 134 } 135 136 val sampledRegionWithOffset = convertBounds(screenLocationBounds) 137 if ( 138 sampledRegionWithOffset.left < 0.0 || 139 sampledRegionWithOffset.right > 1.0 || 140 sampledRegionWithOffset.top < 0.0 || 141 sampledRegionWithOffset.bottom > 1.0 142 ) { 143 if (DEBUG) 144 Log.d( 145 TAG, 146 "startRegionSampler() | view out of bounds: $screenLocationBounds | " + 147 "screen width: ${displaySize.x}, screen height: ${displaySize.y}", 148 Exception() 149 ) 150 return 151 } 152 153 val regions = ArrayList<RectF>() 154 regions.add(sampledRegionWithOffset) 155 156 wallpaperManager?.addOnColorsChangedListener( 157 this, 158 regions, 159 if (isLockscreen) WallpaperManager.FLAG_LOCK else WallpaperManager.FLAG_SYSTEM 160 ) 161 162 bgExecutor?.execute( 163 Runnable { 164 initialSampling = 165 wallpaperManager?.getWallpaperColors( 166 if (isLockscreen) WallpaperManager.FLAG_LOCK 167 else WallpaperManager.FLAG_SYSTEM 168 ) 169 mainExecutor?.execute { onColorsChanged(sampledRegionWithOffset, initialSampling) } 170 } 171 ) 172 } 173 174 /** Stop region sampler */ stopRegionSamplernull175 fun stopRegionSampler() { 176 wallpaperManager?.removeOnColorsChangedListener(this) 177 sampledView.removeOnLayoutChangeListener(layoutChangedListener) 178 } 179 180 /** Dump region sampler */ dumpnull181 fun dump(pw: PrintWriter) { 182 pw.println("[RegionSampler]") 183 pw.println("regionSamplingEnabled: $regionSamplingEnabled") 184 pw.println("regionDarkness: $regionDarkness") 185 pw.println("lightForegroundColor: ${Integer.toHexString(lightForegroundColor)}") 186 pw.println("darkForegroundColor: ${Integer.toHexString(darkForegroundColor)}") 187 pw.println("passed-in sampledView: $sampledView") 188 pw.println("calculated samplingBounds: $samplingBounds") 189 pw.println( 190 "sampledView width: ${sampledView.width}, sampledView height: ${sampledView.height}" 191 ) 192 pw.println("screen width: ${displaySize.x}, screen height: ${displaySize.y}") 193 pw.println( 194 "sampledRegionWithOffset: ${convertBounds( 195 calculateScreenLocation(sampledView) ?: RectF())}" 196 ) 197 pw.println( 198 "initialSampling for ${if (isLockscreen) "lockscreen" else "homescreen" }" + 199 ": $initialSampling" 200 ) 201 } 202 calculateScreenLocationnull203 fun calculateScreenLocation(sampledView: View): RectF? { 204 205 val screenLocation = tmpScreenLocation 206 /** 207 * The method getLocationOnScreen is used to obtain the view coordinates relative to its 208 * left and top edges on the device screen. Directly accessing the X and Y coordinates of 209 * the view returns the location relative to its parent view instead. 210 */ 211 sampledView.getLocationOnScreen(screenLocation) 212 val left = screenLocation[0] 213 val top = screenLocation[1] 214 215 samplingBounds.left = left 216 samplingBounds.top = top 217 samplingBounds.right = left + sampledView.width 218 samplingBounds.bottom = top + sampledView.height 219 220 // ensure never go out of bounds 221 if (samplingBounds.right > displaySize.x) samplingBounds.right = displaySize.x 222 if (samplingBounds.bottom > displaySize.y) samplingBounds.bottom = displaySize.y 223 224 return RectF(samplingBounds) 225 } 226 227 /** 228 * Convert the bounds of the region we want to sample from to fractional offsets because 229 * WallpaperManager requires the bounds to be between [0,1]. The wallpaper is treated as one 230 * continuous image, so if there are multiple screens, then each screen falls into a fractional 231 * range. For instance, 4 screens have the ranges [0, 0.25], [0,25, 0.5], [0.5, 0.75], [0.75, 232 * 1]. 233 */ convertBoundsnull234 fun convertBounds(originalBounds: RectF): RectF { 235 236 // TODO(b/265969235): GRAB # PAGES + CURRENT WALLPAPER PAGE # FROM LAUNCHER (--> HS 237 // Smartspace always on 1st page) 238 // TODO(b/265968912): remove hard-coded value once LS wallpaper supported 239 val wallpaperPageNum = 0 240 val numScreens = 1 241 242 val screenWidth = displaySize.x 243 // TODO: investigate small difference between this and the height reported in go/web-hv 244 val screenHeight = displaySize.y 245 246 val newBounds = RectF() 247 // horizontal 248 newBounds.left = ((originalBounds.left / screenWidth) + wallpaperPageNum) / numScreens 249 newBounds.right = ((originalBounds.right / screenWidth) + wallpaperPageNum) / numScreens 250 // vertical 251 newBounds.top = originalBounds.top / screenHeight 252 newBounds.bottom = originalBounds.bottom / screenHeight 253 254 return newBounds 255 } 256 257 init { 258 sampledView?.context?.display?.getSize(displaySize) 259 } 260 onColorsChangednull261 override fun onColorsChanged(area: RectF?, colors: WallpaperColors?) { 262 // update text color when wallpaper color changes 263 regionDarkness = 264 getRegionDarkness( 265 (colors?.colorHints?.and(WallpaperColors.HINT_SUPPORTS_DARK_TEXT)) != 266 WallpaperColors.HINT_SUPPORTS_DARK_TEXT 267 ) 268 if (DEBUG) 269 Log.d(TAG, "onColorsChanged() | region darkness = $regionDarkness for region $area") 270 updateForegroundColor() 271 } 272 273 companion object { 274 private const val TAG = "RegionSampler" 275 private const val DEBUG = false 276 } 277 } 278 279 typealias UpdateColorCallback = () -> Unit 280