Merge changes from topic "mpietal_rewire" into tm-qpr-dev

* changes:
  [DO NOT MERGE] Add KeyguardTransitionRepository
  [DO NOT MERGE] Rewire dozing for clock events
This commit is contained in:
Matt Pietal
2022-10-18 15:38:17 +00:00
committed by Android (Google) Code Review
23 changed files with 1174 additions and 111 deletions

View File

@@ -23,12 +23,18 @@ import android.content.res.Resources
import android.text.format.DateFormat import android.text.format.DateFormat
import android.util.TypedValue import android.util.TypedValue
import android.view.View 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.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags 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.ClockController import com.android.systemui.plugins.ClockController
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.shared.regionsampling.RegionSamplingInstance import com.android.systemui.shared.regionsampling.RegionSamplingInstance
import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
@@ -38,13 +44,20 @@ import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import java.util.concurrent.Executor import java.util.concurrent.Executor
import javax.inject.Inject 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 * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by
* [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController]. * [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController].
*/ */
open class ClockEventController @Inject constructor( open class ClockEventController @Inject constructor(
private val statusBarStateController: StatusBarStateController, private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val broadcastDispatcher: BroadcastDispatcher, private val broadcastDispatcher: BroadcastDispatcher,
private val batteryController: BatteryController, private val batteryController: BatteryController,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@@ -53,7 +66,7 @@ open class ClockEventController @Inject constructor(
private val context: Context, private val context: Context,
@Main private val mainExecutor: Executor, @Main private val mainExecutor: Executor,
@Background private val bgExecutor: Executor, @Background private val bgExecutor: Executor,
private val featureFlags: FeatureFlags, private val featureFlags: FeatureFlags
) { ) {
var clock: ClockController? = null var clock: ClockController? = null
set(value) { set(value) {
@@ -70,9 +83,9 @@ open class ClockEventController @Inject constructor(
private var isCharging = false private var isCharging = false
private var dozeAmount = 0f private var dozeAmount = 0f
private var isKeyguardVisible = false private var isKeyguardVisible = false
private var isRegistered = false
private val regionSamplingEnabled = private var disposableHandle: DisposableHandle? = null
featureFlags.isEnabled(com.android.systemui.flags.Flags.REGION_SAMPLING) private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING)
private fun updateColors() { private fun updateColors() {
if (regionSamplingEnabled && smallRegionSampler != null && largeRegionSampler != null) { if (regionSamplingEnabled && smallRegionSampler != null && largeRegionSampler != null) {
@@ -165,15 +178,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() { private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
override fun onKeyguardVisibilityChanged(visible: Boolean) { override fun onKeyguardVisibilityChanged(visible: Boolean) {
isKeyguardVisible = visible isKeyguardVisible = visible
@@ -195,13 +199,11 @@ open class ClockEventController @Inject constructor(
} }
} }
init { fun registerListeners(parent: View) {
isDozing = statusBarStateController.isDozing if (isRegistered) {
} return
}
fun registerListeners() { isRegistered = true
dozeAmount = statusBarStateController.dozeAmount
isDozing = statusBarStateController.isDozing || dozeAmount != 0f
broadcastDispatcher.registerReceiver( broadcastDispatcher.registerReceiver(
localeBroadcastReceiver, localeBroadcastReceiver,
@@ -210,17 +212,28 @@ open class ClockEventController @Inject constructor(
configurationController.addCallback(configListener) configurationController.addCallback(configListener)
batteryController.addCallback(batteryCallback) batteryController.addCallback(batteryCallback)
keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
statusBarStateController.addCallback(statusBarStateListener)
smallRegionSampler?.startRegionSampler() smallRegionSampler?.startRegionSampler()
largeRegionSampler?.startRegionSampler() largeRegionSampler?.startRegionSampler()
disposableHandle = parent.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
listenForDozing(this)
listenForDozeAmount(this)
listenForDozeAmountTransition(this)
}
}
} }
fun unregisterListeners() { fun unregisterListeners() {
if (!isRegistered) {
return
}
isRegistered = false
disposableHandle?.dispose()
broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver) broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver)
configurationController.removeCallback(configListener) configurationController.removeCallback(configListener)
batteryController.removeCallback(batteryCallback) batteryController.removeCallback(batteryCallback)
keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback) keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
statusBarStateController.removeCallback(statusBarStateListener)
smallRegionSampler?.stopRegionSampler() smallRegionSampler?.stopRegionSampler()
largeRegionSampler?.stopRegionSampler() largeRegionSampler?.stopRegionSampler()
} }
@@ -235,8 +248,39 @@ open class ClockEventController @Inject constructor(
largeRegionSampler?.dump(pw) largeRegionSampler?.dump(pw)
} }
companion object { @VisibleForTesting
private val TAG = ClockEventController::class.simpleName internal fun listenForDozeAmount(scope: CoroutineScope): Job {
private const val FORMAT_NUMBER = 1234567890 return scope.launch {
keyguardInteractor.dozeAmount.collect {
dozeAmount = it
clock?.animations?.doze(dozeAmount)
}
}
}
@VisibleForTesting
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,
keyguardInteractor.isDozing,
) { localDozeAmount, localIsDozing ->
localDozeAmount > dozeAmount || localIsDozing
}
.collect { localIsDozing ->
isDozing = localIsDozing
}
}
} }
} }

View File

@@ -164,7 +164,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS
protected void onViewAttached() { protected void onViewAttached() {
mClockRegistry.registerClockChangeListener(mClockChangedListener); mClockRegistry.registerClockChangeListener(mClockChangedListener);
setClock(mClockRegistry.createCurrentClock()); setClock(mClockRegistry.createCurrentClock());
mClockEventController.registerListeners(); mClockEventController.registerListeners(mView);
mKeyguardClockTopMargin = mKeyguardClockTopMargin =
mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin); mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);

View File

@@ -43,6 +43,7 @@ import com.android.systemui.keyguard.DismissCallbackRegistry;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; 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.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule;
import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -72,6 +73,7 @@ import dagger.Provides;
FalsingModule.class, FalsingModule.class,
KeyguardQuickAffordanceModule.class, KeyguardQuickAffordanceModule.class,
KeyguardRepositoryModule.class, KeyguardRepositoryModule.class,
StartKeyguardTransitionModule.class,
}) })
public class KeyguardModule { public class KeyguardModule {
/** /**

View File

@@ -21,6 +21,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall
import com.android.systemui.common.shared.model.Position import com.android.systemui.common.shared.model.Position
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.doze.DozeHost 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.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.KeyguardStateController
import javax.inject.Inject import javax.inject.Inject
@@ -85,6 +86,9 @@ interface KeyguardRepository {
*/ */
val dozeAmount: Flow<Float> val dozeAmount: Flow<Float>
/** Observable for the [StatusBarState] */
val statusBarState: Flow<StatusBarState>
/** /**
* Returns `true` if the keyguard is showing; `false` otherwise. * Returns `true` if the keyguard is showing; `false` otherwise.
* *
@@ -185,6 +189,24 @@ constructor(
return keyguardStateController.isShowing return keyguardStateController.isShowing
} }
override val statusBarState: Flow<StatusBarState> = 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) { override fun setAnimateDozingTransitions(animate: Boolean) {
_animateBottomAreaDozingTransitions.value = animate _animateBottomAreaDozingTransitions.value = animate
} }
@@ -197,6 +219,15 @@ constructor(
_clockPosition.value = Position(x, y) _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 { companion object {
private const val TAG = "KeyguardRepositoryImpl" private const val TAG = "KeyguardRepositoryImpl"
} }

View File

@@ -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<TransitionStep> {
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"
}
}

View File

@@ -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
}
}

View File

@@ -38,7 +38,7 @@ constructor(
val dozeAmount: Flow<Float> = repository.dozeAmount val dozeAmount: Flow<Float> = repository.dozeAmount
/** Whether the system is in doze mode. */ /** Whether the system is in doze mode. */
val isDozing: Flow<Boolean> = repository.isDozing val isDozing: Flow<Boolean> = repository.isDozing
/** Whether the keyguard is showing ot not. */ /** Whether the keyguard is showing to not. */
val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
fun isKeyguardShowing(): Boolean { fun isKeyguardShowing(): Boolean {

View File

@@ -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<TransitionInteractor>,
) : 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"
}
}

View File

@@ -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<TransitionStep> = repository.transition(AOD, LOCKSCREEN)
}

View File

@@ -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,
)
)
}
}
}
}
}
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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"
}) +
")"
}

View File

@@ -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
}

View File

@@ -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,
)

View File

@@ -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<ShadeModel> =
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"
}
}

View File

@@ -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,
)

View File

@@ -17,17 +17,21 @@ package com.android.keyguard
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.flags.FeatureFlags 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.ClockAnimations
import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.ClockController
import com.android.systemui.plugins.ClockEvents import com.android.systemui.plugins.ClockEvents
import com.android.systemui.plugins.ClockFaceController import com.android.systemui.plugins.ClockFaceController
import com.android.systemui.plugins.ClockFaceEvents 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.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
@@ -37,6 +41,9 @@ import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.mock
import java.util.TimeZone import java.util.TimeZone
import java.util.concurrent.Executor import java.util.concurrent.Executor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@@ -57,7 +64,7 @@ import org.mockito.junit.MockitoJUnit
class ClockEventControllerTest : SysuiTestCase() { class ClockEventControllerTest : SysuiTestCase() {
@JvmField @Rule val mockito = MockitoJUnit.rule() @JvmField @Rule val mockito = MockitoJUnit.rule()
@Mock private lateinit var statusBarStateController: StatusBarStateController @Mock private lateinit var keyguardInteractor: KeyguardInteractor
@Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
@Mock private lateinit var batteryController: BatteryController @Mock private lateinit var batteryController: BatteryController
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@@ -72,8 +79,11 @@ class ClockEventControllerTest : SysuiTestCase() {
@Mock private lateinit var largeClockController: ClockFaceController @Mock private lateinit var largeClockController: ClockFaceController
@Mock private lateinit var smallClockEvents: ClockFaceEvents @Mock private lateinit var smallClockEvents: ClockFaceEvents
@Mock private lateinit var largeClockEvents: 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 clockEventController: ClockEventController private lateinit var underTest: ClockEventController
@Before @Before
fun setUp() { fun setUp() {
@@ -86,8 +96,11 @@ class ClockEventControllerTest : SysuiTestCase() {
whenever(clock.events).thenReturn(events) whenever(clock.events).thenReturn(events)
whenever(clock.animations).thenReturn(animations) whenever(clock.animations).thenReturn(animations)
clockEventController = ClockEventController( repository = FakeKeyguardRepository()
statusBarStateController,
underTest = ClockEventController(
KeyguardInteractor(repository = repository),
KeyguardTransitionInteractor(repository = transitionRepository),
broadcastDispatcher, broadcastDispatcher,
batteryController, batteryController,
keyguardUpdateMonitor, keyguardUpdateMonitor,
@@ -98,31 +111,33 @@ class ClockEventControllerTest : SysuiTestCase() {
bgExecutor, bgExecutor,
featureFlags featureFlags
) )
underTest.clock = clock
runBlocking(IMMEDIATE) {
underTest.registerListeners(parentView)
repository.setDozing(true)
repository.setDozeAmount(1f)
}
} }
@Test @Test
fun clockSet_validateInitialization() { fun clockSet_validateInitialization() {
clockEventController.clock = clock
verify(clock).initialize(any(), anyFloat(), anyFloat()) verify(clock).initialize(any(), anyFloat(), anyFloat())
} }
@Test @Test
fun clockUnset_validateState() { fun clockUnset_validateState() {
clockEventController.clock = clock underTest.clock = null
clockEventController.clock = null
assertEquals(clockEventController.clock, null) assertEquals(underTest.clock, null)
} }
@Test @Test
fun themeChanged_verifyClockPaletteUpdated() { fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) {
clockEventController.clock = clock
verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
clockEventController.registerListeners()
val captor = argumentCaptor<ConfigurationController.ConfigurationListener>() val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
verify(configurationController).addCallback(capture(captor)) verify(configurationController).addCallback(capture(captor))
captor.value.onThemeChanged() captor.value.onThemeChanged()
@@ -131,13 +146,10 @@ class ClockEventControllerTest : SysuiTestCase() {
} }
@Test @Test
fun fontChanged_verifyFontSizeUpdated() { fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) {
clockEventController.clock = clock
verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
clockEventController.registerListeners()
val captor = argumentCaptor<ConfigurationController.ConfigurationListener>() val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
verify(configurationController).addCallback(capture(captor)) verify(configurationController).addCallback(capture(captor))
captor.value.onDensityOrFontScaleChanged() captor.value.onDensityOrFontScaleChanged()
@@ -146,10 +158,7 @@ class ClockEventControllerTest : SysuiTestCase() {
} }
@Test @Test
fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() { fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) {
clockEventController.clock = clock
clockEventController.registerListeners()
val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
verify(batteryController).addCallback(capture(batteryCaptor)) verify(batteryController).addCallback(capture(batteryCaptor))
val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
@@ -161,26 +170,21 @@ class ClockEventControllerTest : SysuiTestCase() {
} }
@Test @Test
fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() { fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() =
clockEventController.clock = clock runBlocking(IMMEDIATE) {
clockEventController.registerListeners() val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
verify(batteryController).addCallback(capture(batteryCaptor))
val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
keyguardCaptor.value.onKeyguardVisibilityChanged(true)
batteryCaptor.value.onBatteryLevelChanged(10, false, true)
batteryCaptor.value.onBatteryLevelChanged(10, false, true)
val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() verify(animations, times(1)).charge()
verify(batteryController).addCallback(capture(batteryCaptor)) }
val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
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()
}
@Test @Test
fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() { fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) {
clockEventController.clock = clock
clockEventController.registerListeners()
val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
verify(batteryController).addCallback(capture(batteryCaptor)) verify(batteryController).addCallback(capture(batteryCaptor))
val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
@@ -192,25 +196,20 @@ class ClockEventControllerTest : SysuiTestCase() {
} }
@Test @Test
fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() { fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() =
clockEventController.clock = clock runBlocking(IMMEDIATE) {
clockEventController.registerListeners() val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
verify(batteryController).addCallback(capture(batteryCaptor))
val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
keyguardCaptor.value.onKeyguardVisibilityChanged(true)
batteryCaptor.value.onBatteryLevelChanged(10, false, false)
val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() verify(animations, never()).charge()
verify(batteryController).addCallback(capture(batteryCaptor)) }
val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
keyguardCaptor.value.onKeyguardVisibilityChanged(true)
batteryCaptor.value.onBatteryLevelChanged(10, false, false)
verify(animations, never()).charge()
}
@Test @Test
fun localeCallback_verifyClockNotified() { fun localeCallback_verifyClockNotified() = runBlocking(IMMEDIATE) {
clockEventController.clock = clock
clockEventController.registerListeners()
val captor = argumentCaptor<BroadcastReceiver>() val captor = argumentCaptor<BroadcastReceiver>()
verify(broadcastDispatcher).registerReceiver( verify(broadcastDispatcher).registerReceiver(
capture(captor), any(), eq(null), eq(null), anyInt(), eq(null) capture(captor), any(), eq(null), eq(null), anyInt(), eq(null)
@@ -221,10 +220,7 @@ class ClockEventControllerTest : SysuiTestCase() {
} }
@Test @Test
fun keyguardCallback_visibilityChanged_clockDozeCalled() { fun keyguardCallback_visibilityChanged_clockDozeCalled() = runBlocking(IMMEDIATE) {
clockEventController.clock = clock
clockEventController.registerListeners()
val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
verify(keyguardUpdateMonitor).registerCallback(capture(captor)) verify(keyguardUpdateMonitor).registerCallback(capture(captor))
@@ -236,10 +232,7 @@ class ClockEventControllerTest : SysuiTestCase() {
} }
@Test @Test
fun keyguardCallback_timeFormat_clockNotified() { fun keyguardCallback_timeFormat_clockNotified() = runBlocking(IMMEDIATE) {
clockEventController.clock = clock
clockEventController.registerListeners()
val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
verify(keyguardUpdateMonitor).registerCallback(capture(captor)) verify(keyguardUpdateMonitor).registerCallback(capture(captor))
captor.value.onTimeFormatChanged("12h") captor.value.onTimeFormatChanged("12h")
@@ -248,11 +241,8 @@ class ClockEventControllerTest : SysuiTestCase() {
} }
@Test @Test
fun keyguardCallback_timezoneChanged_clockNotified() { fun keyguardCallback_timezoneChanged_clockNotified() = runBlocking(IMMEDIATE) {
val mockTimeZone = mock<TimeZone>() val mockTimeZone = mock<TimeZone>()
clockEventController.clock = clock
clockEventController.registerListeners()
val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
verify(keyguardUpdateMonitor).registerCallback(capture(captor)) verify(keyguardUpdateMonitor).registerCallback(capture(captor))
captor.value.onTimeZoneChanged(mockTimeZone) captor.value.onTimeZoneChanged(mockTimeZone)
@@ -261,10 +251,7 @@ class ClockEventControllerTest : SysuiTestCase() {
} }
@Test @Test
fun keyguardCallback_userSwitched_clockNotified() { fun keyguardCallback_userSwitched_clockNotified() = runBlocking(IMMEDIATE) {
clockEventController.clock = clock
clockEventController.registerListeners()
val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
verify(keyguardUpdateMonitor).registerCallback(capture(captor)) verify(keyguardUpdateMonitor).registerCallback(capture(captor))
captor.value.onUserSwitchComplete(10) captor.value.onUserSwitchComplete(10)
@@ -273,25 +260,27 @@ class ClockEventControllerTest : SysuiTestCase() {
} }
@Test @Test
fun keyguardCallback_verifyKeyguardChanged() { fun keyguardCallback_verifyKeyguardChanged() = runBlocking(IMMEDIATE) {
clockEventController.clock = clock val job = underTest.listenForDozeAmount(this)
clockEventController.registerListeners() repository.setDozeAmount(0.4f)
val captor = argumentCaptor<StatusBarStateController.StateListener>() yield()
verify(statusBarStateController).addCallback(capture(captor))
captor.value.onDozeAmountChanged(0.4f, 0.6f)
verify(animations).doze(0.4f) verify(animations).doze(0.4f)
job.cancel()
} }
@Test @Test
fun unregisterListeners_validate() { fun unregisterListeners_validate() = runBlocking(IMMEDIATE) {
clockEventController.clock = clock underTest.unregisterListeners()
clockEventController.unregisterListeners()
verify(broadcastDispatcher).unregisterReceiver(any()) verify(broadcastDispatcher).unregisterReceiver(any())
verify(configurationController).removeCallback(any()) verify(configurationController).removeCallback(any())
verify(batteryController).removeCallback(any()) verify(batteryController).removeCallback(any())
verify(keyguardUpdateMonitor).removeCallback(any()) verify(keyguardUpdateMonitor).removeCallback(any())
verify(statusBarStateController).removeCallback(any()) }
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
} }
} }

View File

@@ -265,6 +265,6 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase {
private void verifyAttachment(VerificationMode times) { private void verifyAttachment(VerificationMode times) {
verify(mClockRegistry, times).registerClockChangeListener( verify(mClockRegistry, times).registerClockChangeListener(
any(ClockRegistry.ClockChangeListener.class)); any(ClockRegistry.ClockChangeListener.class));
verify(mClockEventController, times).registerListeners(); verify(mClockEventController, times).registerListeners(mView);
} }
} }

View File

@@ -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<TransitionStep>()
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<TransitionStep>()
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<BigDecimal> {
val steps = mutableListOf<BigDecimal>()
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<TransitionStep>, fractions: List<BigDecimal>) {
// + 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<ValueAnimator, TestFrameCallbackProvider> {
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<Long, FrameCallback?>(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
}
}

View File

@@ -18,6 +18,7 @@
package com.android.systemui.keyguard.data.repository package com.android.systemui.keyguard.data.repository
import com.android.systemui.common.shared.model.Position 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.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -44,6 +45,9 @@ class FakeKeyguardRepository : KeyguardRepository {
private val _dozeAmount = MutableStateFlow(0f) private val _dozeAmount = MutableStateFlow(0f)
override val dozeAmount: Flow<Float> = _dozeAmount override val dozeAmount: Flow<Float> = _dozeAmount
private val _statusBarState = MutableStateFlow(StatusBarState.SHADE)
override val statusBarState: Flow<StatusBarState> = _statusBarState
override fun isKeyguardShowing(): Boolean { override fun isKeyguardShowing(): Boolean {
return _isKeyguardShowing.value return _isKeyguardShowing.value
} }