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

Commit 83b3d0fd authored by dshivangi's avatar dshivangi
Browse files

Do not play fold animation when sleep on fold

Currently when the device is folded with fold setting 'Never' on of two
happens:
1. AOD enabled: AOD animation is played along with Folding animation
2. AOD disabled: The logic waits 2 seconds for the screen to turns on,
meanwhile in case the screen is turned on for some other reason(power button
press, tap to wake) then the respective animation could be seen along
with folding animation.

As a solution in this change, every time while preparing to play fold
animation we will also listen for device to go to sleep.
The animation is only played if the device did not go to sleep on fold,
otherwise the inner flow is completed immediately and clean-up is
carried out.
Fixes: 329229552, 328570727
Flag: ACONFIG fold_lock_setting_enabled NEXTFOOD
Test: Flash build locally to device
* Fold -> Unfold the device with different fold setting values and AOD
  disabled/enabled
* Check if fold animation is playing correctly and not interfering with
  other animations
* Also confirm perfetto traces to check if folding animation is
  finishing immediately(unlike currently it is taking ~200ms to finish)

Change-Id: Ifcade3fcff638109e91572550885bccd8ec0be6b
parent 7cd86981
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