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.view.View 25 import androidx.annotation.VisibleForTesting 26 import com.android.systemui.shared.navigationbar.RegionSamplingHelper 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 mainExecutor: Executor?, 36 val bgExecutor: Executor?, 37 val regionSamplingEnabled: Boolean, 38 val updateForegroundColor: UpdateColorCallback, 39 val wallpaperManager: WallpaperManager? = WallpaperManager.getInstance(sampledView?.context) 40 ) : WallpaperManager.LocalWallpaperColorConsumer { 41 private var regionDarkness = RegionDarkness.DEFAULT 42 private var samplingBounds = Rect() 43 private val tmpScreenLocation = IntArray(2) 44 @VisibleForTesting var regionSampler: RegionSamplingHelper? = null 45 private var lightForegroundColor = Color.WHITE 46 private var darkForegroundColor = Color.BLACK 47 private val displaySize = Point() 48 49 /** 50 * Sets the colors to be used for Dark and Light Foreground. 51 * 52 * @param lightColor The color used for Light Foreground. 53 * @param darkColor The color used for Dark Foreground. 54 */ setForegroundColorsnull55 fun setForegroundColors(lightColor: Int, darkColor: Int) { 56 lightForegroundColor = lightColor 57 darkForegroundColor = darkColor 58 } 59 60 /** 61 * Determines which foreground color to use based on region darkness. 62 * 63 * @return the determined foreground color 64 */ currentForegroundColornull65 fun currentForegroundColor(): Int { 66 return if (regionDarkness.isDark) { 67 lightForegroundColor 68 } else { 69 darkForegroundColor 70 } 71 } 72 getRegionDarknessnull73 private fun getRegionDarkness(isRegionDark: Boolean): RegionDarkness { 74 return if (isRegionDark) { 75 RegionDarkness.DARK 76 } else { 77 RegionDarkness.LIGHT 78 } 79 } 80 currentRegionDarknessnull81 fun currentRegionDarkness(): RegionDarkness { 82 return regionDarkness 83 } 84 85 /** Start region sampler */ startRegionSamplernull86 fun startRegionSampler() { 87 if (!regionSamplingEnabled || sampledView == null) { 88 return 89 } 90 91 val sampledRegion = calculateSampledRegion(sampledView) 92 val regions = ArrayList<RectF>() 93 val sampledRegionWithOffset = convertBounds(sampledRegion) 94 95 if ( 96 sampledRegionWithOffset.left < 0.0 || 97 sampledRegionWithOffset.right > 1.0 || 98 sampledRegionWithOffset.top < 0.0 || 99 sampledRegionWithOffset.bottom > 1.0 100 ) { 101 android.util.Log.e( 102 "RegionSampler", 103 "view out of bounds: $sampledRegion | " + 104 "screen width: ${displaySize.x}, screen height: ${displaySize.y}", 105 Exception() 106 ) 107 return 108 } 109 110 regions.add(sampledRegionWithOffset) 111 112 wallpaperManager?.removeOnColorsChangedListener(this) 113 wallpaperManager?.addOnColorsChangedListener(this, regions) 114 115 // TODO(b/265969235): conditionally set FLAG_LOCK or FLAG_SYSTEM once HS smartspace 116 // implemented 117 bgExecutor?.execute( 118 Runnable { 119 val initialSampling = 120 wallpaperManager?.getWallpaperColors(WallpaperManager.FLAG_LOCK) 121 onColorsChanged(sampledRegionWithOffset, initialSampling) 122 } 123 ) 124 } 125 126 /** Stop region sampler */ stopRegionSamplernull127 fun stopRegionSampler() { 128 wallpaperManager?.removeOnColorsChangedListener(this) 129 } 130 131 /** Dump region sampler */ dumpnull132 fun dump(pw: PrintWriter) { 133 pw.println("[RegionSampler]") 134 pw.println("regionSamplingEnabled: $regionSamplingEnabled") 135 pw.println("regionDarkness: $regionDarkness") 136 pw.println("lightForegroundColor: ${Integer.toHexString(lightForegroundColor)}") 137 pw.println("darkForegroundColor: ${Integer.toHexString(darkForegroundColor)}") 138 pw.println("passed-in sampledView: $sampledView") 139 pw.println("calculated samplingBounds: $samplingBounds") 140 pw.println( 141 "sampledView width: ${sampledView?.width}, sampledView height: ${sampledView?.height}" 142 ) 143 pw.println("screen width: ${displaySize.x}, screen height: ${displaySize.y}") 144 pw.println( 145 "sampledRegionWithOffset: ${convertBounds(calculateSampledRegion(sampledView!!))}" 146 ) 147 // TODO(b/265969235): mock initialSampling based on if component is on HS or LS wallpaper 148 // HS Smartspace - wallpaperManager?.getWallpaperColors(WallpaperManager.FLAG_SYSTEM) 149 // LS Smartspace, clock - wallpaperManager?.getWallpaperColors(WallpaperManager.FLAG_LOCK) 150 pw.println( 151 "initialSampling for lockscreen: " + 152 "${wallpaperManager?.getWallpaperColors(WallpaperManager.FLAG_LOCK)}" 153 ) 154 } 155 calculateSampledRegionnull156 fun calculateSampledRegion(sampledView: View): RectF { 157 val screenLocation = tmpScreenLocation 158 /** 159 * The method getLocationOnScreen is used to obtain the view coordinates relative to its 160 * left and top edges on the device screen. Directly accessing the X and Y coordinates of 161 * the view returns the location relative to its parent view instead. 162 */ 163 sampledView.getLocationOnScreen(screenLocation) 164 val left = screenLocation[0] 165 val top = screenLocation[1] 166 167 samplingBounds.left = left 168 samplingBounds.top = top 169 samplingBounds.right = left + sampledView.width 170 samplingBounds.bottom = top + sampledView.height 171 172 return RectF(samplingBounds) 173 } 174 175 /** 176 * Convert the bounds of the region we want to sample from to fractional offsets because 177 * WallpaperManager requires the bounds to be between [0,1]. The wallpaper is treated as one 178 * continuous image, so if there are multiple screens, then each screen falls into a fractional 179 * range. For instance, 4 screens have the ranges [0, 0.25], [0,25, 0.5], [0.5, 0.75], [0.75, 180 * 1]. 181 */ convertBoundsnull182 fun convertBounds(originalBounds: RectF): RectF { 183 184 // TODO(b/265969235): GRAB # PAGES + CURRENT WALLPAPER PAGE # FROM LAUNCHER 185 // TODO(b/265968912): remove hard-coded value once LS wallpaper supported 186 val wallpaperPageNum = 0 187 val numScreens = 1 188 189 val screenWidth = displaySize.x 190 // TODO: investigate small difference between this and the height reported in go/web-hv 191 val screenHeight = displaySize.y 192 193 val newBounds = RectF() 194 // horizontal 195 newBounds.left = ((originalBounds.left / screenWidth) + wallpaperPageNum) / numScreens 196 newBounds.right = ((originalBounds.right / screenWidth) + wallpaperPageNum) / numScreens 197 // vertical 198 newBounds.top = originalBounds.top / screenHeight 199 newBounds.bottom = originalBounds.bottom / screenHeight 200 201 return newBounds 202 } 203 204 init { 205 sampledView?.context?.display?.getSize(displaySize) 206 } 207 onColorsChangednull208 override fun onColorsChanged(area: RectF?, colors: WallpaperColors?) { 209 // update text color when wallpaper color changes 210 regionDarkness = 211 getRegionDarkness( 212 (colors?.colorHints?.and(WallpaperColors.HINT_SUPPORTS_DARK_TEXT)) != 213 WallpaperColors.HINT_SUPPORTS_DARK_TEXT 214 ) 215 updateForegroundColor() 216 } 217 } 218 219 typealias UpdateColorCallback = () -> Unit 220