Merge "Continue folding animation after it cancelled if necessary" into tm-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
54c3d1b82e
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user