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

Commit fa005d7c authored by Nick Chameyev's avatar Nick Chameyev Committed by Automerger Merge Worker
Browse files

Merge "Continue folding animation after it cancelled if necessary" into tm-dev am: 54c3d1b8

parents 09c018ab 54c3d1b8
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) }
    }
}