Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8c49232b authored by Matt Pietal's avatar Matt Pietal
Browse files

[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
parent 215a1f76
Loading
Loading
Loading
Loading
+17 −3
Original line number Diff line number Diff line
@@ -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,
+2 −0
Original line number Diff line number Diff line
@@ -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 {
    /**
+31 −0
Original line number Diff line number Diff line
@@ -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<Float>

    /** Observable for the [StatusBarState] */
    val statusBarState: Flow<StatusBarState>

    /**
     * Returns `true` if the keyguard is showing; `false` otherwise.
     *
@@ -185,6 +189,24 @@ constructor(
        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) {
        _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"
    }
+169 −0
Original line number Diff line number Diff line
/*
 * 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"
    }
}
+77 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
Loading