/* * Copyright (C) 2022 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.keyguard import android.content.BroadcastReceiver import android.testing.AndroidTestingRunner import android.view.View import android.view.ViewTreeObserver import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.log.core.LogLevel import com.android.systemui.log.core.LogcatOnlyMessageBuffer import com.android.systemui.plugins.clocks.ClockAnimations import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockEvents import com.android.systemui.plugins.clocks.ClockFaceConfig import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.plugins.clocks.ClockFaceEvents import com.android.systemui.plugins.clocks.ClockMessageBuffers import com.android.systemui.plugins.clocks.ClockTickRate import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ZenModeController import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import java.util.TimeZone import java.util.concurrent.Executor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.runBlocking import kotlinx.coroutines.yield import org.junit.Assert.assertEquals 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 import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import com.android.systemui.Flags as AConfigFlags import org.mockito.Mockito.`when` as whenever @RunWith(AndroidTestingRunner::class) @SmallTest class ClockEventControllerTest : SysuiTestCase() { @JvmField @Rule val mockito = MockitoJUnit.rule() @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var batteryController: BatteryController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var animations: ClockAnimations @Mock private lateinit var events: ClockEvents @Mock private lateinit var clock: ClockController @Mock private lateinit var mainExecutor: DelayableExecutor @Mock private lateinit var bgExecutor: Executor @Mock private lateinit var smallClockController: ClockFaceController @Mock private lateinit var smallClockView: View @Mock private lateinit var smallClockViewTreeObserver: ViewTreeObserver @Mock private lateinit var smallClockFrame: FrameLayout @Mock private lateinit var smallClockFrameViewTreeObserver: ViewTreeObserver @Mock private lateinit var largeClockController: ClockFaceController @Mock private lateinit var largeClockView: View @Mock private lateinit var largeClockViewTreeObserver: ViewTreeObserver @Mock private lateinit var smallClockEvents: ClockFaceEvents @Mock private lateinit var largeClockEvents: ClockFaceEvents @Mock private lateinit var parentView: View private lateinit var repository: FakeKeyguardRepository @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor private val messageBuffer = LogcatOnlyMessageBuffer(LogLevel.DEBUG) private val clockBuffers = ClockMessageBuffers(messageBuffer, messageBuffer, messageBuffer) private lateinit var underTest: ClockEventController @Mock private lateinit var zenModeController: ZenModeController @Before fun setUp() { whenever(clock.smallClock).thenReturn(smallClockController) whenever(clock.largeClock).thenReturn(largeClockController) whenever(smallClockController.view).thenReturn(smallClockView) whenever(smallClockView.parent).thenReturn(smallClockFrame) whenever(smallClockView.viewTreeObserver).thenReturn(smallClockViewTreeObserver) whenever(smallClockFrame.viewTreeObserver).thenReturn(smallClockFrameViewTreeObserver) whenever(largeClockController.view).thenReturn(largeClockView) whenever(largeClockView.viewTreeObserver).thenReturn(largeClockViewTreeObserver) whenever(smallClockController.events).thenReturn(smallClockEvents) whenever(largeClockController.events).thenReturn(largeClockEvents) whenever(clock.events).thenReturn(events) whenever(smallClockController.animations).thenReturn(animations) whenever(largeClockController.animations).thenReturn(animations) whenever(smallClockController.config) .thenReturn(ClockFaceConfig(tickRate = ClockTickRate.PER_MINUTE)) whenever(largeClockController.config) .thenReturn(ClockFaceConfig(tickRate = ClockTickRate.PER_MINUTE)) repository = FakeKeyguardRepository() val withDeps = KeyguardInteractorFactory.create( repository = repository, ) withDeps.featureFlags.apply { set(Flags.REGION_SAMPLING, false) } underTest = ClockEventController( withDeps.keyguardInteractor, keyguardTransitionInteractor, broadcastDispatcher, batteryController, keyguardUpdateMonitor, configurationController, context.resources, context, mainExecutor, bgExecutor, clockBuffers, withDeps.featureFlags, zenModeController ) underTest.clock = clock runBlocking(IMMEDIATE) { underTest.registerListeners(parentView) repository.setIsDozing(true) repository.setDozeAmount(1f) } } @Test fun clockSet_validateInitialization() { verify(clock).initialize(any(), anyFloat(), anyFloat()) } @Test fun clockUnset_validateState() { underTest.clock = null assertEquals(underTest.clock, null) } @Test fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) { verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) val captor = argumentCaptor() verify(configurationController).addCallback(capture(captor)) captor.value.onThemeChanged() verify(events).onColorPaletteChanged(any()) } @Test fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) { val captor = argumentCaptor() verify(configurationController).addCallback(capture(captor)) captor.value.onDensityOrFontScaleChanged() verify(smallClockEvents, times(2)).onFontSettingChanged(anyFloat()) verify(largeClockEvents, times(2)).onFontSettingChanged(anyFloat()) } @Test fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) { val batteryCaptor = argumentCaptor() verify(batteryController).addCallback(capture(batteryCaptor)) val keyguardCaptor = argumentCaptor() verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) keyguardCaptor.value.onKeyguardVisibilityChanged(true) batteryCaptor.value.onBatteryLevelChanged(10, false, true) verify(animations, times(2)).charge() } @Test fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() = runBlocking(IMMEDIATE) { val batteryCaptor = argumentCaptor() verify(batteryController).addCallback(capture(batteryCaptor)) val keyguardCaptor = argumentCaptor() verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) keyguardCaptor.value.onKeyguardVisibilityChanged(true) batteryCaptor.value.onBatteryLevelChanged(10, false, true) batteryCaptor.value.onBatteryLevelChanged(10, false, true) verify(animations, times(2)).charge() } @Test fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) { val batteryCaptor = argumentCaptor() verify(batteryController).addCallback(capture(batteryCaptor)) val keyguardCaptor = argumentCaptor() verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) keyguardCaptor.value.onKeyguardVisibilityChanged(false) batteryCaptor.value.onBatteryLevelChanged(10, false, true) verify(animations, never()).charge() } @Test fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) { val batteryCaptor = argumentCaptor() verify(batteryController).addCallback(capture(batteryCaptor)) val keyguardCaptor = argumentCaptor() verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) keyguardCaptor.value.onKeyguardVisibilityChanged(true) batteryCaptor.value.onBatteryLevelChanged(10, false, false) verify(animations, never()).charge() } @Test fun localeCallback_verifyClockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor() verify(broadcastDispatcher) .registerReceiver(capture(captor), any(), eq(null), eq(null), anyInt(), eq(null)) captor.value.onReceive(context, mock()) verify(events).onLocaleChanged(any()) } @Test fun keyguardCallback_visibilityChanged_clockDozeCalled() = runBlocking(IMMEDIATE) { mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) val captor = argumentCaptor() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onKeyguardVisibilityChanged(true) verify(animations, never()).doze(0f) captor.value.onKeyguardVisibilityChanged(false) verify(animations, times(2)).doze(0f) } @Test fun keyguardCallback_timeFormat_clockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onTimeFormatChanged("12h") verify(events).onTimeFormatChanged(false) } @Test fun keyguardCallback_timezoneChanged_clockNotified() = runBlocking(IMMEDIATE) { val mockTimeZone = mock() val captor = argumentCaptor() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onTimeZoneChanged(mockTimeZone) verify(events).onTimeZoneChanged(mockTimeZone) } @Test fun keyguardCallback_userSwitched_clockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onUserSwitchComplete(10) verify(events).onTimeFormatChanged(false) } @Test fun keyguardCallback_verifyKeyguardChanged() = runBlocking(IMMEDIATE) { val job = underTest.listenForDozeAmount(this) repository.setDozeAmount(0.4f) yield() verify(animations, times(2)).doze(0.4f) job.cancel() } @Test fun listenForDozeAmountTransition_updatesClockDozeAmount() = runBlocking(IMMEDIATE) { val transitionStep = MutableStateFlow(TransitionStep()) whenever(keyguardTransitionInteractor.transition(Edge.create(LOCKSCREEN, AOD))) .thenReturn(transitionStep) whenever(keyguardTransitionInteractor.transition(Edge.create(AOD, LOCKSCREEN))) .thenReturn(transitionStep) val job = underTest.listenForDozeAmountTransition(this) transitionStep.value = TransitionStep( from = LOCKSCREEN, to = AOD, value = 0.4f, transitionState = TransitionState.RUNNING, ) yield() verify(animations, times(2)).doze(0.4f) job.cancel() } @Test fun listenForTransitionToAodFromGone_updatesClockDozeAmountToOne() = runBlocking(IMMEDIATE) { val transitionStep = MutableStateFlow(TransitionStep()) whenever(keyguardTransitionInteractor .transition(Edge.create(to = AOD))) .thenReturn(transitionStep) val job = underTest.listenForAnyStateToAodTransition(this) transitionStep.value = TransitionStep( from = GONE, to = AOD, transitionState = TransitionState.STARTED, ) yield() verify(animations, times(2)).doze(1f) job.cancel() } @Test fun listenForTransitionToLSFromOccluded_updatesClockDozeAmountToZero() = runBlocking(IMMEDIATE) { val transitionStep = MutableStateFlow(TransitionStep()) whenever(keyguardTransitionInteractor .transition(Edge.create(to = LOCKSCREEN))) .thenReturn(transitionStep) val job = underTest.listenForAnyStateToLockscreenTransition(this) transitionStep.value = TransitionStep( from = OCCLUDED, to = LOCKSCREEN, transitionState = TransitionState.STARTED, ) yield() verify(animations, times(2)).doze(0f) job.cancel() } @Test fun listenForTransitionToAodFromLockscreen_neverUpdatesClockDozeAmount() = runBlocking(IMMEDIATE) { val transitionStep = MutableStateFlow(TransitionStep()) whenever(keyguardTransitionInteractor .transition(Edge.create(to = AOD))) .thenReturn(transitionStep) val job = underTest.listenForAnyStateToAodTransition(this) transitionStep.value = TransitionStep( from = LOCKSCREEN, to = AOD, transitionState = TransitionState.STARTED, ) yield() verify(animations, never()).doze(1f) job.cancel() } @Test fun listenForAnyStateToLockscreenTransition_neverUpdatesClockDozeAmount() = runBlocking(IMMEDIATE) { val transitionStep = MutableStateFlow(TransitionStep()) whenever(keyguardTransitionInteractor .transition(Edge.create(to = LOCKSCREEN))) .thenReturn(transitionStep) val job = underTest.listenForAnyStateToLockscreenTransition(this) transitionStep.value = TransitionStep( from = AOD, to = LOCKSCREEN, transitionState = TransitionState.STARTED, ) yield() verify(animations, never()).doze(0f) job.cancel() } @Test fun listenForAnyStateToDozingTransition_UpdatesClockDozeAmountToOne() = runBlocking(IMMEDIATE) { val transitionStep = MutableStateFlow(TransitionStep()) whenever(keyguardTransitionInteractor .transition(Edge.create(to = DOZING))) .thenReturn(transitionStep) val job = underTest.listenForAnyStateToDozingTransition(this) transitionStep.value = TransitionStep( from = LOCKSCREEN, to = DOZING, transitionState = TransitionState.STARTED, ) yield() verify(animations, times(2)).doze(1f) job.cancel() } @Test fun unregisterListeners_validate() = runBlocking(IMMEDIATE) { underTest.unregisterListeners() verify(broadcastDispatcher).unregisterReceiver(any()) verify(configurationController).removeCallback(any()) verify(batteryController).removeCallback(any()) verify(keyguardUpdateMonitor).removeCallback(any()) verify(smallClockController.view) .removeOnAttachStateChangeListener(underTest.smallClockOnAttachStateChangeListener) verify(largeClockController.view) .removeOnAttachStateChangeListener(underTest.largeClockOnAttachStateChangeListener) } @Test fun registerOnAttachStateChangeListener_validate() = runBlocking(IMMEDIATE) { verify(smallClockController.view) .addOnAttachStateChangeListener(underTest.smallClockOnAttachStateChangeListener) verify(largeClockController.view) .addOnAttachStateChangeListener(underTest.largeClockOnAttachStateChangeListener) } @Test fun registerAndRemoveOnGlobalLayoutListener_correctly() = runBlocking(IMMEDIATE) { underTest.smallClockOnAttachStateChangeListener!!.onViewAttachedToWindow(smallClockView) verify(smallClockFrame.viewTreeObserver).addOnGlobalLayoutListener(any()) underTest.smallClockOnAttachStateChangeListener!!.onViewDetachedFromWindow( smallClockView ) verify(smallClockFrame.viewTreeObserver).removeOnGlobalLayoutListener(any()) } @Test fun registerOnGlobalLayoutListener_RemoveOnAttachStateChangeListener_correctly() = runBlocking(IMMEDIATE) { underTest.smallClockOnAttachStateChangeListener!!.onViewAttachedToWindow(smallClockView) verify(smallClockFrame.viewTreeObserver).addOnGlobalLayoutListener(any()) underTest.unregisterListeners() verify(smallClockFrame.viewTreeObserver).removeOnGlobalLayoutListener(any()) } companion object { private val IMMEDIATE = Dispatchers.Main.immediate } }