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

Commit 6df6f068 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Extracting display switch logic to FoldableDisplaySwitchTrackingInteractor" into main

parents 1f1c7fe0 920b161b
Loading
Loading
Loading
Loading
+39 −14
Original line number Original line Diff line number Diff line
@@ -47,13 +47,14 @@ import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_ON
import com.android.systemui.shared.system.SysUiStatsLog
import com.android.systemui.shared.system.SysUiStatsLog
import com.android.systemui.statusbar.policy.FakeConfigurationController
import com.android.systemui.statusbar.policy.FakeConfigurationController
import com.android.systemui.testKosmos
import com.android.systemui.testKosmos
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.COOL_DOWN_DURATION
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_CLOSED
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_CLOSED
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_HALF_OPEN
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_HALF_OPEN
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.SCREEN_EVENT_TIMEOUT
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent
import com.android.systemui.unfold.data.repository.ScreenTimeoutPolicyRepository
import com.android.systemui.unfold.data.repository.ScreenTimeoutPolicyRepository
import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
import com.android.systemui.unfold.domain.interactor.FoldableDisplaySwitchTrackingInteractor
import com.android.systemui.unfold.domain.interactor.FoldableDisplaySwitchTrackingInteractor.Companion.COOL_DOWN_DURATION
import com.android.systemui.unfold.domain.interactor.FoldableDisplaySwitchTrackingInteractor.Companion.SCREEN_EVENT_TIMEOUT
import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
import com.android.systemui.unfoldedDeviceState
import com.android.systemui.unfoldedDeviceState
import com.android.systemui.util.animation.data.repository.fakeAnimationStatusRepository
import com.android.systemui.util.animation.data.repository.fakeAnimationStatusRepository
@@ -62,7 +63,6 @@ import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import java.util.Optional
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestDispatcher
@@ -93,7 +93,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val kosmos = testKosmos()
    private val mockContext = mock<Context>()
    private val mockContext = mock<Context>()
    private val resources = mock<Resources>()
    private val resources = mock<Resources>()
    private val foldStateRepository = kosmos.fakeDeviceStateRepository
    private val deviceStateRepository = kosmos.fakeDeviceStateRepository
    private val powerInteractor = PowerInteractorFactory.create().powerInteractor
    private val powerInteractor = PowerInteractorFactory.create().powerInteractor
    private val animationStatusRepository = kosmos.fakeAnimationStatusRepository
    private val animationStatusRepository = kosmos.fakeAnimationStatusRepository
    private val keyguardInteractor = mock<KeyguardInteractor>()
    private val keyguardInteractor = mock<KeyguardInteractor>()
@@ -143,20 +143,29 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
        animationStatusRepository.onAnimationStatusChanged(true)
        animationStatusRepository.onAnimationStatusChanged(true)
        powerInteractor.setAwakeForTest()
        powerInteractor.setAwakeForTest()
        powerInteractor.setScreenPowerState(SCREEN_ON)
        powerInteractor.setScreenPowerState(SCREEN_ON)

        val displaySwitchInteractor =
            FoldableDisplaySwitchTrackingInteractor(
                deviceStateRepository,
                powerInteractor,
                unfoldTransitionInteractor,
                animationStatusRepository,
                keyguardInteractor,
                systemClock,
                testScope.backgroundScope,
            )
        displaySwitchInteractor.start()
        displaySwitchLatencyTracker =
        displaySwitchLatencyTracker =
            DisplaySwitchLatencyTracker(
            DisplaySwitchLatencyTracker(
                mockContext,
                mockContext,
                foldStateRepository,
                powerInteractor,
                powerInteractor,
                screenTimeoutPolicyRepository,
                screenTimeoutPolicyRepository,
                unfoldTransitionInteractor,
                animationStatusRepository,
                keyguardInteractor,
                keyguardInteractor,
                testDispatcher.asExecutor(),
                testScope.backgroundScope,
                testScope.backgroundScope,
                displaySwitchLatencyLogger,
                displaySwitchLatencyLogger,
                systemClock,
                systemClock,
                deviceStateManager,
                deviceStateManager,
                displaySwitchInteractor,
                latencyTracker,
                latencyTracker,
            )
            )
    }
    }
@@ -196,20 +205,28 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
                    UnfoldTransitionRepositoryImpl(Optional.empty()),
                    UnfoldTransitionRepositoryImpl(Optional.empty()),
                    configurationInteractor,
                    configurationInteractor,
                )
                )
            val displaySwitchInteractor =
                FoldableDisplaySwitchTrackingInteractor(
                    deviceStateRepository,
                    powerInteractor,
                    unfoldTransitionInteractorWithEmptyProgressProvider,
                    animationStatusRepository,
                    keyguardInteractor,
                    systemClock,
                    testScope.backgroundScope,
                )
            displaySwitchInteractor.start()
            displaySwitchLatencyTracker =
            displaySwitchLatencyTracker =
                DisplaySwitchLatencyTracker(
                DisplaySwitchLatencyTracker(
                    mockContext,
                    mockContext,
                    foldStateRepository,
                    powerInteractor,
                    powerInteractor,
                    screenTimeoutPolicyRepository,
                    screenTimeoutPolicyRepository,
                    unfoldTransitionInteractorWithEmptyProgressProvider,
                    animationStatusRepository,
                    keyguardInteractor,
                    keyguardInteractor,
                    testDispatcher.asExecutor(),
                    testScope.backgroundScope,
                    testScope.backgroundScope,
                    displaySwitchLatencyLogger,
                    displaySwitchLatencyLogger,
                    systemClock,
                    systemClock,
                    deviceStateManager,
                    deviceStateManager,
                    displaySwitchInteractor,
                    latencyTracker,
                    latencyTracker,
                )
                )


@@ -467,13 +484,14 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
    }
    }


    @Test
    @Test
    fun displaySwitchInterrupted_cancelsTrackingWhenNewDeviceStateEmitted() {
    fun displaySwitchInterrupted_newDeviceState_trackingNotSent() {
        testScope.runTest {
        testScope.runTest {
            startInFoldedState(displaySwitchLatencyTracker)
            startInFoldedState(displaySwitchLatencyTracker)


            startUnfolding()
            startUnfolding()
            startFolding()
            startFolding()
            finishFolding()
            finishFolding()
            waitForCorruptedStateToPass()


            verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
            verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
            verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
            verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
@@ -491,6 +509,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
            startFolding()
            startFolding()
            startUnfolding()
            startUnfolding()
            finishUnfolding()
            finishUnfolding()
            waitForCorruptedStateToPass()


            verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
            verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
            verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
            verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
@@ -621,6 +640,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
            startInFoldedState(displaySwitchLatencyTracker)
            startInFoldedState(displaySwitchLatencyTracker)


            startUnfolding()
            startUnfolding()
            systemClock.advanceTime(SCREEN_EVENT_TIMEOUT.inWholeMilliseconds)
            advanceTimeBy(SCREEN_EVENT_TIMEOUT + 10.milliseconds)
            advanceTimeBy(SCREEN_EVENT_TIMEOUT + 10.milliseconds)
            finishUnfolding()
            finishUnfolding()


@@ -747,7 +767,12 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
        runCurrent()
        runCurrent()
    }
    }


    private fun TestScope.waitForCorruptedStateToPass() {
        // extra buffer time so corrupted state is finished
        advanceTimeBy(COOL_DOWN_DURATION.plus(10.milliseconds))
    }

    private suspend fun setDeviceState(state: DeviceState) {
    private suspend fun setDeviceState(state: DeviceState) {
        foldStateRepository.emit(state)
        deviceStateRepository.emit(state)
    }
    }
}
}
+25 −12
Original line number Original line Diff line number Diff line
@@ -18,6 +18,7 @@ import android.os.Handler
import android.os.HandlerThread
import android.os.HandlerThread
import android.os.Looper
import android.os.Looper
import android.os.Process
import android.os.Process
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig
import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig
@@ -25,6 +26,7 @@ import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.dagger.UnfoldBg
import com.android.systemui.unfold.dagger.UnfoldBg
import com.android.systemui.unfold.dagger.UnfoldMain
import com.android.systemui.unfold.dagger.UnfoldMain
import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
import com.android.systemui.unfold.dagger.UnfoldTracking
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.util.CurrentActivityTypeProvider
import com.android.systemui.unfold.util.CurrentActivityTypeProvider
import dagger.Binds
import dagger.Binds
@@ -33,7 +35,10 @@ import dagger.Provides
import java.util.concurrent.Executor
import java.util.concurrent.Executor
import javax.inject.Singleton
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.plus


/**
/**
 * Dagger module with system-only dependencies for the unfold animation. The code that is used to
 * Dagger module with system-only dependencies for the unfold animation. The code that is used to
@@ -45,25 +50,20 @@ import kotlinx.coroutines.android.asCoroutineDispatcher
abstract class SystemUnfoldSharedModule {
abstract class SystemUnfoldSharedModule {


    @Binds
    @Binds
    abstract fun activityTypeProvider(executor: ActivityManagerActivityTypeProvider):
    abstract fun activityTypeProvider(
            CurrentActivityTypeProvider
        executor: ActivityManagerActivityTypeProvider
    ): CurrentActivityTypeProvider


    @Binds
    @Binds abstract fun config(config: ResourceUnfoldTransitionConfig): UnfoldTransitionConfig
    abstract fun config(config: ResourceUnfoldTransitionConfig): UnfoldTransitionConfig


    @Binds
    @Binds abstract fun foldState(provider: DeviceStateManagerFoldProvider): FoldProvider
    abstract fun foldState(provider: DeviceStateManagerFoldProvider): FoldProvider


    @Binds
    @Binds
    abstract fun deviceStateRepository(provider: DeviceStateRepositoryImpl): DeviceStateRepository
    abstract fun deviceStateRepository(provider: DeviceStateRepositoryImpl): DeviceStateRepository


    @Binds
    @Binds @UnfoldMain abstract fun mainExecutor(@Main executor: Executor): Executor
    @UnfoldMain
    abstract fun mainExecutor(@Main executor: Executor): Executor


    @Binds
    @Binds @UnfoldMain abstract fun mainHandler(@Main handler: Handler): Handler
    @UnfoldMain
    abstract fun mainHandler(@Main handler: Handler): Handler


    @Binds
    @Binds
    @UnfoldSingleThreadBg
    @UnfoldSingleThreadBg
@@ -92,5 +92,18 @@ abstract class SystemUnfoldSharedModule {
                .apply { start() }
                .apply { start() }
                .looper
                .looper
        }
        }

        @Provides
        @UnfoldTracking
        @Singleton
        fun unfoldTrackingContext(
            @UnfoldSingleThreadBg singleThreadBgExecutor: Executor,
            @Application applicationScope: CoroutineScope,
        ): CoroutineScope {
            // tracking depends on being executed on a single thread so when changing it, ensure all
            // consumers are not accessing shared state
            val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher()
            return applicationScope + backgroundDispatcher
        }
    }
    }
}
}
+132 −201

File changed.

Preview size limit exceeded, changes collapsed.

+10 −5
Original line number Original line Diff line number Diff line
@@ -23,6 +23,8 @@ import com.android.systemui.shade.NotificationPanelUnfoldAnimationController
import com.android.systemui.statusbar.phone.StatusBarMoveFromCenterAnimationController
import com.android.systemui.statusbar.phone.StatusBarMoveFromCenterAnimationController
import com.android.systemui.unfold.dagger.NaturalRotation
import com.android.systemui.unfold.dagger.NaturalRotation
import com.android.systemui.unfold.dagger.UnfoldBg
import com.android.systemui.unfold.dagger.UnfoldBg
import com.android.systemui.unfold.domain.interactor.DisplaySwitchTrackingInteractor
import com.android.systemui.unfold.domain.interactor.FoldableDisplaySwitchTrackingInteractor
import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
import com.android.systemui.unfold.util.UnfoldKeyguardVisibilityManager
import com.android.systemui.unfold.util.UnfoldKeyguardVisibilityManager
@@ -56,9 +58,7 @@ import javax.inject.Scope
@Module(subcomponents = [SysUIUnfoldComponent::class])
@Module(subcomponents = [SysUIUnfoldComponent::class])
class SysUIUnfoldModule {
class SysUIUnfoldModule {


    /**
    /** Qualifier for dependencies bound in [com.android.systemui.unfold.SysUIUnfoldModule] */
     * Qualifier for dependencies bound in [com.android.systemui.unfold.SysUIUnfoldModule]
     */
    @Qualifier
    @Qualifier
    @MustBeDocumented
    @MustBeDocumented
    @Retention(AnnotationRetention.RUNTIME)
    @Retention(AnnotationRetention.RUNTIME)
@@ -72,7 +72,7 @@ class SysUIUnfoldModule {
        rotationProvider: Optional<NaturalRotationUnfoldProgressProvider>,
        rotationProvider: Optional<NaturalRotationUnfoldProgressProvider>,
        @Named(UNFOLD_STATUS_BAR) scopedProvider: Optional<ScopedUnfoldTransitionProgressProvider>,
        @Named(UNFOLD_STATUS_BAR) scopedProvider: Optional<ScopedUnfoldTransitionProgressProvider>,
        @UnfoldBg bgProvider: Optional<UnfoldTransitionProgressProvider>,
        @UnfoldBg bgProvider: Optional<UnfoldTransitionProgressProvider>,
        factory: SysUIUnfoldComponent.Factory
        factory: SysUIUnfoldComponent.Factory,
    ): Optional<SysUIUnfoldComponent> {
    ): Optional<SysUIUnfoldComponent> {
        val p1 = provider.getOrNull()
        val p1 = provider.getOrNull()
        val p2 = rotationProvider.getOrNull()
        val p2 = rotationProvider.getOrNull()
@@ -92,6 +92,11 @@ interface SysUIUnfoldStartableModule {
    @IntoMap
    @IntoMap
    @ClassKey(UnfoldInitializationStartable::class)
    @ClassKey(UnfoldInitializationStartable::class)
    fun bindsUnfoldInitializationStartable(impl: UnfoldInitializationStartable): CoreStartable
    fun bindsUnfoldInitializationStartable(impl: UnfoldInitializationStartable): CoreStartable

    @Binds
    @IntoMap
    @ClassKey(DisplaySwitchTrackingInteractor::class)
    fun bindsDisplaySwitchTracker(impl: FoldableDisplaySwitchTrackingInteractor): CoreStartable
}
}


@Module
@Module
@@ -128,7 +133,7 @@ interface SysUIUnfoldComponent {
            @BindsInstance p1: UnfoldTransitionProgressProvider,
            @BindsInstance p1: UnfoldTransitionProgressProvider,
            @BindsInstance p2: NaturalRotationUnfoldProgressProvider,
            @BindsInstance p2: NaturalRotationUnfoldProgressProvider,
            @BindsInstance p3: ScopedUnfoldTransitionProgressProvider,
            @BindsInstance p3: ScopedUnfoldTransitionProgressProvider,
            @BindsInstance @UnfoldBg p4: UnfoldTransitionProgressProvider
            @BindsInstance @UnfoldBg p4: UnfoldTransitionProgressProvider,
        ): SysUIUnfoldComponent
        ): SysUIUnfoldComponent
    }
    }


+238 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2025 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.unfold.domain.interactor

import android.os.Build
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.app.tracing.TraceUtils.traceAsync
import com.android.app.tracing.instantForTrack
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.display.data.repository.DeviceStateRepository
import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.power.shared.model.ScreenPowerState
import com.android.systemui.power.shared.model.WakefulnessModel
import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.unfold.dagger.UnfoldTracking
import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted
import com.android.systemui.unfold.domain.interactor.DisplaySwitchState.Corrupted
import com.android.systemui.unfold.domain.interactor.DisplaySwitchState.Idle
import com.android.systemui.util.animation.data.repository.AnimationStatusRepository
import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.kotlin.race
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.timeout
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout

interface DisplaySwitchTrackingInteractor {
    /**
     * Emits latest [DisplaySwitchState] with a guarantee it doesn't emit the same class of state
     * twice in a row.
     */
    val displaySwitchState: StateFlow<DisplaySwitchState>
}

sealed interface DisplaySwitchState {
    val newDeviceState: DeviceState

    /**
     * Displays are in a stable state aka not in the process of switching. If we couldn't track
     * display switch properly because end event never arrived within [SCREEN_EVENT_TIMEOUT],
     * [timedOut] is set to true.
     */
    data class Idle(override val newDeviceState: DeviceState, val timedOut: Boolean = false) :
        DisplaySwitchState

    /** Displays are currently switching. This state can only come directly after [Idle] state. */
    data class Switching(override val newDeviceState: DeviceState) : DisplaySwitchState

    /**
     * Switching displays happened multiple times before [Idle] state could settle. This state will
     * hold until no new display switch related events are sent within [COOL_DOWN_DURATION] window.
     * This event can only happen directly after [Switching] state and is always directly followed
     * by [Idle] state.
     */
    data class Corrupted(override val newDeviceState: DeviceState) : DisplaySwitchState

    /** Missing data about device state, should happen only when class is initialized. */
    data object Unknown : DisplaySwitchState {
        override val newDeviceState: DeviceState = DeviceState.UNKNOWN
    }
}

/**
 * Contains data about current state of display switch on foldable device which can be one of
 * [DisplaySwitchState]. [displaySwitchState] emits only states that we're sure are legit changes,
 * compared to super quick toggling between the states caused by Hall sensor misfiring or user doing
 * something very creative. Those kind of changes cause [DisplaySwitchState.CORRUPTED] to be
 * emitted.
 *
 * You need to call [start] for [displaySwitchState] to start emitting state changes.
 */
@SysUISingleton
class FoldableDisplaySwitchTrackingInteractor
@Inject
constructor(
    private val deviceStateRepository: DeviceStateRepository,
    private val powerInteractor: PowerInteractor,
    private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
    private val animationStatusRepository: AnimationStatusRepository,
    private val keyguardInteractor: KeyguardInteractor,
    private val systemClock: SystemClock,
    @UnfoldTracking private val scope: CoroutineScope,
) : DisplaySwitchTrackingInteractor, CoreStartable {

    private val _displaySwitchState =
        MutableStateFlow<DisplaySwitchState>(DisplaySwitchState.Unknown)

    override val displaySwitchState: StateFlow<DisplaySwitchState> = _displaySwitchState

    private val displaySwitchStarted =
        deviceStateRepository.state.pairwise().filter {
            // React only when the foldable device is
            // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or unfolding(FOLDED -> HALF_FOLD/UNFOLDED)
            foldableDeviceState ->
            foldableDeviceState.previousValue == DeviceState.FOLDED ||
                foldableDeviceState.newValue == DeviceState.FOLDED
        }

    private val startOrEndEvent: Flow<Any> = merge(displaySwitchStarted, anyEndEventFlow())

    private var isCoolingDown = false

    override fun start() {
        scope.launch {
            _displaySwitchState.value =
                Idle(deviceStateRepository.state.first { it != DeviceState.UNKNOWN })
            displaySwitchStarted.collectLatest { (previousState, newState) ->
                if (isCoolingDown) return@collectLatest
                log { "received previousState=$previousState, newState=$newState" }
                try {
                    _displaySwitchState.value = DisplaySwitchState.Switching(newState)
                    withTimeout(SCREEN_EVENT_TIMEOUT) {
                        traceAsync(TAG, "displaySwitch") { waitForDisplaySwitch(newState) }
                        _displaySwitchState.value = Idle(deviceStateRepository.state.value)
                    }
                } catch (e: TimeoutCancellationException) {
                    log { "tracking timed out" }
                    _displaySwitchState.value = Idle(newState, timedOut = true)
                } catch (e: CancellationException) {
                    log { "new state interrupted, entering cool down" }
                    _displaySwitchState.value = Corrupted(newState)
                    startCoolDown()
                }
            }
        }
    }

    private inline fun log(msg: () -> String) {
        if (DEBUG) Log.d(TAG, msg())
    }

    @OptIn(FlowPreview::class)
    private fun CoroutineScope.startCoolDown() {
        if (isCoolingDown) return
        isCoolingDown = true
        launch {
            val startTime = systemClock.elapsedRealtime()
            try {
                startOrEndEvent.timeout(COOL_DOWN_DURATION).collect {}
            } catch (e: TimeoutCancellationException) {
                val totalCooldownTime = systemClock.elapsedRealtime() - startTime
                instantForTrack(TAG) { "cool down finished, lasted $totalCooldownTime ms" }
                _displaySwitchState.value = Idle(deviceStateRepository.state.value)
                isCoolingDown = false
            }
        }
    }

    private suspend fun waitForDisplaySwitch(toFoldableDeviceState: DeviceState) {
        val isTransitionEnabled =
            unfoldTransitionInteractor.isAvailable &&
                animationStatusRepository.areAnimationsEnabled().first()
        if (shouldWaitForTransitionStart(toFoldableDeviceState, isTransitionEnabled)) {
            traceAsync(TAG, "waitForTransitionStart()") {
                unfoldTransitionInteractor.waitForTransitionStart()
            }
        } else {
            race({ waitForScreenTurnedOn() }, { waitForGoToSleepWithScreenOff() })
        }
    }

    private fun anyEndEventFlow(): Flow<Any> {
        val unfoldStatus =
            unfoldTransitionInteractor.unfoldTransitionStatus.filter { it is TransitionStarted }
        // dropping first emission as we're only interested in new emissions, not current state
        val screenOn =
            powerInteractor.screenPowerState.drop(1).filter { it == ScreenPowerState.SCREEN_ON }
        val goToSleep =
            powerInteractor.detailedWakefulness.drop(1).filter { sleepWithScreenOff(it) }
        return merge(screenOn, goToSleep, unfoldStatus)
    }

    private fun shouldWaitForTransitionStart(
        toFoldableDeviceState: DeviceState,
        isTransitionEnabled: Boolean,
    ): Boolean = (toFoldableDeviceState != DeviceState.FOLDED && isTransitionEnabled)

    private suspend fun waitForScreenTurnedOn() {
        traceAsync(TAG, "waitForScreenTurnedOn()") {
            // dropping first as it's stateFlow and will always emit latest value but we're
            // only interested in new states
            powerInteractor.screenPowerState
                .drop(1)
                .filter { it == ScreenPowerState.SCREEN_ON }
                .first()
        }
    }

    private suspend fun waitForGoToSleepWithScreenOff() {
        traceAsync(TAG, "waitForGoToSleepWithScreenOff()") {
            powerInteractor.detailedWakefulness.filter { sleepWithScreenOff(it) }.first()
        }
    }

    private fun sleepWithScreenOff(model: WakefulnessModel) =
        model.internalWakefulnessState == WakefulnessState.ASLEEP &&
            !keyguardInteractor.isAodAvailable.value

    companion object {
        private const val TAG = "FoldableDisplaySwitch"
        private val DEBUG = Build.IS_DEBUGGABLE

        @VisibleForTesting val COOL_DOWN_DURATION = 2.seconds
        @VisibleForTesting val SCREEN_EVENT_TIMEOUT = 15.seconds
    }
}
Loading