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

Commit 8694fd04 authored by Justin Weir's avatar Justin Weir
Browse files

Convert ShadeInteractor to interface

Fixes: 308778968
Flag: NONE
Test: manually checked shade CUJs
Test: updated and ran existing tests
Change-Id: Ie87196a66989e4213dd069eb77a87f67a2fa59b5
parent 514bbc88
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -17,6 +17,8 @@
package com.android.systemui.shade

import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractorEmptyImpl
import dagger.Binds
import dagger.Module

@@ -30,4 +32,8 @@ abstract class ShadeEmptyImplModule {
    @Binds
    @SysUISingleton
    abstract fun bindsShadeController(sc: ShadeControllerEmptyImpl): ShadeController

    @Binds
    @SysUISingleton
    abstract fun bindsShadeInteractor(si: ShadeInteractorEmptyImpl): ShadeInteractor
}
+28 −1
Original line number Diff line number Diff line
@@ -17,13 +17,40 @@
package com.android.systemui.shade

import com.android.systemui.dagger.SysUISingleton

import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.shade.domain.interactor.BaseShadeInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl
import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl
import com.android.systemui.shade.domain.interactor.ShadeInteractorSceneContainerImpl
import dagger.Binds
import dagger.Module
import dagger.Provides
import javax.inject.Provider

/** Module for classes related to the notification shade. */
@Module(includes = [StartShadeModule::class, ShadeViewProviderModule::class])
abstract class ShadeModule {
    companion object {
        @Provides
        @SysUISingleton
        fun provideBaseShadeInteractor(
            sceneContainerFlags: SceneContainerFlags,
            sceneContainerOn: Provider<ShadeInteractorSceneContainerImpl>,
            sceneContainerOff: Provider<ShadeInteractorLegacyImpl>
        ): BaseShadeInteractor {
            return if (sceneContainerFlags.isEnabled()) {
                sceneContainerOn.get()
            } else {
                sceneContainerOff.get()
            }
        }
    }

    @Binds
    @SysUISingleton
    abstract fun bindsShadeInteractor(si: ShadeInteractorImpl): ShadeInteractor

    @Binds
    @SysUISingleton
    abstract fun bindsShadeViewController(
+40 −260
Original line number Diff line number Diff line
@@ -16,149 +16,39 @@

package com.android.systemui.shade.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.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.scene.shared.model.ObservableTransitionState
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository
import com.android.systemui.user.domain.interactor.UserSwitcherInteractor
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.isActive

/** Business logic for shade interactions. */
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class ShadeInteractor
@Inject
constructor(
    @Application scope: CoroutineScope,
    deviceProvisioningRepository: DeviceProvisioningRepository,
    disableFlagsRepository: DisableFlagsRepository,
    dozeParams: DozeParameters,
    sceneContainerFlags: SceneContainerFlags,
    // TODO(b/300258424) convert to direct reference instead of provider
    sceneInteractorProvider: Provider<SceneInteractor>,
    keyguardRepository: KeyguardRepository,
    keyguardTransitionInteractor: KeyguardTransitionInteractor,
    powerInteractor: PowerInteractor,
    userSetupRepository: UserSetupRepository,
    userSwitcherInteractor: UserSwitcherInteractor,
    sharedNotificationContainerInteractor: SharedNotificationContainerInteractor,
    private val repository: ShadeRepository,
) {
interface ShadeInteractor : BaseShadeInteractor {
    /** Emits true if the shade is currently allowed and false otherwise. */
    val isShadeEnabled: StateFlow<Boolean> =
        disableFlagsRepository.disableFlags
            .map { it.isShadeEnabled() }
            .stateIn(scope, SharingStarted.Eagerly, initialValue = false)
    val isShadeEnabled: StateFlow<Boolean>

    /**
     * Whether split shade, the combined notifications and quick settings shade used for large
     * screens, is enabled.
     */
    val isSplitShadeEnabled: Flow<Boolean> =
        sharedNotificationContainerInteractor.configurationBasedDimensions
            .map { dimens -> dimens.useSplitShade }
            .distinctUntilChanged()

    /** The amount [0-1] that the shade has been opened */
    val shadeExpansion: Flow<Float> =
        if (sceneContainerFlags.isEnabled()) {
            sceneBasedExpansion(sceneInteractorProvider.get(), SceneKey.Shade)
        } else {
            combine(
                    repository.lockscreenShadeExpansion,
                    keyguardRepository.statusBarState,
                    repository.legacyShadeExpansion,
                    repository.qsExpansion,
                    isSplitShadeEnabled
                ) {
                    lockscreenShadeExpansion,
                    statusBarState,
                    legacyShadeExpansion,
                    qsExpansion,
                    splitShadeEnabled ->
                    when (statusBarState) {
                        // legacyShadeExpansion is 1 instead of 0 when QS is expanded
                        StatusBarState.SHADE ->
                            if (!splitShadeEnabled && qsExpansion > 0f) 0f else legacyShadeExpansion
                        StatusBarState.KEYGUARD -> lockscreenShadeExpansion
                        // dragDownAmount, which drives lockscreenShadeExpansion resets to 0f when
                        // the pointer is lifted and the lockscreen shade is fully expanded
                        StatusBarState.SHADE_LOCKED -> 1f
                    }
                }
                .distinctUntilChanged()
        }
    /** Whether either the shade or QS is fully expanded. */
    val isAnyFullyExpanded: Flow<Boolean>

    /**
     * The amount [0-1] QS has been opened. Normal shade with notifications (QQS) visible will
     * report 0f. If split shade is enabled, value matches shadeExpansion.
     * Whether the user is expanding or collapsing either the shade or quick settings with user
     * input (i.e. dragging a pointer). This will be true even if the user's input gesture had ended
     * but a transition they initiated is still animating.
     */
    val qsExpansion: StateFlow<Float> =
        if (sceneContainerFlags.isEnabled()) {
            val qsExp = sceneBasedExpansion(sceneInteractorProvider.get(), SceneKey.QuickSettings)
            combine(isSplitShadeEnabled, shadeExpansion, qsExp) {
                    isSplitShadeEnabled,
                    shadeExp,
                    qsExp ->
                    if (isSplitShadeEnabled) {
                        shadeExp
                    } else {
                        qsExp
                    }
                }
                .stateIn(scope, SharingStarted.Eagerly, 0f)
        } else {
            repository.qsExpansion
        }
    val isUserInteracting: Flow<Boolean>

    /** Whether Quick Settings is expanded a non-zero amount. */
    val isQsExpanded: StateFlow<Boolean> =
        if (sceneContainerFlags.isEnabled()) {
            qsExpansion
                .map { it > 0 }
                .distinctUntilChanged()
                .stateIn(scope, SharingStarted.Eagerly, false)
        } else {
            repository.legacyIsQsExpanded
    /** Are touches allowed on the notification panel? */
    val isShadeTouchable: Flow<Boolean>

    /** Emits true if the shade can be expanded from QQS to QS and false otherwise. */
    val isExpandToQsEnabled: Flow<Boolean>
}

/** ShadeInteractor methods with implementations that differ between non-empty impls. */
interface BaseShadeInteractor {
    /** The amount [0-1] either QS or the shade has been opened. */
    val anyExpansion: StateFlow<Float> =
        combine(shadeExpansion, qsExpansion) { shadeExp, qsExp -> maxOf(shadeExp, qsExp) }
            .stateIn(scope, SharingStarted.Eagerly, 0f)

    /** Whether either the shade or QS is fully expanded. */
    val isAnyFullyExpanded: Flow<Boolean> = anyExpansion.map { it >= 1f }.distinctUntilChanged()
    val anyExpansion: StateFlow<Float>

    /**
     * Whether either the shade or QS is partially or fully expanded, i.e. not fully collapsed. At
@@ -169,149 +59,39 @@ constructor(
     *
     * TODO(b/300258424) remove all but the first sentence of this comment
     */
    val isAnyExpanded: StateFlow<Boolean> =
        if (sceneContainerFlags.isEnabled()) {
                anyExpansion.map { it > 0f }.distinctUntilChanged()
            } else {
                repository.legacyExpandedOrAwaitingInputTransfer
            }
            .stateIn(scope, SharingStarted.Eagerly, false)
    val isAnyExpanded: StateFlow<Boolean>

    /** The amount [0-1] that the shade has been opened */
    val shadeExpansion: Flow<Float>

    /**
     * The amount [0-1] QS has been opened. Normal shade with notifications (QQS) visible will
     * report 0f. If split shade is enabled, value matches shadeExpansion.
     */
    val qsExpansion: StateFlow<Float>

    /** Whether Quick Settings is expanded a non-zero amount. */
    val isQsExpanded: StateFlow<Boolean>

    /**
     * Whether the user is expanding or collapsing the shade with user input. This will be true even
     * if the user's input gesture has ended but a transition they initiated is animating.
     */
    val isUserInteractingWithShade: Flow<Boolean> =
        if (sceneContainerFlags.isEnabled()) {
            sceneBasedInteracting(sceneInteractorProvider.get(), SceneKey.Shade)
        } else {
            combine(
                userInteractingFlow(
                    repository.legacyShadeTracking,
                    repository.legacyShadeExpansion
                ),
                repository.legacyLockscreenShadeTracking
            ) { legacyShadeTracking, legacyLockscreenShadeTracking ->
                legacyShadeTracking || legacyLockscreenShadeTracking
            }
        }
    val isUserInteractingWithShade: Flow<Boolean>

    /**
     * Whether the user is expanding or collapsing quick settings with user input. This will be true
     * even if the user's input gesture has ended but a transition they initiated is still
     * animating.
     */
    val isUserInteractingWithQs: Flow<Boolean> =
        if (sceneContainerFlags.isEnabled()) {
            sceneBasedInteracting(sceneInteractorProvider.get(), SceneKey.QuickSettings)
        } else {
            userInteractingFlow(repository.legacyQsTracking, repository.qsExpansion)
        }

    /**
     * Whether the user is expanding or collapsing either the shade or quick settings with user
     * input (i.e. dragging a pointer). This will be true even if the user's input gesture had ended
     * but a transition they initiated is still animating.
     */
    val isUserInteracting: Flow<Boolean> =
        combine(isUserInteractingWithShade, isUserInteractingWithQs) { shade, qs -> shade || qs }
            .distinctUntilChanged()

    /** Are touches allowed on the notification panel? */
    val isShadeTouchable: Flow<Boolean> =
        combine(
            powerInteractor.isAsleep,
            keyguardTransitionInteractor.isInTransitionToStateWhere { it == KeyguardState.AOD },
            keyguardRepository.dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING },
            deviceProvisioningRepository.isFactoryResetProtectionActive,
        ) { isAsleep, goingToSleep, isPulsing, isFrpActive ->
            when {
                // Touches are disabled when Factory Reset Protection is active
                isFrpActive -> false
                // If the device is going to sleep, only accept touches if we're still
                // animating
                goingToSleep -> dozeParams.shouldControlScreenOff()
                // If the device is asleep, only accept touches if there's a pulse
                isAsleep -> isPulsing
                else -> true
            }
    val isUserInteractingWithQs: Flow<Boolean>
}

    /** Emits true if the shade can be expanded from QQS to QS and false otherwise. */
    val isExpandToQsEnabled: Flow<Boolean> =
        combine(
            disableFlagsRepository.disableFlags,
            isShadeEnabled,
            keyguardRepository.isDozing,
            userSetupRepository.isUserSetupFlow,
            deviceProvisioningRepository.isDeviceProvisioned,
        ) { disableFlags, isShadeEnabled, isDozing, isUserSetup, isDeviceProvisioned ->
            isDeviceProvisioned &&
                // Disallow QS during setup if it's a simple user switcher. (The user intends to
                // use the lock screen user switcher, QS is not needed.)
                (isUserSetup || !userSwitcherInteractor.isSimpleUserSwitcher) &&
                isShadeEnabled &&
                disableFlags.isQuickSettingsEnabled() &&
                !isDozing
        }

    fun sceneBasedExpansion(sceneInteractor: SceneInteractor, sceneKey: SceneKey) =
        sceneInteractor.transitionState
            .flatMapLatest { state ->
                when (state) {
                    is ObservableTransitionState.Idle ->
                        if (state.scene == sceneKey) {
                            flowOf(1f)
                        } else {
                            flowOf(0f)
                        }
                    is ObservableTransitionState.Transition ->
                        if (state.toScene == sceneKey) {
                            state.progress
                        } else if (state.fromScene == sceneKey) {
                            state.progress.map { progress -> 1 - progress }
                        } else {
                            flowOf(0f)
                        }
                }
            }
            .distinctUntilChanged()

    fun sceneBasedInteracting(sceneInteractor: SceneInteractor, sceneKey: SceneKey) =
        sceneInteractor.transitionState
            .map { state ->
                when (state) {
                    is ObservableTransitionState.Idle -> false
                    is ObservableTransitionState.Transition ->
                        state.isInitiatedByUserInput &&
                            (state.toScene == sceneKey || state.fromScene == sceneKey)
                }
            }
            .distinctUntilChanged()

    /**
     * Return a flow for whether a user is interacting with an expandable shade component using
     * tracking and expansion flows. NOTE: expansion must be a `StateFlow` to guarantee that
     * [expansion.first] checks the current value of the flow.
     */
    private fun userInteractingFlow(
        tracking: Flow<Boolean>,
        expansion: StateFlow<Float>
    ): Flow<Boolean> {
        return flow {
            // initial value is false
            emit(false)
            while (currentCoroutineContext().isActive) {
                // wait for tracking to become true
                tracking.first { it }
                emit(true)
                // wait for tracking to become false
                tracking.first { !it }
                // wait for expansion to complete in either direction
                expansion.first { it <= 0f || it >= 1f }
                // interaction complete
                emit(false)
            }
        }
    }
fun createAnyExpansionFlow(
    scope: CoroutineScope,
    shadeExpansion: Flow<Float>,
    qsExpansion: Flow<Float>
): StateFlow<Float> {
    return combine(shadeExpansion, qsExpansion) { shadeExp, qsExp -> maxOf(shadeExp, qsExp) }
        .stateIn(scope, SharingStarted.Eagerly, 0f)
}
+42 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.systemui.shade.domain.interactor

import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

/** Empty implementation of ShadeInteractor for System UI variants with no shade. */
@SysUISingleton
class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor {
    private val inactiveFlowBoolean = MutableStateFlow(false)
    private val inactiveFlowFloat = MutableStateFlow(0f)
    override val isShadeEnabled: StateFlow<Boolean> = inactiveFlowBoolean
    override val shadeExpansion: Flow<Float> = inactiveFlowFloat
    override val qsExpansion: StateFlow<Float> = inactiveFlowFloat
    override val isQsExpanded: StateFlow<Boolean> = inactiveFlowBoolean
    override val anyExpansion: StateFlow<Float> = inactiveFlowFloat
    override val isAnyFullyExpanded: Flow<Boolean> = inactiveFlowBoolean
    override val isAnyExpanded: StateFlow<Boolean> = inactiveFlowBoolean
    override val isUserInteractingWithShade: Flow<Boolean> = inactiveFlowBoolean
    override val isUserInteractingWithQs: Flow<Boolean> = inactiveFlowBoolean
    override val isUserInteracting: Flow<Boolean> = inactiveFlowBoolean
    override val isShadeTouchable: Flow<Boolean> = inactiveFlowBoolean
    override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean
}
+104 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.systemui.shade.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.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository
import com.android.systemui.user.domain.interactor.UserSwitcherInteractor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

/** The non-empty SceneInteractor implementation. */
@SysUISingleton
class ShadeInteractorImpl
@Inject
constructor(
    @Application val scope: CoroutineScope,
    deviceProvisioningRepository: DeviceProvisioningRepository,
    disableFlagsRepository: DisableFlagsRepository,
    dozeParams: DozeParameters,
    keyguardRepository: KeyguardRepository,
    keyguardTransitionInteractor: KeyguardTransitionInteractor,
    powerInteractor: PowerInteractor,
    userSetupRepository: UserSetupRepository,
    userSwitcherInteractor: UserSwitcherInteractor,
    private val baseShadeInteractor: BaseShadeInteractor,
) : ShadeInteractor, BaseShadeInteractor by baseShadeInteractor {
    override val isShadeEnabled: StateFlow<Boolean> =
        disableFlagsRepository.disableFlags
            .map { it.isShadeEnabled() }
            .stateIn(scope, SharingStarted.Eagerly, initialValue = false)

    override val isAnyFullyExpanded: Flow<Boolean> =
        anyExpansion.map { it >= 1f }.distinctUntilChanged()

    override val isUserInteracting: Flow<Boolean> =
        combine(isUserInteractingWithShade, isUserInteractingWithQs) { shade, qs -> shade || qs }
            .distinctUntilChanged()

    override val isShadeTouchable: Flow<Boolean> =
        combine(
            powerInteractor.isAsleep,
            keyguardTransitionInteractor.isInTransitionToStateWhere { it == KeyguardState.AOD },
            keyguardRepository.dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING },
            deviceProvisioningRepository.isFactoryResetProtectionActive,
        ) { isAsleep, goingToSleep, isPulsing, isFrpActive ->
            when {
                // Touches are disabled when Factory Reset Protection is active
                isFrpActive -> false
                // If the device is going to sleep, only accept touches if we're still
                // animating
                goingToSleep -> dozeParams.shouldControlScreenOff()
                // If the device is asleep, only accept touches if there's a pulse
                isAsleep -> isPulsing
                else -> true
            }
        }

    override val isExpandToQsEnabled: Flow<Boolean> =
        combine(
            disableFlagsRepository.disableFlags,
            isShadeEnabled,
            keyguardRepository.isDozing,
            userSetupRepository.isUserSetupFlow,
            deviceProvisioningRepository.isDeviceProvisioned,
        ) { disableFlags, isShadeEnabled, isDozing, isUserSetup, isDeviceProvisioned ->
            isDeviceProvisioned &&
                // Disallow QS during setup if it's a simple user switcher. (The user intends to
                // use the lock screen user switcher, QS is not needed.)
                (isUserSetup || !userSwitcherInteractor.isSimpleUserSwitcher) &&
                isShadeEnabled &&
                disableFlags.isQuickSettingsEnabled() &&
                !isDozing
        }
}
Loading