Loading packages/SystemUI/shared/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt +12 −2 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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) } } Loading packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt 0 → 100644 +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) } } } packages/SystemUI/tests/src/com/android/systemui/unfold/util/TestFoldStateProvider.kt 0 → 100644 +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) } } } Loading
packages/SystemUI/shared/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt +12 −2 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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) } } Loading
packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt 0 → 100644 +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) } } }
packages/SystemUI/tests/src/com/android/systemui/unfold/util/TestFoldStateProvider.kt 0 → 100644 +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) } } }