Merge "Continue folding animation after it cancelled if necessary" into tm-dev

This commit is contained in:
Nick Chameyev
2022-03-25 10:28:40 +00:00
committed by Android (Google) Code Review
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 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
/** Maps fold updates to unfold transition progress using DynamicAnimation. */ /** Maps fold updates to unfold transition progress using DynamicAnimation. */
internal class PhysicsBasedUnfoldTransitionProgressProvider( class PhysicsBasedUnfoldTransitionProgressProvider(
private val foldStateProvider: FoldStateProvider private val foldStateProvider: FoldStateProvider
) : UnfoldTransitionProgressProvider, FoldUpdatesListener, DynamicAnimation.OnAnimationEndListener { ) : UnfoldTransitionProgressProvider, FoldUpdatesListener, DynamicAnimation.OnAnimationEndListener {
@@ -97,7 +97,17 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider(
FOLD_UPDATE_START_CLOSING -> { FOLD_UPDATE_START_CLOSING -> {
// The transition might be already running as the device might start closing several // The transition might be already running as the device might start closing several
// times before reaching an end state. // 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) 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) }
}
}