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