From 215a1f768bd1d337f15da5d16fa0a7dae61c1ddb Mon Sep 17 00:00:00 2001 From: Matt Pietal Date: Wed, 28 Sep 2022 18:46:55 +0000 Subject: [PATCH 1/2] [DO NOT MERGE] Rewire dozing for clock events Continuing exploration of moving all doze events to flowables, including both isDozing and the dozeAmount animations. At the moment, the source of these events still comes from StatusBarStateController, which informs the KeyguardRepository. Next up is to create a parallel way to drive these animations that is solely based on repository information. Bug: 242853098 Test: atest ClockEventControllerTest KeyguardClockSwitchControllerTest Change-Id: I8c40076ad7e716093f602e5bbf5d9f03871ac679 --- .../android/keyguard/ClockEventController.kt | 86 +++++++---- .../KeyguardClockSwitchController.java | 2 +- .../keyguard/ClockEventControllerTest.kt | 144 ++++++++---------- .../KeyguardClockSwitchControllerTest.java | 2 +- 4 files changed, 126 insertions(+), 108 deletions(-) diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 9151238499dc3..55f8bea4d0569 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -23,12 +23,17 @@ import android.content.res.Resources import android.text.format.DateFormat import android.util.TypedValue import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags -import com.android.systemui.plugins.ClockController -import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.flags.Flags.REGION_SAMPLING +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.plugins.Clock import com.android.systemui.shared.regionsampling.RegionSamplingInstance import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback @@ -38,13 +43,19 @@ 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.launch /** * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by * [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController]. */ open class ClockEventController @Inject constructor( - private val statusBarStateController: StatusBarStateController, + private val keyguardInteractor: KeyguardInteractor, private val broadcastDispatcher: BroadcastDispatcher, private val batteryController: BatteryController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, @@ -53,7 +64,7 @@ open class ClockEventController @Inject constructor( private val context: Context, @Main private val mainExecutor: Executor, @Background private val bgExecutor: Executor, - private val featureFlags: FeatureFlags, + private val featureFlags: FeatureFlags ) { var clock: ClockController? = null set(value) { @@ -70,9 +81,9 @@ open class ClockEventController @Inject constructor( private var isCharging = false private var dozeAmount = 0f private var isKeyguardVisible = false - - private val regionSamplingEnabled = - featureFlags.isEnabled(com.android.systemui.flags.Flags.REGION_SAMPLING) + private var isRegistered = false + private var disposableHandle: DisposableHandle? = null + private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING) private fun updateColors() { if (regionSamplingEnabled && smallRegionSampler != null && largeRegionSampler != null) { @@ -165,15 +176,6 @@ open class ClockEventController @Inject constructor( } } - private val statusBarStateListener = object : StatusBarStateController.StateListener { - override fun onDozeAmountChanged(linear: Float, eased: Float) { - clock?.animations?.doze(linear) - - isDozing = linear > dozeAmount - dozeAmount = linear - } - } - private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() { override fun onKeyguardVisibilityChanged(visible: Boolean) { isKeyguardVisible = visible @@ -195,13 +197,11 @@ open class ClockEventController @Inject constructor( } } - init { - isDozing = statusBarStateController.isDozing - } - - fun registerListeners() { - dozeAmount = statusBarStateController.dozeAmount - isDozing = statusBarStateController.isDozing || dozeAmount != 0f + fun registerListeners(parent: View) { + if (isRegistered) { + return + } + isRegistered = true broadcastDispatcher.registerReceiver( localeBroadcastReceiver, @@ -210,17 +210,27 @@ open class ClockEventController @Inject constructor( configurationController.addCallback(configListener) batteryController.addCallback(batteryCallback) keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) - statusBarStateController.addCallback(statusBarStateListener) smallRegionSampler?.startRegionSampler() largeRegionSampler?.startRegionSampler() + disposableHandle = parent.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + listenForDozing(this) + listenForDozeAmount(this) + } + } } fun unregisterListeners() { + if (!isRegistered) { + return + } + isRegistered = false + + disposableHandle?.dispose() broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver) configurationController.removeCallback(configListener) batteryController.removeCallback(batteryCallback) keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback) - statusBarStateController.removeCallback(statusBarStateListener) smallRegionSampler?.stopRegionSampler() largeRegionSampler?.stopRegionSampler() } @@ -235,8 +245,28 @@ open class ClockEventController @Inject constructor( largeRegionSampler?.dump(pw) } - companion object { - private val TAG = ClockEventController::class.simpleName - private const val FORMAT_NUMBER = 1234567890 + @VisibleForTesting + internal suspend fun listenForDozeAmount(scope: CoroutineScope): Job { + return scope.launch { + keyguardInteractor.dozeAmount.collect { + dozeAmount = it + clock?.animations?.doze(dozeAmount) + } + } + } + + @VisibleForTesting + internal suspend fun listenForDozing(scope: CoroutineScope): Job { + return scope.launch { + combine ( + keyguardInteractor.dozeAmount, + keyguardInteractor.isDozing, + ) { localDozeAmount, localIsDozing -> + localDozeAmount > dozeAmount || localIsDozing + } + .collect { localIsDozing -> + isDozing = localIsDozing + } + } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index b450ec35db322..202134b699d7a 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -164,7 +164,7 @@ public class KeyguardClockSwitchController extends ViewController() verify(configurationController).addCallback(capture(captor)) captor.value.onThemeChanged() @@ -131,13 +144,11 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun fontChanged_verifyFontSizeUpdated() { + fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) { clockEventController.clock = clock verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) - clockEventController.registerListeners() - val captor = argumentCaptor() verify(configurationController).addCallback(capture(captor)) captor.value.onDensityOrFontScaleChanged() @@ -146,10 +157,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) { val batteryCaptor = argumentCaptor() verify(batteryController).addCallback(capture(batteryCaptor)) val keyguardCaptor = argumentCaptor() @@ -161,26 +169,21 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() + 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) - 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(1)).charge() - } + verify(animations, times(1)).charge() + } @Test - fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) { val batteryCaptor = argumentCaptor() verify(batteryController).addCallback(capture(batteryCaptor)) val keyguardCaptor = argumentCaptor() @@ -192,25 +195,20 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() + 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) - 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() - } + verify(animations, never()).charge() + } @Test - fun localeCallback_verifyClockNotified() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun localeCallback_verifyClockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor() verify(broadcastDispatcher).registerReceiver( capture(captor), any(), eq(null), eq(null), anyInt(), eq(null) @@ -221,10 +219,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_visibilityChanged_clockDozeCalled() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun keyguardCallback_visibilityChanged_clockDozeCalled() = runBlocking(IMMEDIATE) { val captor = argumentCaptor() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) @@ -236,10 +231,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_timeFormat_clockNotified() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun keyguardCallback_timeFormat_clockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onTimeFormatChanged("12h") @@ -248,11 +240,8 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_timezoneChanged_clockNotified() { + fun keyguardCallback_timezoneChanged_clockNotified() = runBlocking(IMMEDIATE) { val mockTimeZone = mock() - clockEventController.clock = clock - clockEventController.registerListeners() - val captor = argumentCaptor() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onTimeZoneChanged(mockTimeZone) @@ -261,10 +250,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_userSwitched_clockNotified() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun keyguardCallback_userSwitched_clockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onUserSwitchComplete(10) @@ -273,25 +259,27 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_verifyKeyguardChanged() { - clockEventController.clock = clock - clockEventController.registerListeners() + fun keyguardCallback_verifyKeyguardChanged() = runBlocking(IMMEDIATE) { + val job = underTest.listenForDozeAmount(this) + repository.setDozeAmount(0.4f) - val captor = argumentCaptor() - verify(statusBarStateController).addCallback(capture(captor)) - captor.value.onDozeAmountChanged(0.4f, 0.6f) + yield() verify(animations).doze(0.4f) + + job.cancel() } @Test - fun unregisterListeners_validate() { - clockEventController.clock = clock - clockEventController.unregisterListeners() + fun unregisterListeners_validate() = runBlocking(IMMEDIATE) { + underTest.unregisterListeners() verify(broadcastDispatcher).unregisterReceiver(any()) verify(configurationController).removeCallback(any()) verify(batteryController).removeCallback(any()) verify(keyguardUpdateMonitor).removeCallback(any()) - verify(statusBarStateController).removeCallback(any()) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index 400caa3a352a4..bb03a47e025c7 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -265,6 +265,6 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { private void verifyAttachment(VerificationMode times) { verify(mClockRegistry, times).registerClockChangeListener( any(ClockRegistry.ClockChangeListener.class)); - verify(mClockEventController, times).registerListeners(); + verify(mClockEventController, times).registerListeners(mView); } } From 8c49232ba654627b7ddb2b8b4d1c1c123236bdd6 Mon Sep 17 00:00:00 2001 From: Matt Pietal Date: Thu, 13 Oct 2022 14:54:39 +0000 Subject: [PATCH 2/2] [DO NOT MERGE] Add KeyguardTransitionRepository Added in order to better control transitions within keyguard. Transitions must begin with a call to startTransition and can be either animated or manually controlled. The domain layer is responsible for both triggering these transitions and listening to flowable events emitted by this repository, and should respond by updating the UI elements. Bug: 195430376 Test: atest KeyguardTransitionRepositoryTest ClockEventControllerTest Change-Id: I56bd9d560db265c523fa3e0b8c03310e9f4067de --- .../android/keyguard/ClockEventController.kt | 20 +- .../keyguard/dagger/KeyguardModule.java | 2 + .../data/repository/KeyguardRepository.kt | 31 +++ .../KeyguardTransitionRepository.kt | 169 ++++++++++++ .../AodLockscreenTransitionInteractor.kt | 77 ++++++ .../domain/interactor/KeyguardInteractor.kt | 2 +- .../KeyguardTransitionCoreStartable.kt | 49 ++++ .../KeyguardTransitionInteractor.kt | 37 +++ .../LockscreenBouncerTransitionInteractor.kt | 98 +++++++ .../StartKeyguardTransitionModule.kt | 42 +++ .../domain/interactor/TransitionInteractor.kt | 32 +++ .../keyguard/shared/model/KeyguardState.kt | 34 +++ .../keyguard/shared/model/StatusBarState.kt | 23 ++ .../keyguard/shared/model/TransitionInfo.kt | 35 +++ .../keyguard/shared/model/TransitionState.kt | 24 ++ .../keyguard/shared/model/TransitionStep.kt | 24 ++ .../shade/data/repository/ShadeRepository.kt | 62 +++++ .../systemui/shade/domain/model/ShadeModel.kt | 28 ++ .../keyguard/ClockEventControllerTest.kt | 7 +- .../KeyguardTransitionRepositoryTest.kt | 259 ++++++++++++++++++ .../data/repository/FakeKeyguardRepository.kt | 4 + 21 files changed, 1052 insertions(+), 7 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt create mode 100644 packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt create mode 100644 packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 55f8bea4d0569..910955a45f7b7 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -32,8 +32,9 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags.REGION_SAMPLING import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.plugins.Clock +import com.android.systemui.plugins.ClockController import com.android.systemui.shared.regionsampling.RegionSamplingInstance import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback @@ -56,6 +57,7 @@ import kotlinx.coroutines.launch */ open class ClockEventController @Inject constructor( private val keyguardInteractor: KeyguardInteractor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val broadcastDispatcher: BroadcastDispatcher, private val batteryController: BatteryController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, @@ -216,6 +218,7 @@ open class ClockEventController @Inject constructor( repeatOnLifecycle(Lifecycle.State.STARTED) { listenForDozing(this) listenForDozeAmount(this) + listenForDozeAmountTransition(this) } } } @@ -246,7 +249,7 @@ open class ClockEventController @Inject constructor( } @VisibleForTesting - internal suspend fun listenForDozeAmount(scope: CoroutineScope): Job { + internal fun listenForDozeAmount(scope: CoroutineScope): Job { return scope.launch { keyguardInteractor.dozeAmount.collect { dozeAmount = it @@ -256,7 +259,18 @@ open class ClockEventController @Inject constructor( } @VisibleForTesting - internal suspend fun listenForDozing(scope: CoroutineScope): Job { + internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job { + return scope.launch { + keyguardTransitionInteractor.aodToLockscreenTransition.collect { + // Would eventually run this: + // dozeAmount = it.value + // clock?.animations?.doze(dozeAmount) + } + } + } + + @VisibleForTesting + internal fun listenForDozing(scope: CoroutineScope): Job { return scope.launch { combine ( keyguardInteractor.dozeAmount, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 56f1ac46a8758..56a1f1ae936e5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -43,6 +43,7 @@ import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; +import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule; import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.statusbar.NotificationShadeDepthController; @@ -72,6 +73,7 @@ import dagger.Provides; FalsingModule.class, KeyguardQuickAffordanceModule.class, KeyguardRepositoryModule.class, + StartKeyguardTransitionModule.class, }) public class KeyguardModule { /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 45b668e609eae..b186ae0ceec4d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -21,6 +21,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.common.shared.model.Position import com.android.systemui.dagger.SysUISingleton import com.android.systemui.doze.DozeHost +import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject @@ -85,6 +86,9 @@ interface KeyguardRepository { */ val dozeAmount: Flow + /** Observable for the [StatusBarState] */ + val statusBarState: Flow + /** * Returns `true` if the keyguard is showing; `false` otherwise. * @@ -185,6 +189,24 @@ constructor( return keyguardStateController.isShowing } + override val statusBarState: Flow = conflatedCallbackFlow { + val callback = + object : StatusBarStateController.StateListener { + override fun onStateChanged(state: Int) { + trySendWithFailureLogging(statusBarStateIntToObject(state), TAG, "state") + } + } + + statusBarStateController.addCallback(callback) + trySendWithFailureLogging( + statusBarStateIntToObject(statusBarStateController.getState()), + TAG, + "initial state" + ) + + awaitClose { statusBarStateController.removeCallback(callback) } + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.value = animate } @@ -197,6 +219,15 @@ constructor( _clockPosition.value = Position(x, y) } + private fun statusBarStateIntToObject(value: Int): StatusBarState { + return when (value) { + 0 -> StatusBarState.SHADE + 1 -> StatusBarState.KEYGUARD + 2 -> StatusBarState.SHADE_LOCKED + else -> throw IllegalArgumentException("Invalid StatusBarState value: $value") + } + } + companion object { private const val TAG = "KeyguardRepositoryImpl" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt new file mode 100644 index 0000000000000..e8532ecfdc375 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -0,0 +1,169 @@ +/* + * 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.systemui.keyguard.data.repository + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.animation.ValueAnimator.AnimatorUpdateListener +import android.annotation.FloatRange +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter + +@SysUISingleton +class KeyguardTransitionRepository @Inject constructor() { + /* + * Each transition between [KeyguardState]s will have an associated Flow. + * In order to collect these events, clients should call [transition]. + */ + private val _transitions = MutableStateFlow(TransitionStep()) + val transitions = _transitions.asStateFlow() + + /* Information about the active transition. */ + private var currentTransitionInfo: TransitionInfo? = null + /* + * When manual control of the transition is requested, a unique [UUID] is used as the handle + * to permit calls to [updateTransition] + */ + private var updateTransitionId: UUID? = null + + /** + * Interactors that require information about changes between [KeyguardState]s will call this to + * register themselves for flowable [TransitionStep]s when that transition occurs. + */ + fun transition(from: KeyguardState, to: KeyguardState): Flow { + return transitions.filter { step -> step.from == from && step.to == to } + } + + /** + * Begin a transition from one state to another. The [info.from] must match + * [currentTransitionInfo.to], or the request will be denied. This is enforced to avoid + * unplanned transitions. + */ + fun startTransition(info: TransitionInfo): UUID? { + if (currentTransitionInfo != null) { + // Open questions: + // * Queue of transitions? buffer of 1? + // * Are transitions cancellable if a new one is triggered? + // * What validation does this need to do? + Log.wtf(TAG, "Transition still active: $currentTransitionInfo") + return null + } + currentTransitionInfo?.animator?.cancel() + + currentTransitionInfo = info + info.animator?.let { animator -> + // An animator was provided, so use it to run the transition + animator.setFloatValues(0f, 1f) + val updateListener = + object : AnimatorUpdateListener { + override fun onAnimationUpdate(animation: ValueAnimator) { + emitTransition( + info, + (animation.getAnimatedValue() as Float), + TransitionState.RUNNING + ) + } + } + val adapter = + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + Log.i(TAG, "Starting transition: $info") + emitTransition(info, 0f, TransitionState.STARTED) + } + override fun onAnimationCancel(animation: Animator) { + Log.i(TAG, "Cancelling transition: $info") + } + override fun onAnimationEnd(animation: Animator) { + Log.i(TAG, "Ending transition: $info") + emitTransition(info, 1f, TransitionState.FINISHED) + animator.removeListener(this) + animator.removeUpdateListener(updateListener) + } + } + animator.addListener(adapter) + animator.addUpdateListener(updateListener) + animator.start() + return@startTransition null + } + ?: run { + Log.i(TAG, "Starting transition (manual): $info") + emitTransition(info, 0f, TransitionState.STARTED) + + // No animator, so it's manual. Provide a mechanism to callback + updateTransitionId = UUID.randomUUID() + return@startTransition updateTransitionId + } + } + + /** + * Allows manual control of a transition. When calling [startTransition], the consumer must pass + * in a null animator. In return, it will get a unique [UUID] that will be validated to allow + * further updates. + * + * When the transition is over, TransitionState.FINISHED must be passed into the [state] + * parameter. + */ + fun updateTransition( + transitionId: UUID, + @FloatRange(from = 0.0, to = 1.0) value: Float, + state: TransitionState + ) { + if (updateTransitionId != transitionId) { + Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId") + return + } + + if (currentTransitionInfo == null) { + Log.wtf(TAG, "Attempting to update with null 'currentTransitionInfo'") + return + } + + currentTransitionInfo?.let { info -> + if (state == TransitionState.FINISHED) { + updateTransitionId = null + Log.i(TAG, "Ending transition: $info") + } + + emitTransition(info, value, state) + } + } + + private fun emitTransition( + info: TransitionInfo, + value: Float, + transitionState: TransitionState + ) { + if (transitionState == TransitionState.FINISHED) { + currentTransitionInfo = null + } + _transitions.value = TransitionStep(info.from, info.to, value, transitionState) + } + + companion object { + private const val TAG = "KeyguardTransitionRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt new file mode 100644 index 0000000000000..400376663f1af --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt @@ -0,0 +1,77 @@ +/* + * 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.systemui.keyguard.domain.interactor + +import android.animation.ValueAnimator +import com.android.systemui.animation.Interpolators +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@SysUISingleton +class AodLockscreenTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardRepository: KeyguardRepository, + private val keyguardTransitionRepository: KeyguardTransitionRepository, +) : TransitionInteractor("AOD<->LOCKSCREEN") { + + override fun start() { + scope.launch { + keyguardRepository.isDozing.collect { isDozing -> + if (isDozing) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.LOCKSCREEN, + KeyguardState.AOD, + getAnimator(), + ) + ) + } else { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + getAnimator(), + ) + ) + } + } + } + } + + private fun getAnimator(): ValueAnimator { + return ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(TRANSITION_DURATION_MS) + } + } + + companion object { + private const val TRANSITION_DURATION_MS = 500L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 192919e32cf64..fc2269c6b01ca 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -38,7 +38,7 @@ constructor( val dozeAmount: Flow = repository.dozeAmount /** Whether the system is in doze mode. */ val isDozing: Flow = repository.isDozing - /** Whether the keyguard is showing ot not. */ + /** Whether the keyguard is showing to not. */ val isKeyguardShowing: Flow = repository.isKeyguardShowing fun isKeyguardShowing(): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt new file mode 100644 index 0000000000000..b166681433a8b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt @@ -0,0 +1,49 @@ +/* + * 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.systemui.keyguard.domain.interactor + +import android.util.Log +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import java.util.Set +import javax.inject.Inject + +@SysUISingleton +class KeyguardTransitionCoreStartable +@Inject +constructor( + private val interactors: Set, +) : CoreStartable { + + override fun start() { + // By listing the interactors in a when, the compiler will help enforce all classes + // extending the sealed class [TransitionInteractor] will be initialized. + interactors.forEach { + // `when` needs to be an expression in order for the compiler to enforce it being + // exhaustive + val ret = + when (it) { + is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it") + is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it") + } + it.start() + } + } + + companion object { + private const val TAG = "KeyguardTransitionCoreStartable" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt new file mode 100644 index 0000000000000..59bb227869175 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -0,0 +1,37 @@ +/* + * 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.systemui.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionStep +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/** Encapsulates business-logic related to the keyguard transitions. */ +@SysUISingleton +class KeyguardTransitionInteractor +@Inject +constructor( + repository: KeyguardTransitionRepository, +) { + /** AOD->LOCKSCREEN transition information. */ + val aodToLockscreenTransition: Flow = repository.transition(AOD, LOCKSCREEN) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt new file mode 100644 index 0000000000000..3c2a12e3836a4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt @@ -0,0 +1,98 @@ +/* + * 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.systemui.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.shade.data.repository.ShadeRepository +import com.android.systemui.util.kotlin.sample +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +@SysUISingleton +class LockscreenBouncerTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardRepository: KeyguardRepository, + private val shadeRepository: ShadeRepository, + private val keyguardTransitionRepository: KeyguardTransitionRepository, +) : TransitionInteractor("LOCKSCREEN<->BOUNCER") { + + private var transitionId: UUID? = null + + override fun start() { + scope.launch { + shadeRepository.shadeModel.sample( + combine( + keyguardTransitionRepository.transitions, + keyguardRepository.statusBarState, + ) { transitions, statusBarState -> + Pair(transitions, statusBarState) + } + ) { shadeModel, pair -> + val (transitions, statusBarState) = pair + + val id = transitionId + if (id != null) { + // An existing `id` means a transition is started, and calls to + // `updateTransition` will control it until FINISHED + keyguardTransitionRepository.updateTransition( + id, + shadeModel.expansionAmount, + if (shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f) { + transitionId = null + TransitionState.FINISHED + } else { + TransitionState.RUNNING + } + ) + } else { + // TODO (b/251849525): Remove statusbarstate check when that state is integrated + // into KeyguardTransitionRepository + val isOnLockscreen = + transitions.transitionState == TransitionState.FINISHED && + transitions.to == KeyguardState.LOCKSCREEN + if ( + isOnLockscreen && + shadeModel.isUserDragging && + statusBarState != SHADE_LOCKED + ) { + transitionId = + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = name, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.BOUNCER, + animator = null, + ) + ) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt new file mode 100644 index 0000000000000..74c542c0043f6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt @@ -0,0 +1,42 @@ +/* + * 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.systemui.keyguard.domain.interactor + +import com.android.systemui.CoreStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import dagger.multibindings.IntoSet + +@Module +abstract class StartKeyguardTransitionModule { + + @Binds + @IntoMap + @ClassKey(KeyguardTransitionCoreStartable::class) + abstract fun bind(impl: KeyguardTransitionCoreStartable): CoreStartable + + @Binds + @IntoSet + abstract fun lockscreenBouncer( + impl: LockscreenBouncerTransitionInteractor + ): TransitionInteractor + + @Binds + @IntoSet + abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt new file mode 100644 index 0000000000000..a2a46d9e3a71c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt @@ -0,0 +1,32 @@ +/* + * 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.systemui.keyguard.domain.interactor +/** + * Each TransitionInteractor is responsible for determining under which conditions to notify + * [KeyguardTransitionRepository] to signal a transition. When (and if) the transition occurs is + * determined by [KeyguardTransitionRepository]. + * + * [name] field should be a unique identifiable string representing this state, used primarily for + * logging + * + * MUST list implementing classes in dagger module [StartKeyguardTransitionModule] and also in the + * 'when' clause of [KeyguardTransitionCoreStartable] + */ +sealed class TransitionInteractor(val name: String) { + + abstract fun start() +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt new file mode 100644 index 0000000000000..f66d5d3650c8d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt @@ -0,0 +1,34 @@ +/* + * 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.systemui.keyguard.shared.model + +/** List of all possible states to transition to/from */ +enum class KeyguardState { + /** For initialization only */ + NONE, + /* Always-on Display. The device is in a low-power mode with a minimal UI visible */ + AOD, + /* + * The security screen prompt UI, containing PIN, Password, Pattern, and all FPS + * (Fingerprint Sensor) variations, for the user to verify their credentials + */ + BOUNCER, + /* + * Device is actively displaying keyguard UI and is not in low-power mode. Device may be + * unlocked if SWIPE security method is used, or if face lockscreen bypass is false. + */ + LOCKSCREEN, +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt new file mode 100644 index 0000000000000..bb953477583d9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt @@ -0,0 +1,23 @@ +/* + * 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.systemui.keyguard.shared.model + +/** See [com.android.systemui.statusbar.StatusBarState] for definitions */ +enum class StatusBarState { + SHADE, + KEYGUARD, + SHADE_LOCKED, +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt new file mode 100644 index 0000000000000..bfccf3fe076cb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt @@ -0,0 +1,35 @@ +/* + * 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.systemui.keyguard.shared.model + +import android.animation.ValueAnimator + +/** Tracks who is controlling the current transition, and how to run it. */ +data class TransitionInfo( + val ownerName: String, + val from: KeyguardState, + val to: KeyguardState, + val animator: ValueAnimator?, // 'null' animator signal manual control +) { + override fun toString(): String = + "TransitionInfo(ownerName=$ownerName, from=$from, to=$to, " + + (if (animator != null) { + "animated" + } else { + "manual" + }) + + ")" +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt new file mode 100644 index 0000000000000..d8691c17f53db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt @@ -0,0 +1,24 @@ +/* + * 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.systemui.keyguard.shared.model + +/** Possible states for a running transition between [State] */ +enum class TransitionState { + NONE, + STARTED, + RUNNING, + FINISHED +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt new file mode 100644 index 0000000000000..688ec912aac8c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt @@ -0,0 +1,24 @@ +/* + * 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.systemui.keyguard.shared.model + +/** This information will flow from the [KeyguardTransitionRepository] to control the UI layer */ +data class TransitionStep( + val from: KeyguardState = KeyguardState.NONE, + val to: KeyguardState = KeyguardState.NONE, + val value: Float = 0f, // constrained [0.0, 1.0] + val transitionState: TransitionState = TransitionState.NONE, +) diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt new file mode 100644 index 0000000000000..09019a69df47d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt @@ -0,0 +1,62 @@ +/* + * 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.systemui.shade.data.repository + +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shade.ShadeExpansionChangeEvent +import com.android.systemui.shade.ShadeExpansionListener +import com.android.systemui.shade.ShadeExpansionStateManager +import com.android.systemui.shade.domain.model.ShadeModel +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged + +/** Business logic for shade interactions */ +@SysUISingleton +class ShadeRepository @Inject constructor(shadeExpansionStateManager: ShadeExpansionStateManager) { + + val shadeModel: Flow = + conflatedCallbackFlow { + val callback = + object : ShadeExpansionListener { + override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) { + // Don't propagate ShadeExpansionChangeEvent.dragDownPxAmount field. + // It is too noisy and produces extra events that consumers won't care + // about + val info = + ShadeModel( + expansionAmount = event.fraction, + isExpanded = event.expanded, + isUserDragging = event.tracking + ) + trySendWithFailureLogging(info, TAG, "updated shade expansion info") + } + } + + shadeExpansionStateManager.addExpansionListener(callback) + trySendWithFailureLogging(ShadeModel(), TAG, "initial shade expansion info") + + awaitClose { shadeExpansionStateManager.removeExpansionListener(callback) } + } + .distinctUntilChanged() + + companion object { + private const val TAG = "ShadeRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt new file mode 100644 index 0000000000000..ce0f4283ff831 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt @@ -0,0 +1,28 @@ +/* + * 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.systemui.shade.domain.model + +import android.annotation.FloatRange + +/** Information about shade (NotificationPanel) expansion */ +data class ShadeModel( + /** 0 when collapsed, 1 when fully expanded. */ + @FloatRange(from = 0.0, to = 1.0) val expansionAmount: Float = 0f, + /** Whether the panel should be considered expanded */ + val isExpanded: Boolean = false, + /** Whether the user is actively dragging the panel. */ + val isUserDragging: Boolean = false, +) diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index af439f0e009b4..03efd06d0e5e5 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -24,13 +24,14 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.flags.FeatureFlags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.plugins.ClockAnimations import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.ClockEvents import com.android.systemui.plugins.ClockFaceController import com.android.systemui.plugins.ClockFaceEvents -import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.mockito.any @@ -79,6 +80,7 @@ class ClockEventControllerTest : SysuiTestCase() { @Mock private lateinit var smallClockEvents: ClockFaceEvents @Mock private lateinit var largeClockEvents: ClockFaceEvents @Mock private lateinit var parentView: View + @Mock private lateinit var transitionRepository: KeyguardTransitionRepository private lateinit var repository: FakeKeyguardRepository private lateinit var underTest: ClockEventController @@ -98,6 +100,7 @@ class ClockEventControllerTest : SysuiTestCase() { underTest = ClockEventController( KeyguardInteractor(repository = repository), + KeyguardTransitionInteractor(repository = transitionRepository), broadcastDispatcher, batteryController, keyguardUpdateMonitor, @@ -132,7 +135,6 @@ class ClockEventControllerTest : SysuiTestCase() { @Test fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) { - clockEventController.clock = clock verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) @@ -145,7 +147,6 @@ class ClockEventControllerTest : SysuiTestCase() { @Test fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) { - clockEventController.clock = clock verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt new file mode 100644 index 0000000000000..1b34100b1cef1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt @@ -0,0 +1,259 @@ +/* + * 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.systemui.keyguard.data.repository + +import android.animation.AnimationHandler.AnimationFrameCallbackProvider +import android.animation.ValueAnimator +import android.util.Log +import android.util.Log.TerribleFailure +import android.util.Log.TerribleFailureHandler +import android.view.Choreographer.FrameCallback +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Interpolators +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.google.common.truth.Truth.assertThat +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.After +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardTransitionRepositoryTest : SysuiTestCase() { + + private lateinit var underTest: KeyguardTransitionRepository + private lateinit var oldWtfHandler: TerribleFailureHandler + private lateinit var wtfHandler: WtfHandler + + @Before + fun setUp() { + underTest = KeyguardTransitionRepository() + wtfHandler = WtfHandler() + oldWtfHandler = Log.setWtfHandler(wtfHandler) + } + + @After + fun tearDown() { + oldWtfHandler?.let { Log.setWtfHandler(it) } + } + + @Test + fun `startTransition runs animator to completion`() = + runBlocking(IMMEDIATE) { + val (animator, provider) = setupAnimator(this) + + val steps = mutableListOf() + val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this) + + underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator)) + + val startTime = System.currentTimeMillis() + while (animator.isRunning()) { + yield() + if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) { + fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION") + } + } + + assertSteps(steps, listWithStep(BigDecimal(.1))) + + job.cancel() + provider.stop() + } + + @Test + fun `startTransition called during another transition fails`() { + underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, null)) + underTest.startTransition(TransitionInfo(OWNER_NAME, LOCKSCREEN, BOUNCER, null)) + + assertThat(wtfHandler.failed).isTrue() + } + + @Test + fun `Null animator enables manual control with updateTransition`() = + runBlocking(IMMEDIATE) { + val steps = mutableListOf() + val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this) + + val uuid = + underTest.startTransition( + TransitionInfo( + ownerName = OWNER_NAME, + from = AOD, + to = LOCKSCREEN, + animator = null, + ) + ) + + checkNotNull(uuid).let { + underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) + underTest.updateTransition(it, 1f, TransitionState.FINISHED) + } + + assertThat(steps.size).isEqualTo(3) + assertThat(steps[0]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED)) + assertThat(steps[1]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0.5f, TransitionState.RUNNING)) + assertThat(steps[2]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED)) + job.cancel() + } + + @Test + fun `Attempt to manually update transition with invalid UUID throws exception`() { + underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING) + assertThat(wtfHandler.failed).isTrue() + } + + @Test + fun `Attempt to manually update transition after FINISHED state throws exception`() { + val uuid = + underTest.startTransition( + TransitionInfo( + ownerName = OWNER_NAME, + from = AOD, + to = LOCKSCREEN, + animator = null, + ) + ) + + checkNotNull(uuid).let { + underTest.updateTransition(it, 1f, TransitionState.FINISHED) + underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) + } + assertThat(wtfHandler.failed).isTrue() + } + + private fun listWithStep(step: BigDecimal): List { + val steps = mutableListOf() + + var i = BigDecimal.ZERO + while (i.compareTo(BigDecimal.ONE) <= 0) { + steps.add(i) + i = (i + step).setScale(2, RoundingMode.HALF_UP) + } + + return steps + } + + private fun assertSteps(steps: List, fractions: List) { + // + 2 accounts for start and finish of automated transition + assertThat(steps.size).isEqualTo(fractions.size + 2) + + assertThat(steps[0]).isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED)) + fractions.forEachIndexed { index, fraction -> + assertThat(steps[index + 1]) + .isEqualTo( + TransitionStep(AOD, LOCKSCREEN, fraction.toFloat(), TransitionState.RUNNING) + ) + } + assertThat(steps[steps.size - 1]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED)) + + assertThat(wtfHandler.failed).isFalse() + } + + private fun setupAnimator( + scope: CoroutineScope + ): Pair { + val animator = + ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(ANIMATION_DURATION) + } + + val provider = TestFrameCallbackProvider(animator, scope) + provider.start() + + return Pair(animator, provider) + } + + /** Gives direct control over ValueAnimator. See [AnimationHandler] */ + private class TestFrameCallbackProvider( + private val animator: ValueAnimator, + private val scope: CoroutineScope, + ) : AnimationFrameCallbackProvider { + + private var frameCount = 1L + private var frames = MutableStateFlow(Pair(0L, null)) + private var job: Job? = null + + fun start() { + animator.getAnimationHandler().setProvider(this) + + job = + scope.launch { + frames.collect { + // Delay is required for AnimationHandler to properly register a callback + delay(1) + val (frameNumber, callback) = it + callback?.doFrame(frameNumber) + } + } + } + + fun stop() { + job?.cancel() + animator.getAnimationHandler().setProvider(null) + } + + override fun postFrameCallback(cb: FrameCallback) { + frames.value = Pair(++frameCount, cb) + } + override fun postCommitCallback(runnable: Runnable) {} + override fun getFrameTime() = frameCount + override fun getFrameDelay() = 1L + override fun setFrameDelay(delay: Long) {} + } + + private class WtfHandler : TerribleFailureHandler { + var failed = false + override fun onTerribleFailure(tag: String, what: TerribleFailure, system: Boolean) { + failed = true + } + } + + companion object { + private const val MAX_TEST_DURATION = 100L + private const val ANIMATION_DURATION = 10L + private const val OWNER_NAME = "Test" + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 725b1f41372c9..0c126805fb787 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.data.repository import com.android.systemui.common.shared.model.Position +import com.android.systemui.keyguard.shared.model.StatusBarState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -44,6 +45,9 @@ class FakeKeyguardRepository : KeyguardRepository { private val _dozeAmount = MutableStateFlow(0f) override val dozeAmount: Flow = _dozeAmount + private val _statusBarState = MutableStateFlow(StatusBarState.SHADE) + override val statusBarState: Flow = _statusBarState + override fun isKeyguardShowing(): Boolean { return _isKeyguardShowing.value }