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
This commit is contained in:
Nick Chameyev
2022-03-23 12:15:14 +00:00
parent e0934dadbe
commit e40fb48498
3 changed files with 290 additions and 2 deletions

View File

@@ -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)
}
}

View File

@@ -0,0 +1,221 @@
/*
* 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)
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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) }
}
}