diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/CommonRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/CommonRepositoryModule.kt new file mode 100644 index 0000000000000..9b0c3fa7e92bb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/CommonRepositoryModule.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.common.ui.data.repository + +import dagger.Binds +import dagger.Module + +@Module +interface CommonRepositoryModule { + @Binds fun bindRepository(impl: ConfigurationRepositoryImpl): ConfigurationRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt new file mode 100644 index 0000000000000..3e6ac86e2417a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.common.ui.data.repository + +import android.content.Context +import android.content.res.Configuration +import android.view.DisplayInfo +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.wrapper.DisplayUtilsWrapper +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +interface ConfigurationRepository { + /** Called whenever ui mode, theme or configuration has changed. */ + val onAnyConfigurationChange: Flow + val scaleForResolution: Flow + + fun getResolutionScale(): Float +} + +@ExperimentalCoroutinesApi +@SysUISingleton +class ConfigurationRepositoryImpl +@Inject +constructor( + private val configurationController: ConfigurationController, + private val context: Context, + @Application private val scope: CoroutineScope, + private val displayUtils: DisplayUtilsWrapper, +) : ConfigurationRepository { + private val displayInfo = MutableStateFlow(DisplayInfo()) + + override val onAnyConfigurationChange: Flow = + ConflatedCallbackFlow.conflatedCallbackFlow { + val callback = + object : ConfigurationController.ConfigurationListener { + override fun onUiModeChanged() { + sendUpdate("ConfigurationRepository#onUiModeChanged") + } + + override fun onThemeChanged() { + sendUpdate("ConfigurationRepository#onThemeChanged") + } + + override fun onConfigChanged(newConfig: Configuration) { + sendUpdate("ConfigurationRepository#onConfigChanged") + } + + fun sendUpdate(reason: String) { + trySendWithFailureLogging(Unit, reason) + } + } + configurationController.addCallback(callback) + awaitClose { configurationController.removeCallback(callback) } + } + + private val configurationChange: Flow = + ConflatedCallbackFlow.conflatedCallbackFlow { + val callback = + object : ConfigurationController.ConfigurationListener { + override fun onConfigChanged(newConfig: Configuration) { + trySendWithFailureLogging(Unit, "ConfigurationRepository#onConfigChanged") + } + } + configurationController.addCallback(callback) + awaitClose { configurationController.removeCallback(callback) } + } + + override val scaleForResolution: StateFlow = + configurationChange + .mapLatest { getResolutionScale() } + .distinctUntilChanged() + .stateIn(scope, SharingStarted.WhileSubscribed(), getResolutionScale()) + + override fun getResolutionScale(): Float { + context.display.getDisplayInfo(displayInfo.value) + val maxDisplayMode = + displayUtils.getMaximumResolutionDisplayMode(displayInfo.value.supportedModes) + maxDisplayMode?.let { + val scaleFactor = + displayUtils.getPhysicalPixelDisplaySizeRatio( + maxDisplayMode.physicalWidth, + maxDisplayMode.physicalHeight, + displayInfo.value.naturalWidth, + displayInfo.value.naturalHeight + ) + return if (scaleFactor == Float.POSITIVE_INFINITY) 1f else scaleFactor + } + return 1f + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 8e6e0dd37a900..89c45d7f5e12d 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -39,6 +39,7 @@ import com.android.systemui.biometrics.dagger.BiometricsModule; import com.android.systemui.biometrics.dagger.UdfpsModule; import com.android.systemui.classifier.FalsingModule; import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule; +import com.android.systemui.common.ui.data.repository.CommonRepositoryModule; import com.android.systemui.complication.dagger.ComplicationComponent; import com.android.systemui.controls.dagger.ControlsModule; import com.android.systemui.dagger.qualifiers.Main; @@ -155,6 +156,7 @@ import javax.inject.Named; ClipboardOverlayModule.class, ClockInfoModule.class, ClockRegistryModule.class, + CommonRepositoryModule.class, ConnectivityModule.class, CoroutinesModule.class, DreamModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelperWrapper.kt b/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelperWrapper.kt index d853e04fed908..10578521f1826 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelperWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelperWrapper.kt @@ -24,4 +24,8 @@ class BurnInHelperWrapper @Inject constructor() { fun burnInOffset(amplitude: Int, xAxis: Boolean): Int { return getBurnInOffset(amplitude, xAxis) } + + fun burnInProgressOffset(): Float { + return getBurnInProgressOffset() + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt new file mode 100644 index 0000000000000..252982fd019ff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.content.Context +import androidx.annotation.DimenRes +import com.android.systemui.R +import com.android.systemui.common.ui.data.repository.ConfigurationRepository +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.util.time.SystemClock +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +/** Encapsulates business-logic related to Ambient Display burn-in offsets. */ +@ExperimentalCoroutinesApi +@SysUISingleton +class BurnInInteractor +@Inject +constructor( + private val context: Context, + private val burnInHelperWrapper: BurnInHelperWrapper, + @Application private val scope: CoroutineScope, + private val configurationRepository: ConfigurationRepository, + private val systemClock: SystemClock, +) { + private val _dozeTimeTick = MutableStateFlow(0) + val dozeTimeTick: StateFlow = _dozeTimeTick.asStateFlow() + + val udfpsBurnInXOffset: StateFlow = + burnInOffsetDefinedInPixels(R.dimen.udfps_burn_in_offset_x, isXAxis = true) + val udfpsBurnInYOffset: StateFlow = + burnInOffsetDefinedInPixels(R.dimen.udfps_burn_in_offset_y, isXAxis = false) + val udfpsBurnInProgress: StateFlow = + dozeTimeTick + .mapLatest { burnInHelperWrapper.burnInProgressOffset() } + .stateIn(scope, SharingStarted.Lazily, burnInHelperWrapper.burnInProgressOffset()) + + fun dozeTimeTick() { + _dozeTimeTick.value = systemClock.uptimeMillis() + } + + /** + * Use for max burn-in offsets that are NOT specified in pixels. This flow will recalculate the + * max burn-in offset on any configuration changes. If the max burn-in offset is specified in + * pixels, use [burnInOffsetDefinedInPixels]. + */ + private fun burnInOffset( + @DimenRes maxBurnInOffsetResourceId: Int, + isXAxis: Boolean, + ): StateFlow { + return configurationRepository.onAnyConfigurationChange + .flatMapLatest { + val maxBurnInOffsetPixels = + context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId) + dozeTimeTick.mapLatest { calculateOffset(maxBurnInOffsetPixels, isXAxis) } + } + .stateIn( + scope, + SharingStarted.Lazily, + calculateOffset( + context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId), + isXAxis, + ) + ) + } + + /** + * Use for max burn-in offBurn-in offsets that ARE specified in pixels. This flow will apply the + * a scale for any resolution changes. If the max burn-in offset is specified in dp, use + * [burnInOffset]. + */ + private fun burnInOffsetDefinedInPixels( + @DimenRes maxBurnInOffsetResourceId: Int, + isXAxis: Boolean, + ): StateFlow { + return configurationRepository.scaleForResolution + .flatMapLatest { scale -> + val maxBurnInOffsetPixels = + context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId) + dozeTimeTick.mapLatest { calculateOffset(maxBurnInOffsetPixels, isXAxis, scale) } + } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + calculateOffset( + context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId), + isXAxis, + configurationRepository.getResolutionScale(), + ) + ) + } + + private fun calculateOffset( + maxBurnInOffsetPixels: Int, + isXAxis: Boolean, + scale: Float = 1f + ): Int { + return (burnInHelperWrapper.burnInOffset(maxBurnInOffsetPixels, isXAxis) * scale).toInt() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java index 89c3946939a23..618120d406cb8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java @@ -37,6 +37,7 @@ import com.android.systemui.doze.DozeHost; import com.android.systemui.doze.DozeLog; import com.android.systemui.doze.DozeReceiver; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.domain.interactor.BurnInInteractor; import com.android.systemui.shade.NotificationShadeWindowViewController; import com.android.systemui.shade.ShadeViewController; import com.android.systemui.statusbar.NotificationShadeWindowController; @@ -56,10 +57,12 @@ import java.util.ArrayList; import javax.inject.Inject; +import kotlinx.coroutines.ExperimentalCoroutinesApi; + /** * Implementation of DozeHost for SystemUI. */ -@SysUISingleton +@ExperimentalCoroutinesApi @SysUISingleton public final class DozeServiceHost implements DozeHost { private static final String TAG = "DozeServiceHost"; private final ArrayList mCallbacks = new ArrayList<>(); @@ -89,6 +92,7 @@ public final class DozeServiceHost implements DozeHost { private NotificationShadeWindowViewController mNotificationShadeWindowViewController; private final AuthController mAuthController; private final NotificationIconAreaController mNotificationIconAreaController; + private final BurnInInteractor mBurnInInteractor; private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private ShadeViewController mNotificationPanel; private View mAmbientIndicationContainer; @@ -110,7 +114,8 @@ public final class DozeServiceHost implements DozeHost { NotificationShadeWindowController notificationShadeWindowController, NotificationWakeUpCoordinator notificationWakeUpCoordinator, AuthController authController, - NotificationIconAreaController notificationIconAreaController) { + NotificationIconAreaController notificationIconAreaController, + BurnInInteractor burnInInteractor) { super(); mDozeLog = dozeLog; mPowerManager = powerManager; @@ -129,6 +134,7 @@ public final class DozeServiceHost implements DozeHost { mNotificationWakeUpCoordinator = notificationWakeUpCoordinator; mAuthController = authController; mNotificationIconAreaController = notificationIconAreaController; + mBurnInInteractor = burnInInteractor; mHeadsUpManagerPhone.addListener(mOnHeadsUpChangedListener); } @@ -304,6 +310,7 @@ public final class DozeServiceHost implements DozeHost { if (mAmbientIndicationContainer instanceof DozeReceiver) { ((DozeReceiver) mAmbientIndicationContainer).dozeTimeTick(); } + mBurnInInteractor.dozeTimeTick(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/util/wrapper/DisplayUtilsWrapper.kt b/packages/SystemUI/src/com/android/systemui/util/wrapper/DisplayUtilsWrapper.kt new file mode 100644 index 0000000000000..cfca7f9da6861 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/wrapper/DisplayUtilsWrapper.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.wrapper + +import android.util.DisplayUtils +import android.view.Display +import javax.inject.Inject + +/** Injectable wrapper around `DisplayUtils` functions */ +class DisplayUtilsWrapper @Inject constructor() { + fun getPhysicalPixelDisplaySizeRatio( + physicalWidth: Int, + physicalHeight: Int, + currentWidth: Int, + currentHeight: Int + ): Float { + return DisplayUtils.getPhysicalPixelDisplaySizeRatio( + physicalWidth, + physicalHeight, + currentWidth, + currentHeight + ) + } + + fun getMaximumResolutionDisplayMode(modes: Array?): Display.Mode? { + return DisplayUtils.getMaximumResolutionDisplayMode(modes) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt new file mode 100644 index 0000000000000..a308c8ee38ca3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.common.ui.data.repository + +import android.content.res.Configuration +import android.view.Display +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.util.wrapper.DisplayUtilsWrapper +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class ConfigurationRepositoryImplTest : SysuiTestCase() { + private var displaySizeRatio = 0f + @Mock private lateinit var configurationController: ConfigurationController + @Mock private lateinit var displayUtils: DisplayUtilsWrapper + + private lateinit var testScope: TestScope + private lateinit var underTest: ConfigurationRepositoryImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + setPhysicalPixelDisplaySizeRatio(displaySizeRatio) + + testScope = TestScope() + underTest = + ConfigurationRepositoryImpl( + configurationController, + context, + testScope.backgroundScope, + displayUtils, + ) + } + + @Test + fun onAnyConfigurationChange_updatesOnUiModeChanged() = + testScope.runTest { + val lastAnyConfigurationChange by collectLastValue(underTest.onAnyConfigurationChange) + assertThat(lastAnyConfigurationChange).isNull() + + val configurationCallback = withArgCaptor { + verify(configurationController).addCallback(capture()) + } + + configurationCallback.onUiModeChanged() + runCurrent() + assertThat(lastAnyConfigurationChange).isNotNull() + } + + @Test + fun onAnyConfigurationChange_updatesOnThemeChanged() = + testScope.runTest { + val lastAnyConfigurationChange by collectLastValue(underTest.onAnyConfigurationChange) + assertThat(lastAnyConfigurationChange).isNull() + + val configurationCallback = withArgCaptor { + verify(configurationController).addCallback(capture()) + } + + configurationCallback.onThemeChanged() + runCurrent() + assertThat(lastAnyConfigurationChange).isNotNull() + } + + @Test + fun onAnyConfigurationChange_updatesOnConfigChanged() = + testScope.runTest { + val lastAnyConfigurationChange by collectLastValue(underTest.onAnyConfigurationChange) + assertThat(lastAnyConfigurationChange).isNull() + + val configurationCallback = withArgCaptor { + verify(configurationController).addCallback(capture()) + } + + configurationCallback.onConfigChanged(mock(Configuration::class.java)) + runCurrent() + assertThat(lastAnyConfigurationChange).isNotNull() + } + + @Test + fun onResolutionScale_updatesOnConfigurationChange() = + testScope.runTest { + val scaleForResolution by collectLastValue(underTest.scaleForResolution) + assertThat(scaleForResolution).isEqualTo(displaySizeRatio) + + val configurationCallback = withArgCaptor { + verify(configurationController).addCallback(capture()) + } + + setPhysicalPixelDisplaySizeRatio(2f) + configurationCallback.onConfigChanged(mock(Configuration::class.java)) + assertThat(scaleForResolution).isEqualTo(displaySizeRatio) + + setPhysicalPixelDisplaySizeRatio(.21f) + configurationCallback.onConfigChanged(mock(Configuration::class.java)) + assertThat(scaleForResolution).isEqualTo(displaySizeRatio) + } + + @Test + fun onResolutionScale_nullMaxResolution() = + testScope.runTest { + val scaleForResolution by collectLastValue(underTest.scaleForResolution) + runCurrent() + + givenNullMaxResolutionDisplayMode() + val configurationCallback = withArgCaptor { + verify(configurationController).addCallback(capture()) + } + configurationCallback.onConfigChanged(mock(Configuration::class.java)) + assertThat(scaleForResolution).isEqualTo(1f) + } + + @Test + fun getResolutionScale_nullMaxResolutionDisplayMode() { + givenNullMaxResolutionDisplayMode() + assertThat(underTest.getResolutionScale()).isEqualTo(1f) + } + + @Test + fun getResolutionScale_infiniteDisplayRatios() { + setPhysicalPixelDisplaySizeRatio(Float.POSITIVE_INFINITY) + assertThat(underTest.getResolutionScale()).isEqualTo(1f) + } + + @Test + fun getResolutionScale_differentDisplayRatios() { + setPhysicalPixelDisplaySizeRatio(.5f) + assertThat(underTest.getResolutionScale()).isEqualTo(displaySizeRatio) + + setPhysicalPixelDisplaySizeRatio(.283f) + assertThat(underTest.getResolutionScale()).isEqualTo(displaySizeRatio) + + setPhysicalPixelDisplaySizeRatio(3.58f) + assertThat(underTest.getResolutionScale()).isEqualTo(displaySizeRatio) + + setPhysicalPixelDisplaySizeRatio(0f) + assertThat(underTest.getResolutionScale()).isEqualTo(displaySizeRatio) + + setPhysicalPixelDisplaySizeRatio(1f) + assertThat(underTest.getResolutionScale()).isEqualTo(displaySizeRatio) + } + + private fun givenNullMaxResolutionDisplayMode() { + whenever(displayUtils.getMaximumResolutionDisplayMode(any())).thenReturn(null) + } + + private fun setPhysicalPixelDisplaySizeRatio(ratio: Float) { + displaySizeRatio = ratio + whenever(displayUtils.getMaximumResolutionDisplayMode(any())) + .thenReturn(Display.Mode(0, 0, 0, 90f)) + whenever( + displayUtils.getPhysicalPixelDisplaySizeRatio( + anyInt(), + anyInt(), + anyInt(), + anyInt() + ) + ) + .thenReturn(ratio) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt new file mode 100644 index 0000000000000..b2a1668df7aaa --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.common.ui.data.repository + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow + +class FakeConfigurationRepository : ConfigurationRepository { + private val onAnyConfigurationChangeChannel = Channel() + override val onAnyConfigurationChange: Flow = + onAnyConfigurationChangeChannel.receiveAsFlow() + + private val _scaleForResolution = MutableStateFlow(1f) + override val scaleForResolution: Flow = _scaleForResolution.asStateFlow() + + suspend fun onAnyConfigurationChange() { + onAnyConfigurationChangeChannel.send(Unit) + } + + fun setScaleForResolution(scale: Float) { + _scaleForResolution.value = scale + } + + override fun getResolutionScale(): Float { + return _scaleForResolution.value + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorTest.kt new file mode 100644 index 0000000000000..069a4862ccdcc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import junit.framework.Assert.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class BurnInInteractorTest : SysuiTestCase() { + private val burnInOffset = 7 + private var burnInProgress = 0f + + @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper + + private lateinit var configurationRepository: FakeConfigurationRepository + private lateinit var systemClock: FakeSystemClock + private lateinit var testScope: TestScope + private lateinit var underTest: BurnInInteractor + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + configurationRepository = FakeConfigurationRepository() + systemClock = FakeSystemClock() + + whenever(burnInHelperWrapper.burnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset) + setBurnInProgress(.65f) + + testScope = TestScope() + underTest = + BurnInInteractor( + context, + burnInHelperWrapper, + testScope.backgroundScope, + configurationRepository, + systemClock, + ) + } + + @Test + fun dozeTimeTick_updatesOnDozeTimeTick() = + testScope.runTest { + // Initial state set to 0 + val lastDozeTimeTick by collectLastValue(underTest.dozeTimeTick) + assertEquals(0L, lastDozeTimeTick) + + // WHEN dozeTimeTick updated + incrementUptimeMillis() + underTest.dozeTimeTick() + + // THEN listeners were updated to the latest uptime millis + assertThat(systemClock.uptimeMillis()).isEqualTo(lastDozeTimeTick) + } + + @Test + fun udfpsBurnInOffset_updatesOnResolutionScaleChange() = + testScope.runTest { + val udfpsBurnInOffsetX by collectLastValue(underTest.udfpsBurnInXOffset) + val udfpsBurnInOffsetY by collectLastValue(underTest.udfpsBurnInYOffset) + assertThat(udfpsBurnInOffsetX).isEqualTo(burnInOffset) + assertThat(udfpsBurnInOffsetY).isEqualTo(burnInOffset) + + configurationRepository.setScaleForResolution(3f) + assertThat(udfpsBurnInOffsetX).isEqualTo(burnInOffset * 3) + assertThat(udfpsBurnInOffsetY).isEqualTo(burnInOffset * 3) + + configurationRepository.setScaleForResolution(.5f) + assertThat(udfpsBurnInOffsetX).isEqualTo(burnInOffset / 2) + assertThat(udfpsBurnInOffsetY).isEqualTo(burnInOffset / 2) + } + + @Test + fun udfpsBurnInProgress_updatesOnDozeTimeTick() = + testScope.runTest { + val udfpsBurnInProgress by collectLastValue(underTest.udfpsBurnInProgress) + assertThat(udfpsBurnInProgress).isEqualTo(burnInProgress) + + setBurnInProgress(.88f) + incrementUptimeMillis() + underTest.dozeTimeTick() + assertThat(udfpsBurnInProgress).isEqualTo(burnInProgress) + + setBurnInProgress(.92f) + incrementUptimeMillis() + underTest.dozeTimeTick() + assertThat(udfpsBurnInProgress).isEqualTo(burnInProgress) + + setBurnInProgress(.32f) + incrementUptimeMillis() + underTest.dozeTimeTick() + assertThat(udfpsBurnInProgress).isEqualTo(burnInProgress) + } + + private fun incrementUptimeMillis() { + systemClock.setUptimeMillis(systemClock.uptimeMillis() + 5) + } + + private fun setBurnInProgress(progress: Float) { + burnInProgress = progress + whenever(burnInHelperWrapper.burnInProgressOffset()).thenReturn(burnInProgress) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java index 2831d2fad02a2..163369f0412fa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java @@ -41,6 +41,7 @@ import com.android.systemui.biometrics.AuthController; import com.android.systemui.doze.DozeHost; import com.android.systemui.doze.DozeLog; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.domain.interactor.BurnInInteractor; import com.android.systemui.shade.NotificationShadeWindowViewController; import com.android.systemui.shade.ShadeViewController; import com.android.systemui.statusbar.NotificationShadeWindowController; @@ -92,6 +93,7 @@ public class DozeServiceHostTest extends SysuiTestCase { @Mock private BiometricUnlockController mBiometricUnlockController; @Mock private AuthController mAuthController; @Mock private DozeHost.Callback mCallback; + @Mock private BurnInInteractor mBurnInInteractor; @Before public void setup() { @@ -102,7 +104,8 @@ public class DozeServiceHostTest extends SysuiTestCase { () -> mAssistManager, mDozeScrimController, mKeyguardUpdateMonitor, mPulseExpansionHandler, mNotificationShadeWindowController, mNotificationWakeUpCoordinator, - mAuthController, mNotificationIconAreaController); + mAuthController, mNotificationIconAreaController, + mBurnInInteractor); mDozeServiceHost.initialize( mCentralSurfaces, @@ -213,4 +216,12 @@ public class DozeServiceHostTest extends SysuiTestCase { assertFalse(mDozeServiceHost.isPulsePending()); verify(mDozeScrimController).pulseOutNow(); } + @Test + public void dozeTimeTickSentTBurnInInteractor() { + // WHEN dozeTimeTick + mDozeServiceHost.dozeTimeTick(); + + // THEN burnInInteractor's dozeTimeTick is updated + verify(mBurnInInteractor).dozeTimeTick(); + } }