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

Commit 1804d9f4 authored by Shivangi Dubey's avatar Shivangi Dubey Committed by Android (Google) Code Review
Browse files

Merge "Do not play fold animation when sleep on fold" into main

parents 7ce3ad60 83b3d0fd
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -32,6 +32,8 @@ import dagger.Module
import dagger.Provides
import java.util.concurrent.Executor
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.android.asCoroutineDispatcher

/**
 * Dagger module with system-only dependencies for the unfold animation. The code that is used to
@@ -75,6 +77,13 @@ abstract class SystemUnfoldSharedModule {
            return Handler(looper)
        }

        @Provides
        @UnfoldBg
        @Singleton
        fun unfoldBgDispatcher(@UnfoldBg handler: Handler): CoroutineDispatcher {
            return handler.asCoroutineDispatcher("@UnfoldBg Dispatcher")
        }

        @Provides
        @UnfoldBg
        @Singleton
+5 −0
Original line number Diff line number Diff line
@@ -281,5 +281,10 @@ constructor(
                powerButtonLaunchGestureTriggeredDuringSleep = false,
            )
        }

        /** Helper method for tests to simulate the device screen state change event. */
        fun PowerInteractor.setScreenPowerState(screenPowerState: ScreenPowerState) {
            this.onScreenPowerStateUpdated(screenPowerState)
        }
    }
}
+29 −19
Original line number Diff line number Diff line
@@ -20,11 +20,10 @@ import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.annotation.BinderThread
import android.content.Context
import android.os.Handler
import android.os.SystemProperties
import android.util.Log
import android.view.animation.DecelerateInterpolator
import com.android.app.tracing.TraceUtils.traceAsync
import com.android.internal.foldables.FoldLockSettingAvailabilityProvider
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.display.data.repository.DeviceStateRepository
@@ -36,12 +35,13 @@ import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Comp
import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.isVerticalRotation
import com.android.systemui.unfold.dagger.UnfoldBg
import com.android.systemui.util.animation.data.repository.AnimationStatusRepository
import com.android.systemui.util.kotlin.race
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -59,13 +59,13 @@ import kotlinx.coroutines.withTimeout
class FoldLightRevealOverlayAnimation
@Inject
constructor(
    private val context: Context,
    @UnfoldBg private val bgHandler: Handler,
    @UnfoldBg private val bgDispatcher: CoroutineDispatcher,
    private val deviceStateRepository: DeviceStateRepository,
    private val powerInteractor: PowerInteractor,
    @Background private val applicationScope: CoroutineScope,
    private val animationStatusRepository: AnimationStatusRepository,
    private val controllerFactory: FullscreenLightRevealAnimationController.Factory
    private val controllerFactory: FullscreenLightRevealAnimationController.Factory,
    private val foldLockSettingAvailabilityProvider: FoldLockSettingAvailabilityProvider
) : FullscreenLightRevealAnimation {

    private val revealProgressValueAnimator: ValueAnimator =
@@ -79,7 +79,7 @@ constructor(
    override fun init() {
        // This method will be called only on devices where this animation is enabled,
        // so normally this thread won't be created
        if (!FoldLockSettingAvailabilityProvider(context.resources).isFoldLockBehaviorAvailable) {
        if (!foldLockSettingAvailabilityProvider.isFoldLockBehaviorAvailable) {
            return
        }

@@ -91,7 +91,6 @@ constructor(
            )
        controller.init()

        val bgDispatcher = bgHandler.asCoroutineDispatcher("@UnfoldBg Handler")
        applicationScope.launch(bgDispatcher) {
            powerInteractor.screenPowerState.collect {
                if (it == ScreenPowerState.SCREEN_ON) {
@@ -109,6 +108,9 @@ constructor(
                            if (!areAnimationEnabled.first() || !isFolded) {
                                return@flow
                            }
                            race(
                                {
                                    traceAsync(TAG, "prepareAndPlayFoldAnimation()") {
                                        withTimeout(WAIT_FOR_ANIMATION_TIMEOUT_MS) {
                                            readyCallback = CompletableDeferred()
                                            val onReady = readyCallback?.await()
@@ -118,6 +120,10 @@ constructor(
                                        }
                                        playFoldLightRevealOverlayAnimation()
                                    }
                                },
                                { waitForGoToSleep() }
                            )
                        }
                        .catchTimeoutAndLog()
                        .onCompletion {
                            controller.ensureOverlayRemoved()
@@ -135,10 +141,14 @@ constructor(
        readyCallback?.complete(onOverlayReady) ?: onOverlayReady.run()
    }

    private suspend fun waitForScreenTurnedOn() {
    private suspend fun waitForScreenTurnedOn() =
        traceAsync(TAG, "waitForScreenTurnedOn()") {
            powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first()
        }

    private suspend fun waitForGoToSleep() =
        traceAsync(TAG, "waitForGoToSleep()") { powerInteractor.isAsleep.filter { it }.first() }

    private suspend fun playFoldLightRevealOverlayAnimation() {
        revealProgressValueAnimator.duration = ANIMATION_DURATION
        revealProgressValueAnimator.interpolator = DecelerateInterpolator()
+7 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.unfold
import android.content.Context
import android.hardware.devicestate.DeviceStateManager
import android.os.SystemProperties
import com.android.internal.foldables.FoldLockSettingAvailabilityProvider
import com.android.systemui.CoreStartable
import com.android.systemui.Flags
import com.android.systemui.dagger.qualifiers.Application
@@ -175,6 +176,12 @@ class UnfoldTransitionModule {
    fun provideDisplaySwitchLatencyLogger(): DisplaySwitchLatencyLogger =
        DisplaySwitchLatencyLogger()

    @Provides
    @Singleton
    fun provideFoldLockSettingAvailabilityProvider(
        context: Context
    ): FoldLockSettingAvailabilityProvider = FoldLockSettingAvailabilityProvider(context.resources)

    @Module
    interface Bindings {
        @Binds fun bindRepository(impl: UnfoldTransitionRepositoryImpl): UnfoldTransitionRepository
+200 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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

import android.os.PowerManager
import android.os.SystemProperties
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.internal.foldables.FoldLockSettingAvailabilityProvider
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState
import com.android.systemui.display.data.repository.fakeDeviceStateRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setScreenPowerState
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.power.shared.model.ScreenPowerState
import com.android.systemui.util.animation.data.repository.fakeAnimationStatusRepository
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.atLeast
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidTestingRunner::class)
@OptIn(ExperimentalCoroutinesApi::class)
class FoldLightRevealOverlayAnimationTest : SysuiTestCase() {
    private val kosmos = Kosmos()
    private val testScope: TestScope = kosmos.testScope
    private val fakeDeviceStateRepository = kosmos.fakeDeviceStateRepository
    private val powerInteractor = kosmos.powerInteractor
    private val fakeAnimationStatusRepository = kosmos.fakeAnimationStatusRepository
    private val mockControllerFactory = kosmos.fullscreenLightRevealAnimationControllerFactory
    private val mockFullScreenController = kosmos.fullscreenLightRevealAnimationController
    private val mockFoldLockSettingAvailabilityProvider =
        mock<FoldLockSettingAvailabilityProvider>()
    private val onOverlayReady = mock<Runnable>()
    private lateinit var foldLightRevealAnimation: FoldLightRevealOverlayAnimation

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        whenever(mockFoldLockSettingAvailabilityProvider.isFoldLockBehaviorAvailable)
            .thenReturn(true)
        fakeAnimationStatusRepository.onAnimationStatusChanged(true)

        foldLightRevealAnimation =
            FoldLightRevealOverlayAnimation(
                kosmos.testDispatcher,
                fakeDeviceStateRepository,
                powerInteractor,
                testScope.backgroundScope,
                fakeAnimationStatusRepository,
                mockControllerFactory,
                mockFoldLockSettingAvailabilityProvider
            )
        foldLightRevealAnimation.init()
    }

    @Test
    fun foldToScreenOn_playFoldAnimation() =
        testScope.runTest {
            foldDeviceToScreenOff()
            turnScreenOn()

            verifyFoldAnimationPlayed()
        }

    @Test
    fun foldToAod_doNotPlayFoldAnimation() =
        testScope.runTest {
            foldDeviceToScreenOff()
            emitLastWakefulnessEventStartingToSleep()
            advanceTimeBy(SHORT_DELAY_MS)
            turnScreenOn()
            advanceTimeBy(ANIMATION_DURATION)

            verifyFoldAnimationDidNotPlay()
        }

    @Test
    fun foldToScreenOff_doNotPlayFoldAnimation() =
        testScope.runTest {
            foldDeviceToScreenOff()
            emitLastWakefulnessEventStartingToSleep()
            advanceTimeBy(SHORT_DELAY_MS)
            advanceTimeBy(ANIMATION_DURATION)

            verifyFoldAnimationDidNotPlay()
        }

    @Test
    fun foldToScreenOnWithDelay_doNotPlayFoldAnimation() =
        testScope.runTest {
            foldDeviceToScreenOff()
            foldLightRevealAnimation.onScreenTurningOn {}
            advanceTimeBy(WAIT_FOR_ANIMATION_TIMEOUT_MS)
            powerInteractor.setScreenPowerState(ScreenPowerState.SCREEN_ON)
            advanceTimeBy(SHORT_DELAY_MS)
            advanceTimeBy(ANIMATION_DURATION)

            verifyFoldAnimationDidNotPlay()
        }

    @Test
    fun immediateUnfoldAfterFold_removeOverlayAfterCancellation() =
        testScope.runTest {
            foldDeviceToScreenOff()
            foldLightRevealAnimation.onScreenTurningOn {}
            advanceTimeBy(SHORT_DELAY_MS)
            advanceTimeBy(ANIMATION_DURATION)
            fakeDeviceStateRepository.emit(DeviceState.HALF_FOLDED)
            advanceTimeBy(SHORT_DELAY_MS)
            powerInteractor.setScreenPowerState(ScreenPowerState.SCREEN_ON)

            verifyOverlayWasRemoved()
        }

    @Test
    fun foldToScreenOn_removeOverlayAfterCompletion() =
        testScope.runTest {
            foldDeviceToScreenOff()
            turnScreenOn()
            advanceTimeBy(ANIMATION_DURATION)

            verifyOverlayWasRemoved()
        }

    @Test
    fun unfold_immediatelyRunRunnable() =
        testScope.runTest {
            foldLightRevealAnimation.onScreenTurningOn(onOverlayReady)

            verify(onOverlayReady).run()
        }

    private suspend fun TestScope.foldDeviceToScreenOff() {
        fakeDeviceStateRepository.emit(DeviceState.HALF_FOLDED)
        powerInteractor.setScreenPowerState(ScreenPowerState.SCREEN_ON)
        advanceTimeBy(SHORT_DELAY_MS)
        fakeDeviceStateRepository.emit(DeviceState.FOLDED)
        advanceTimeBy(SHORT_DELAY_MS)
        powerInteractor.setScreenPowerState(ScreenPowerState.SCREEN_OFF)
        advanceTimeBy(SHORT_DELAY_MS)
    }

    private fun TestScope.turnScreenOn() {
        foldLightRevealAnimation.onScreenTurningOn {}
        advanceTimeBy(SHORT_DELAY_MS)
        powerInteractor.setScreenPowerState(ScreenPowerState.SCREEN_ON)
        advanceTimeBy(SHORT_DELAY_MS)
    }

    private fun emitLastWakefulnessEventStartingToSleep() =
        powerInteractor.setAsleepForTest(PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD)

    private fun verifyFoldAnimationPlayed() =
        verify(mockFullScreenController, atLeast(1)).updateRevealAmount(any())

    private fun verifyFoldAnimationDidNotPlay() =
        verify(mockFullScreenController, never()).updateRevealAmount(any())

    private fun verifyOverlayWasRemoved() =
        verify(mockFullScreenController, atLeast(1)).ensureOverlayRemoved()

    private companion object {
        const val WAIT_FOR_ANIMATION_TIMEOUT_MS = 2000L
        val ANIMATION_DURATION: Long
            get() = SystemProperties.getLong("persist.fold_animation_duration", 200L)
        const val SHORT_DELAY_MS = 50L
    }
}
Loading