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

Commit e40fb484 authored by Nick Chameyev's avatar Nick Chameyev
Browse files

Continue folding animation after it cancelled if necessary

When folding the device immediately after unfolding
the folding animation wasn't triggering sometimes.
It happenned because we force the animation to end
by 'cancelling' it (setting target progress to 1f and
ignoring all hinge angle events).

Added resetting cancelling when we started folding
again during this cancellation animation.

Bug: 219962337
Test: atest com.android.systemui.unfold.progress.PhysicsBasedUnfoldTransitionProgressProviderTest
Change-Id: Iafd1b3e40f40debf4c09874ddb418a1f2fd2824c
parent e0934dad
Loading
Loading
Loading
Loading
+12 −2
Original line number Diff line number Diff line
@@ -33,7 +33,7 @@ import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener

/** Maps fold updates to unfold transition progress using DynamicAnimation. */
internal class PhysicsBasedUnfoldTransitionProgressProvider(
class PhysicsBasedUnfoldTransitionProgressProvider(
    private val foldStateProvider: FoldStateProvider
) : UnfoldTransitionProgressProvider, FoldUpdatesListener, DynamicAnimation.OnAnimationEndListener {

@@ -97,7 +97,17 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider(
            FOLD_UPDATE_START_CLOSING -> {
                // The transition might be already running as the device might start closing several
                // times before reaching an end state.
                if (!isTransitionRunning) {
                if (isTransitionRunning) {
                    // If we are cancelling the animation, reset that so we can resume it normally.
                    // The animation could be 'cancelled' when the user stops folding/unfolding
                    // for some period of time or fully unfolds the device. In this case,
                    // it is forced to run to the end ignoring all further hinge angle events.
                    // By resetting this flag we allow reacting to hinge angle events again, so
                    // the transition continues running.
                    if (isAnimatedCancelRunning) {
                        isAnimatedCancelRunning = false
                    }
                } else {
                    startTransition(startValue = 1f)
                }
            }
+221 −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.unfold.progress

import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.systemui.SysuiTestCase
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.updates.FOLD_UPDATE_START_OPENING
import com.android.systemui.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN
import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
import com.android.systemui.unfold.util.TestFoldStateProvider
import com.android.systemui.util.leak.ReferenceTestUtils.waitForCondition
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidTestingRunner::class)
@SmallTest
class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() {

    private val foldStateProvider: TestFoldStateProvider = TestFoldStateProvider()
    private val listener = TestUnfoldProgressListener()
    private lateinit var progressProvider: UnfoldTransitionProgressProvider

    @Before
    fun setUp() {
        progressProvider = PhysicsBasedUnfoldTransitionProgressProvider(
            foldStateProvider
        )
        progressProvider.addCallback(listener)
    }

    @Test
    fun testUnfold_emitsIncreasingTransitionEvents() {
        runOnMainThreadWithInterval(
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_OPENING) },
            { foldStateProvider.sendHingeAngleUpdate(10f) },
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) },
            { foldStateProvider.sendHingeAngleUpdate(90f) },
            { foldStateProvider.sendHingeAngleUpdate(180f) },
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) },
        )

        with(listener.ensureTransitionFinished()) {
            assertIncreasingProgress()
            assertFinishedWithUnfold()
        }
    }

    @Test
    fun testUnfold_screenAvailableOnlyAfterFullUnfold_emitsIncreasingTransitionEvents() {
        runOnMainThreadWithInterval(
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_OPENING) },
            { foldStateProvider.sendHingeAngleUpdate(10f) },
            { foldStateProvider.sendHingeAngleUpdate(90f) },
            { foldStateProvider.sendHingeAngleUpdate(180f) },
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) },
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) },
        )

        with(listener.ensureTransitionFinished()) {
            assertIncreasingProgress()
            assertFinishedWithUnfold()
        }
    }

    @Test
    fun testFold_emitsDecreasingTransitionEvents() {
        runOnMainThreadWithInterval(
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_CLOSING) },
            { foldStateProvider.sendHingeAngleUpdate(170f) },
            { foldStateProvider.sendHingeAngleUpdate(90f) },
            { foldStateProvider.sendHingeAngleUpdate(10f) },
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_CLOSED) },
        )

        with(listener.ensureTransitionFinished()) {
            assertDecreasingProgress()
            assertFinishedWithFold()
        }
    }

    @Test
    fun testUnfoldAndStopUnfolding_finishesTheUnfoldTransition() {
        runOnMainThreadWithInterval(
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_OPENING) },
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) },
            { foldStateProvider.sendHingeAngleUpdate(10f) },
            { foldStateProvider.sendHingeAngleUpdate(90f) },
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN) },
        )

        with(listener.ensureTransitionFinished()) {
            assertIncreasingProgress()
            assertFinishedWithUnfold()
        }
    }

    @Test
    fun testFoldImmediatelyAfterUnfold_runsFoldAnimation() {
        runOnMainThreadWithInterval(
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_OPENING) },
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) },
            { foldStateProvider.sendHingeAngleUpdate(10f) },
            { foldStateProvider.sendHingeAngleUpdate(90f) },
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) },
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_CLOSING) },
            { foldStateProvider.sendHingeAngleUpdate(60f) },
            { foldStateProvider.sendHingeAngleUpdate(10f) },
            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_CLOSED) },
        )

        with(listener.ensureTransitionFinished()) {
            assertHasFoldAnimationAtTheEnd()
        }
    }

    private class TestUnfoldProgressListener : TransitionProgressListener {

        private val recordings: MutableList<UnfoldTransitionRecording> = arrayListOf()
        private var currentRecording: UnfoldTransitionRecording? = null

        override fun onTransitionStarted() {
            assertWithMessage("Trying to start a transition when it is already in progress")
                .that(currentRecording).isNull()

            currentRecording = UnfoldTransitionRecording()
        }

        override fun onTransitionProgress(progress: Float) {
            assertWithMessage("Received transition progress event when it's not started")
                .that(currentRecording).isNotNull()
            currentRecording!!.addProgress(progress)
        }

        override fun onTransitionFinished() {
            assertWithMessage("Received transition finish event when it's not started")
                .that(currentRecording).isNotNull()
            recordings += currentRecording!!
            currentRecording = null
        }

        fun ensureTransitionFinished(): UnfoldTransitionRecording {
            waitForCondition { recordings.size == 1 }
            return recordings.first()
        }

        class UnfoldTransitionRecording {
            private val progressHistory: MutableList<Float> = arrayListOf()

            fun addProgress(progress: Float) {
                assertThat(progress).isAtMost(1.0f)
                assertThat(progress).isAtLeast(0.0f)

                progressHistory += progress
            }

            fun assertIncreasingProgress() {
                assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS)
                assertThat(progressHistory).isInOrder()
            }

            fun assertDecreasingProgress() {
                assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS)
                assertThat(progressHistory).isInOrder(Comparator.reverseOrder<Float>())
            }

            fun assertFinishedWithUnfold() {
                assertThat(progressHistory).isNotEmpty()
                assertThat(progressHistory.last()).isEqualTo(1.0f)
            }

            fun assertFinishedWithFold() {
                assertThat(progressHistory).isNotEmpty()
                assertThat(progressHistory.last()).isEqualTo(0.0f)
            }

            fun assertHasFoldAnimationAtTheEnd() {
                // Check that there are at least a few decreasing events at the end
                assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS)
                assertThat(progressHistory.takeLast(MIN_ANIMATION_EVENTS))
                    .isInOrder(Comparator.reverseOrder<Float>())
                assertThat(progressHistory.last()).isEqualTo(0.0f)
            }
        }

        private companion object {
            private const val MIN_ANIMATION_EVENTS = 5
        }
    }

    private fun runOnMainThreadWithInterval(vararg blocks: () -> Unit, intervalMillis: Long = 60) {
        blocks.forEach {
            InstrumentationRegistry.getInstrumentation().runOnMainSync {
                it()
            }
            Thread.sleep(intervalMillis)
        }
    }
}
+57 −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.unfold.util

import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
import com.android.systemui.unfold.updates.FoldStateProvider
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener

class TestFoldStateProvider : FoldStateProvider {

    private val listeners: MutableList<FoldUpdatesListener> = arrayListOf()

    override fun start() {
    }

    override fun stop() {
        listeners.clear()
    }

    private var _isFullyOpened: Boolean = false

    override val isFullyOpened: Boolean
        get() = _isFullyOpened

    override fun addCallback(listener: FoldUpdatesListener) {
        listeners += listener
    }

    override fun removeCallback(listener: FoldUpdatesListener) {
        listeners -= listener
    }

    fun sendFoldUpdate(@FoldUpdate update: Int) {
        if (update == FOLD_UPDATE_FINISH_FULL_OPEN) {
            _isFullyOpened = true
        }
        listeners.forEach { it.onFoldUpdate(update) }
    }

    fun sendHingeAngleUpdate(angle: Float) {
        listeners.forEach { it.onHingeAngleUpdate(angle) }
    }
}