Region Sampler Wallpaper Colors Implementation

Refactoring Region Sampler to use WallpaperColors/WallpaperManager
instead of Region Sampling Helper so that we can improve performance
metrics by no longer using SurfaceFlinger to sample. This refactor is applied to the lockscreen smartspace and lockscreen clock implementations of Region Sampler.

Bug: 202758428
Test: manual(tested clock and smartspace on white and black solid color
wallpaper), atest RegionSamplerTest, atest ClockEventControllerTest
Change-Id: Id5d16d07885c82e1f756160071cc9df3cf8fcb0f
This commit is contained in:
Florence Yang
2023-01-05 20:25:50 +00:00
parent cafbb75bfa
commit 47a372dbec
5 changed files with 213 additions and 189 deletions

View File

@@ -15,39 +15,36 @@
*/
package com.android.systemui.shared.regionsampling
import android.app.WallpaperColors
import android.app.WallpaperManager
import android.graphics.Color
import android.graphics.Point
import android.graphics.Rect
import android.graphics.RectF
import android.view.View
import androidx.annotation.VisibleForTesting
import com.android.systemui.shared.navigationbar.RegionSamplingHelper
import com.android.systemui.shared.navigationbar.RegionSamplingHelper.SamplingCallback
import java.io.PrintWriter
import java.util.concurrent.Executor
/** Class for instance of RegionSamplingHelper */
open class RegionSampler(
sampledView: View?,
open class RegionSampler
@JvmOverloads
constructor(
val sampledView: View?,
mainExecutor: Executor?,
bgExecutor: Executor?,
regionSamplingEnabled: Boolean,
updateFun: UpdateColorCallback
) {
val bgExecutor: Executor?,
val regionSamplingEnabled: Boolean,
val updateForegroundColor: UpdateColorCallback,
val wallpaperManager: WallpaperManager? = WallpaperManager.getInstance(sampledView?.context)
) : WallpaperManager.LocalWallpaperColorConsumer {
private var regionDarkness = RegionDarkness.DEFAULT
private var samplingBounds = Rect()
private val tmpScreenLocation = IntArray(2)
@VisibleForTesting var regionSampler: RegionSamplingHelper? = null
private var lightForegroundColor = Color.WHITE
private var darkForegroundColor = Color.BLACK
@VisibleForTesting
open fun createRegionSamplingHelper(
sampledView: View,
callback: SamplingCallback,
mainExecutor: Executor?,
bgExecutor: Executor?
): RegionSamplingHelper {
return RegionSamplingHelper(sampledView, callback, mainExecutor, bgExecutor)
}
private val displaySize = Point()
/**
* Sets the colors to be used for Dark and Light Foreground.
@@ -73,7 +70,7 @@ open class RegionSampler(
}
}
private fun convertToClockDarkness(isRegionDark: Boolean): RegionDarkness {
private fun getRegionDarkness(isRegionDark: Boolean): RegionDarkness {
return if (isRegionDark) {
RegionDarkness.DARK
} else {
@@ -87,12 +84,32 @@ open class RegionSampler(
/** Start region sampler */
fun startRegionSampler() {
regionSampler?.start(samplingBounds)
if (!regionSamplingEnabled || sampledView == null) {
return
}
val sampledRegion = calculateSampledRegion(sampledView)
val regions = ArrayList<RectF>()
val sampledRegionWithOffset = convertBounds(sampledRegion)
regions.add(sampledRegionWithOffset)
wallpaperManager?.removeOnColorsChangedListener(this)
wallpaperManager?.addOnColorsChangedListener(this, regions)
// TODO(b/265969235): conditionally set FLAG_LOCK or FLAG_SYSTEM once HS smartspace
// implemented
bgExecutor?.execute(
Runnable {
val initialSampling =
wallpaperManager?.getWallpaperColors(WallpaperManager.FLAG_LOCK)
onColorsChanged(sampledRegionWithOffset, initialSampling)
}
)
}
/** Stop region sampler */
fun stopRegionSampler() {
regionSampler?.stop()
wallpaperManager?.removeOnColorsChangedListener(this)
}
/** Dump region sampler */
@@ -100,43 +117,66 @@ open class RegionSampler(
regionSampler?.dump(pw)
}
init {
if (regionSamplingEnabled && sampledView != null) {
regionSampler =
createRegionSamplingHelper(
sampledView,
object : SamplingCallback {
override fun onRegionDarknessChanged(isRegionDark: Boolean) {
regionDarkness = convertToClockDarkness(isRegionDark)
updateFun()
}
/**
* The method getLocationOnScreen is used to obtain the view coordinates
* relative to its left and top edges on the device screen. Directly
* accessing the X and Y coordinates of the view returns the location
* relative to its parent view instead.
*/
override fun getSampledRegion(sampledView: View): Rect {
val screenLocation = tmpScreenLocation
sampledView.getLocationOnScreen(screenLocation)
val left = screenLocation[0]
val top = screenLocation[1]
samplingBounds.left = left
samplingBounds.top = top
samplingBounds.right = left + sampledView.width
samplingBounds.bottom = top + sampledView.height
return samplingBounds
}
fun calculateSampledRegion(sampledView: View): RectF {
val screenLocation = tmpScreenLocation
/**
* The method getLocationOnScreen is used to obtain the view coordinates relative to its
* left and top edges on the device screen. Directly accessing the X and Y coordinates of
* the view returns the location relative to its parent view instead.
*/
sampledView.getLocationOnScreen(screenLocation)
val left = screenLocation[0]
val top = screenLocation[1]
override fun isSamplingEnabled(): Boolean {
return regionSamplingEnabled
}
},
mainExecutor,
bgExecutor
)
}
regionSampler?.setWindowVisible(true)
samplingBounds.left = left
samplingBounds.top = top
samplingBounds.right = left + sampledView.width
samplingBounds.bottom = top + sampledView.height
return RectF(samplingBounds)
}
/**
* Convert the bounds of the region we want to sample from to fractional offsets because
* WallpaperManager requires the bounds to be between [0,1]. The wallpaper is treated as one
* continuous image, so if there are multiple screens, then each screen falls into a fractional
* range. For instance, 4 screens have the ranges [0, 0.25], [0,25, 0.5], [0.5, 0.75], [0.75,
* 1].
*/
fun convertBounds(originalBounds: RectF): RectF {
// TODO(b/265969235): GRAB # PAGES + CURRENT WALLPAPER PAGE # FROM LAUNCHER
// TODO(b/265968912): remove hard-coded value once LS wallpaper supported
val wallpaperPageNum = 0
val numScreens = 1
val screenWidth = displaySize.x
// TODO: investigate small difference between this and the height reported in go/web-hv
val screenHeight = displaySize.y
val newBounds = RectF()
// horizontal
newBounds.left = ((originalBounds.left / screenWidth) + wallpaperPageNum) / numScreens
newBounds.right = ((originalBounds.right / screenWidth) + wallpaperPageNum) / numScreens
// vertical
newBounds.top = originalBounds.top / screenHeight
newBounds.bottom = originalBounds.bottom / screenHeight
return newBounds
}
init {
sampledView?.context?.display?.getSize(displaySize)
}
override fun onColorsChanged(area: RectF?, colors: WallpaperColors?) {
// update text color when wallpaper color changes
regionDarkness =
getRegionDarkness(
(colors?.colorHints?.and(WallpaperColors.HINT_SUPPORTS_DARK_TEXT)) !=
WallpaperColors.HINT_SUPPORTS_DARK_TEXT
)
updateForegroundColor()
}
}

View File

@@ -24,6 +24,7 @@ import android.content.res.Resources
import android.text.format.DateFormat
import android.util.TypedValue
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
@@ -47,18 +48,17 @@ import com.android.systemui.shared.regionsampling.RegionSampler
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
import com.android.systemui.statusbar.policy.ConfigurationController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import java.io.PrintWriter
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
/**
* Controller for a Clock provided by the registry and used on the keyguard. Instantiated by
@@ -89,7 +89,11 @@ open class ClockEventController @Inject constructor(
value.largeClock.logBuffer = largeLogBuffer
value.initialize(resources, dozeAmount, 0f)
updateRegionSamplers(value)
if (regionSamplingEnabled) {
clock?.smallClock?.view?.addOnLayoutChangeListener(mLayoutChangedListener)
clock?.largeClock?.view?.addOnLayoutChangeListener(mLayoutChangedListener)
}
updateFontSizes()
}
}
@@ -104,47 +108,87 @@ open class ClockEventController @Inject constructor(
private var disposableHandle: DisposableHandle? = null
private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING)
private fun updateColors() {
private val mLayoutChangedListener = object : View.OnLayoutChangeListener {
private var currentSmallClockView: View? = null
private var currentLargeClockView: View? = null
private var currentSmallClockLocation = IntArray(2)
private var currentLargeClockLocation = IntArray(2)
if (regionSamplingEnabled && smallRegionSampler != null && largeRegionSampler != null) {
val wallpaperManager = WallpaperManager.getInstance(context)
if (!wallpaperManager.lockScreenWallpaperExists()) {
smallClockIsDark = smallRegionSampler!!.currentRegionDarkness().isDark
largeClockIsDark = largeRegionSampler!!.currentRegionDarkness().isDark
}
} else {
val isLightTheme = TypedValue()
context.theme.resolveAttribute(android.R.attr.isLightTheme, isLightTheme, true)
smallClockIsDark = isLightTheme.data == 0
largeClockIsDark = isLightTheme.data == 0
override fun onLayoutChange(
view: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
val parent = (view?.parent) as FrameLayout
// don't pass in negative bounds when clocks are in transition state
if (view.locationOnScreen[0] < 0 || view.locationOnScreen[1] < 0) {
return
}
// SMALL CLOCK
if (parent.id == R.id.lockscreen_clock_view) {
// view bounds have changed due to clock size changing (i.e. different character widths)
// AND/OR the view has been translated when transitioning between small and large clock
if (view != currentSmallClockView ||
!view.locationOnScreen.contentEquals(currentSmallClockLocation)) {
currentSmallClockView = view
currentSmallClockLocation = view.locationOnScreen
updateRegionSampler(view)
}
}
// LARGE CLOCK
else if (parent.id == R.id.lockscreen_clock_view_large) {
if (view != currentLargeClockView ||
!view.locationOnScreen.contentEquals(currentLargeClockLocation)) {
currentLargeClockView = view
currentLargeClockLocation = view.locationOnScreen
updateRegionSampler(view)
}
}
}
}
private fun updateColors() {
val wallpaperManager = WallpaperManager.getInstance(context)
if (regionSamplingEnabled && !wallpaperManager.lockScreenWallpaperExists()) {
if (regionSampler != null) {
if (regionSampler?.sampledView == clock?.smallClock?.view) {
smallClockIsDark = regionSampler!!.currentRegionDarkness().isDark
clock?.smallClock?.events?.onRegionDarknessChanged(smallClockIsDark)
return
} else if (regionSampler?.sampledView == clock?.largeClock?.view) {
largeClockIsDark = regionSampler!!.currentRegionDarkness().isDark
clock?.largeClock?.events?.onRegionDarknessChanged(largeClockIsDark)
return
}
}
}
val isLightTheme = TypedValue()
context.theme.resolveAttribute(android.R.attr.isLightTheme, isLightTheme, true)
smallClockIsDark = isLightTheme.data == 0
largeClockIsDark = isLightTheme.data == 0
clock?.smallClock?.events?.onRegionDarknessChanged(smallClockIsDark)
clock?.largeClock?.events?.onRegionDarknessChanged(largeClockIsDark)
}
private fun updateRegionSamplers(currentClock: ClockController?) {
smallRegionSampler?.stopRegionSampler()
largeRegionSampler?.stopRegionSampler()
smallRegionSampler = createRegionSampler(
currentClock?.smallClock?.view,
mainExecutor,
bgExecutor,
regionSamplingEnabled,
::updateColors
)
largeRegionSampler = createRegionSampler(
currentClock?.largeClock?.view,
mainExecutor,
bgExecutor,
regionSamplingEnabled,
::updateColors
)
smallRegionSampler!!.startRegionSampler()
largeRegionSampler!!.startRegionSampler()
private fun updateRegionSampler(sampledRegion: View) {
regionSampler?.stopRegionSampler()
regionSampler = createRegionSampler(
sampledRegion,
mainExecutor,
bgExecutor,
regionSamplingEnabled,
::updateColors
)?.apply { startRegionSampler() }
updateColors()
}
@@ -155,7 +199,7 @@ open class ClockEventController @Inject constructor(
bgExecutor: Executor?,
regionSamplingEnabled: Boolean,
updateColors: () -> Unit
): RegionSampler {
): RegionSampler? {
return RegionSampler(
sampledView,
mainExecutor,
@@ -164,8 +208,7 @@ open class ClockEventController @Inject constructor(
updateColors)
}
var smallRegionSampler: RegionSampler? = null
var largeRegionSampler: RegionSampler? = null
var regionSampler: RegionSampler? = null
private var smallClockIsDark = true
private var largeClockIsDark = true
@@ -232,8 +275,6 @@ open class ClockEventController @Inject constructor(
configurationController.addCallback(configListener)
batteryController.addCallback(batteryCallback)
keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
smallRegionSampler?.startRegionSampler()
largeRegionSampler?.startRegionSampler()
disposableHandle = parent.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
listenForDozing(this)
@@ -258,8 +299,7 @@ open class ClockEventController @Inject constructor(
configurationController.removeCallback(configListener)
batteryController.removeCallback(batteryCallback)
keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
smallRegionSampler?.stopRegionSampler()
largeRegionSampler?.stopRegionSampler()
regionSampler?.stopRegionSampler()
}
private fun updateFontSizes() {
@@ -275,8 +315,7 @@ open class ClockEventController @Inject constructor(
fun dump(pw: PrintWriter) {
pw.println(this)
clock?.dump(pw)
smallRegionSampler?.dump(pw)
largeRegionSampler?.dump(pw)
regionSampler?.dump(pw)
}
@VisibleForTesting

View File

@@ -97,7 +97,7 @@ class LockscreenSmartspaceController @Inject constructor(
private val regionSamplingEnabled =
featureFlags.isEnabled(Flags.REGION_SAMPLING)
private var isContentUpdatedOnce = false
private var showNotifications = false
private var showSensitiveContentForCurrentUser = false
private var showSensitiveContentForManagedUser = false
@@ -111,19 +111,6 @@ class LockscreenSmartspaceController @Inject constructor(
override fun onViewAttachedToWindow(v: View) {
smartspaceViews.add(v as SmartspaceView)
if (regionSamplingEnabled) {
var regionSampler = RegionSampler(
v,
uiExecutor,
bgExecutor,
regionSamplingEnabled,
updateFun
)
initializeTextColors(regionSampler)
regionSampler.startRegionSampler()
regionSamplers.put(v, regionSampler)
}
connectSession()
updateTextColorFromWallpaper()
@@ -133,12 +120,6 @@ class LockscreenSmartspaceController @Inject constructor(
override fun onViewDetachedFromWindow(v: View) {
smartspaceViews.remove(v as SmartspaceView)
if (regionSamplingEnabled) {
var regionSampler = regionSamplers.getValue(v)
regionSampler.stopRegionSampler()
regionSamplers.remove(v)
}
if (smartspaceViews.isEmpty()) {
disconnect()
}
@@ -149,6 +130,24 @@ class LockscreenSmartspaceController @Inject constructor(
execution.assertIsMainThread()
val filteredTargets = targets.filter(::filterSmartspaceTarget)
plugin?.onTargetsAvailable(filteredTargets)
if (!isContentUpdatedOnce) {
for (v in smartspaceViews) {
if (regionSamplingEnabled) {
var regionSampler = RegionSampler(
v as View,
uiExecutor,
bgExecutor,
regionSamplingEnabled,
updateFun
)
initializeTextColors(regionSampler)
regionSamplers[v] = regionSampler
regionSampler.startRegionSampler()
}
updateTextColorFromWallpaper()
}
isContentUpdatedOnce = true
}
}
private val userTrackerCallback = object : UserTracker.Callback {
@@ -394,7 +393,8 @@ class LockscreenSmartspaceController @Inject constructor(
private fun updateTextColorFromWallpaper() {
val wallpaperManager = WallpaperManager.getInstance(context)
if (!regionSamplingEnabled || wallpaperManager.lockScreenWallpaperExists()) {
if (!regionSamplingEnabled || wallpaperManager.lockScreenWallpaperExists() ||
regionSamplers.isEmpty()) {
val wallpaperTextColor =
Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColor)
smartspaceViews.forEach { it.setPrimaryTextColor(wallpaperTextColor) }

View File

@@ -51,7 +51,6 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyFloat
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
@@ -140,8 +139,9 @@ class ClockEventControllerTest : SysuiTestCase() {
@Test
fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) {
verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
// TODO(b/266103601): delete this test and add more coverage for updateColors()
// verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
// verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
verify(configurationController).addCallback(capture(captor))
@@ -152,9 +152,6 @@ class ClockEventControllerTest : SysuiTestCase() {
@Test
fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) {
verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
verify(configurationController).addCallback(capture(captor))
captor.value.onDensityOrFontScaleChanged()

View File

@@ -1,21 +1,17 @@
package com.android.systemui.shared.regionsampling
import android.graphics.Rect
import android.app.WallpaperManager
import android.testing.AndroidTestingRunner
import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.shared.navigationbar.RegionSamplingHelper
import java.io.PrintWriter
import java.util.concurrent.Executor
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
@@ -28,9 +24,8 @@ class RegionSamplerTest : SysuiTestCase() {
@Mock private lateinit var sampledView: View
@Mock private lateinit var mainExecutor: Executor
@Mock private lateinit var bgExecutor: Executor
@Mock private lateinit var regionSampler: RegionSamplingHelper
@Mock private lateinit var pw: PrintWriter
@Mock private lateinit var callback: RegionSamplingHelper.SamplingCallback
@Mock private lateinit var wallpaperManager: WallpaperManager
private lateinit var mRegionSampler: RegionSampler
private var updateFun: UpdateColorCallback = {}
@@ -38,65 +33,18 @@ class RegionSamplerTest : SysuiTestCase() {
@Before
fun setUp() {
whenever(sampledView.isAttachedToWindow).thenReturn(true)
whenever(regionSampler.callback).thenReturn(this@RegionSamplerTest.callback)
mRegionSampler =
object : RegionSampler(sampledView, mainExecutor, bgExecutor, true, updateFun) {
override fun createRegionSamplingHelper(
sampledView: View,
callback: RegionSamplingHelper.SamplingCallback,
mainExecutor: Executor?,
bgExecutor: Executor?
): RegionSamplingHelper {
return this@RegionSamplerTest.regionSampler
}
}
RegionSampler(sampledView, mainExecutor, bgExecutor, true, updateFun, wallpaperManager)
}
@Test
fun testStartRegionSampler() {
mRegionSampler.startRegionSampler()
verify(regionSampler).start(Rect(0, 0, 0, 0))
}
@Test
fun testStopRegionSampler() {
mRegionSampler.stopRegionSampler()
verify(regionSampler).stop()
}
@Test
fun testDump() {
mRegionSampler.dump(pw)
verify(regionSampler).dump(pw)
}
@Test
fun testUpdateColorCallback() {
regionSampler.callback.onRegionDarknessChanged(false)
verify(regionSampler.callback).onRegionDarknessChanged(false)
clearInvocations(regionSampler.callback)
regionSampler.callback.onRegionDarknessChanged(true)
verify(regionSampler.callback).onRegionDarknessChanged(true)
}
@Test
fun testFlagFalse() {
mRegionSampler =
object : RegionSampler(sampledView, mainExecutor, bgExecutor, false, updateFun) {
override fun createRegionSamplingHelper(
sampledView: View,
callback: RegionSamplingHelper.SamplingCallback,
mainExecutor: Executor?,
bgExecutor: Executor?
): RegionSamplingHelper {
return this@RegionSamplerTest.regionSampler
}
}
Assert.assertEquals(mRegionSampler.regionSampler, null)
}
}