Make remote Unfold progress receiver resilient to jank
If the process sending progresses has jank or long main thread pauses, the receiving process would show the same jank pattern. With this cl, on the receving and a filter is added so smooth those cases, and result in an always smooth animation despite sending process jank (for now, sending process is always sysui, and receiving process is always launcher) Bug: 274409068 Test: UnfoldRemoteFilterTest, RemoteUnfoldTransitionReceiverTest and manual Change-Id: I2cb2e32a8639d364495e7133191e0cd8b8c77649
This commit is contained in:
@@ -18,6 +18,7 @@ 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 org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -27,23 +28,27 @@ import org.junit.runner.RunWith
|
||||
@SmallTest
|
||||
class RemoteUnfoldTransitionReceiverTest : SysuiTestCase() {
|
||||
|
||||
private val progressProvider = RemoteUnfoldTransitionReceiver { it.run() }
|
||||
private val progressProvider =
|
||||
RemoteUnfoldTransitionReceiver(useReceivingFilter = true) { runOnMainSync(it) }
|
||||
private val progressProviderWithoutFilter =
|
||||
RemoteUnfoldTransitionReceiver(useReceivingFilter = false) { it.run() }
|
||||
private val listener = TestUnfoldProgressListener()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
progressProvider.addCallback(listener)
|
||||
progressProviderWithoutFilter.addCallback(listener)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTransitionStarted_propagated() {
|
||||
fun onTransitionStarted_withFilter_propagated() {
|
||||
progressProvider.onTransitionStarted()
|
||||
|
||||
listener.assertStarted()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTransitionProgress_propagated() {
|
||||
fun onTransitionProgress_withFilter_propagated() {
|
||||
progressProvider.onTransitionStarted()
|
||||
|
||||
progressProvider.onTransitionProgress(0.5f)
|
||||
@@ -52,7 +57,7 @@ class RemoteUnfoldTransitionReceiverTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTransitionEnded_propagated() {
|
||||
fun onTransitionEnded_withFilter_propagated() {
|
||||
progressProvider.onTransitionStarted()
|
||||
progressProvider.onTransitionProgress(0.5f)
|
||||
|
||||
@@ -62,11 +67,52 @@ class RemoteUnfoldTransitionReceiverTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTransitionStarted_afterCallbackRemoved_notPropagated() {
|
||||
fun onTransitionStarted_withFilter_afterCallbackRemoved_notPropagated() {
|
||||
progressProvider.removeCallback(listener)
|
||||
|
||||
progressProvider.onTransitionStarted()
|
||||
|
||||
listener.assertNotStarted()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTransitionStarted_withoutFilter_propagated() {
|
||||
progressProviderWithoutFilter.onTransitionStarted()
|
||||
|
||||
listener.assertStarted()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTransitionProgress_withoutFilter_propagated() {
|
||||
progressProviderWithoutFilter.onTransitionStarted()
|
||||
|
||||
progressProviderWithoutFilter.onTransitionProgress(0.5f)
|
||||
|
||||
listener.assertLastProgress(0.5f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTransitionEnded_withoutFilter_propagated() {
|
||||
progressProviderWithoutFilter.onTransitionStarted()
|
||||
progressProviderWithoutFilter.onTransitionProgress(0.5f)
|
||||
|
||||
progressProviderWithoutFilter.onTransitionFinished()
|
||||
|
||||
listener.ensureTransitionFinished()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTransitionStarted_withoutFilter_afterCallbackRemoved_notPropagated() {
|
||||
progressProviderWithoutFilter.removeCallback(listener)
|
||||
|
||||
progressProviderWithoutFilter.onTransitionStarted()
|
||||
|
||||
listener.assertNotStarted()
|
||||
}
|
||||
|
||||
private fun runOnMainSync(f: Runnable) {
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync { f.run() }
|
||||
// Sleep as the animator used from the filter has a callback that happens at every frame.
|
||||
Thread.sleep(60)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ class TestUnfoldProgressListener : UnfoldTransitionProgressProvider.TransitionPr
|
||||
}
|
||||
|
||||
fun assertLastProgress(progress: Float) {
|
||||
assertThat(progressHistory.last()).isEqualTo(progress)
|
||||
assertThat(progressHistory.last()).isWithin(1.0E-4F).of(progress)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@SmallTest
|
||||
class UnfoldRemoteFilterTest : SysuiTestCase() {
|
||||
private val listener = TestUnfoldProgressListener()
|
||||
|
||||
private val progressProvider = UnfoldRemoteFilter(listener)
|
||||
|
||||
@Test
|
||||
fun onTransitionStarted_propagated() {
|
||||
runOnMainThreadWithInterval({ progressProvider.onTransitionStarted() })
|
||||
listener.assertStarted()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTransitionProgress_withInterval_propagated() {
|
||||
runOnMainThreadWithInterval(
|
||||
{ progressProvider.onTransitionStarted() },
|
||||
{ progressProvider.onTransitionProgress(0.5f) }
|
||||
)
|
||||
|
||||
listener.assertLastProgress(0.5f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTransitionEnded_propagated() {
|
||||
runOnMainThreadWithInterval(
|
||||
{ progressProvider.onTransitionStarted() },
|
||||
{ progressProvider.onTransitionProgress(0.5f) },
|
||||
{ progressProvider.onTransitionFinished() },
|
||||
)
|
||||
|
||||
listener.ensureTransitionFinished()
|
||||
}
|
||||
|
||||
private fun runOnMainThreadWithInterval(
|
||||
vararg blocks: () -> Unit,
|
||||
interval: Duration = 60.milliseconds
|
||||
) {
|
||||
blocks.forEach {
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync { it() }
|
||||
Thread.sleep(interval.inWholeMilliseconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.android.systemui.unfold
|
||||
|
||||
import com.android.systemui.unfold.config.UnfoldTransitionConfig
|
||||
import com.android.systemui.unfold.dagger.UseReceivingFilter
|
||||
import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver
|
||||
import com.android.systemui.unfold.util.ATraceLoggerTransitionProgressListener
|
||||
import dagger.Module
|
||||
@@ -42,4 +43,6 @@ class UnfoldRemoteModule {
|
||||
remoteReceiver.addCallback(traceListener)
|
||||
return Optional.of(remoteReceiver)
|
||||
}
|
||||
|
||||
@Provides @UseReceivingFilter fun useReceivingFilter(): Boolean = true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.dagger
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
/** Annotates whether to use a filter in [RemoteUnfoldTransitionReceiver]. */
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class UseReceivingFilter
|
||||
@@ -16,9 +16,13 @@
|
||||
|
||||
package com.android.systemui.unfold.progress
|
||||
|
||||
import android.util.Log
|
||||
import androidx.annotation.BinderThread
|
||||
import androidx.annotation.FloatRange
|
||||
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
|
||||
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
|
||||
import com.android.systemui.unfold.dagger.UnfoldMain
|
||||
import com.android.systemui.unfold.dagger.UseReceivingFilter
|
||||
import java.util.concurrent.Executor
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -30,21 +34,40 @@ import javax.inject.Inject
|
||||
*/
|
||||
class RemoteUnfoldTransitionReceiver
|
||||
@Inject
|
||||
constructor(@UnfoldMain private val executor: Executor) :
|
||||
UnfoldTransitionProgressProvider, IUnfoldTransitionListener.Stub() {
|
||||
constructor(
|
||||
@UseReceivingFilter useReceivingFilter: Boolean,
|
||||
@UnfoldMain private val executor: Executor
|
||||
) : UnfoldTransitionProgressProvider, IUnfoldTransitionListener.Stub() {
|
||||
|
||||
private val listeners: MutableSet<TransitionProgressListener> = mutableSetOf()
|
||||
private val outputProgressListener = ProcessedProgressListener()
|
||||
private val filter: TransitionProgressListener? =
|
||||
if (useReceivingFilter) {
|
||||
UnfoldRemoteFilter(outputProgressListener)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@BinderThread
|
||||
override fun onTransitionStarted() {
|
||||
executor.execute { listeners.forEach { it.onTransitionStarted() } }
|
||||
executor.execute {
|
||||
filter?.onTransitionStarted() ?: outputProgressListener.onTransitionStarted()
|
||||
}
|
||||
}
|
||||
|
||||
@BinderThread
|
||||
override fun onTransitionProgress(progress: Float) {
|
||||
executor.execute { listeners.forEach { it.onTransitionProgress(progress) } }
|
||||
executor.execute {
|
||||
filter?.onTransitionProgress(progress)
|
||||
?: outputProgressListener.onTransitionProgress(progress)
|
||||
}
|
||||
}
|
||||
|
||||
@BinderThread
|
||||
override fun onTransitionFinished() {
|
||||
executor.execute { listeners.forEach { it.onTransitionFinished() } }
|
||||
executor.execute {
|
||||
filter?.onTransitionFinished() ?: outputProgressListener.onTransitionFinished()
|
||||
}
|
||||
}
|
||||
|
||||
override fun addCallback(listener: TransitionProgressListener) {
|
||||
@@ -58,4 +81,30 @@ constructor(@UnfoldMain private val executor: Executor) :
|
||||
override fun destroy() {
|
||||
listeners.clear()
|
||||
}
|
||||
|
||||
private inner class ProcessedProgressListener : TransitionProgressListener {
|
||||
override fun onTransitionStarted() {
|
||||
log { "onTransitionStarted" }
|
||||
listeners.forEach { it.onTransitionStarted() }
|
||||
}
|
||||
|
||||
override fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {
|
||||
log { "onTransitionProgress" }
|
||||
listeners.forEach { it.onTransitionProgress(progress) }
|
||||
}
|
||||
|
||||
override fun onTransitionFinished() {
|
||||
log { "onTransitionFinished" }
|
||||
listeners.forEach { it.onTransitionFinished() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun log(s: () -> String) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, s())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG = "RemoteUnfoldReceiver"
|
||||
private val DEBUG = false
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.android.systemui.unfold.progress
|
||||
|
||||
import android.os.Trace
|
||||
import android.util.Log
|
||||
import androidx.dynamicanimation.animation.FloatPropertyCompat
|
||||
import androidx.dynamicanimation.animation.SpringAnimation
|
||||
import androidx.dynamicanimation.animation.SpringForce
|
||||
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
|
||||
|
||||
/**
|
||||
* Makes progress received from other processes resilient to jank.
|
||||
*
|
||||
* Sender and receiver processes might have different frame-rates. If the sending process is
|
||||
* dropping a frame due to jank (or generally because it's main thread is too busy), we don't want
|
||||
* the receiving process to drop progress frames as well. For this reason, a spring animator pass
|
||||
* (with very high stiffness) is applied to the incoming progress. This adds a small delay to the
|
||||
* progress (~30ms), but guarantees an always smooth animation on the receiving end.
|
||||
*/
|
||||
class UnfoldRemoteFilter(
|
||||
private val listener: UnfoldTransitionProgressProvider.TransitionProgressListener
|
||||
) : UnfoldTransitionProgressProvider.TransitionProgressListener {
|
||||
|
||||
private val springAnimation =
|
||||
SpringAnimation(this, AnimationProgressProperty).apply {
|
||||
spring =
|
||||
SpringForce().apply {
|
||||
dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
|
||||
stiffness = 100_000f
|
||||
finalPosition = 1.0f
|
||||
}
|
||||
setMinValue(0f)
|
||||
setMaxValue(1f)
|
||||
minimumVisibleChange = 0.001f
|
||||
}
|
||||
|
||||
private var inProgress = false
|
||||
|
||||
private var processedProgress: Float = 0.0f
|
||||
set(newProgress) {
|
||||
if (inProgress) {
|
||||
logCounter({ "$TAG#filtered_progress" }, newProgress)
|
||||
listener.onTransitionProgress(newProgress)
|
||||
} else {
|
||||
Log.e(TAG, "Filtered progress received received while animation not in progress.")
|
||||
}
|
||||
field = newProgress
|
||||
}
|
||||
|
||||
override fun onTransitionStarted() {
|
||||
listener.onTransitionStarted()
|
||||
inProgress = true
|
||||
}
|
||||
|
||||
override fun onTransitionProgress(progress: Float) {
|
||||
logCounter({ "$TAG#plain_remote_progress" }, progress)
|
||||
if (inProgress) {
|
||||
springAnimation.animateToFinalPosition(progress)
|
||||
} else {
|
||||
Log.e(TAG, "Progress received while not in progress.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTransitionFinished() {
|
||||
inProgress = false
|
||||
listener.onTransitionFinished()
|
||||
}
|
||||
|
||||
private object AnimationProgressProperty :
|
||||
FloatPropertyCompat<UnfoldRemoteFilter>("UnfoldRemoteFilter") {
|
||||
|
||||
override fun setValue(provider: UnfoldRemoteFilter, value: Float) {
|
||||
provider.processedProgress = value
|
||||
}
|
||||
|
||||
override fun getValue(provider: UnfoldRemoteFilter): Float = provider.processedProgress
|
||||
}
|
||||
private fun logCounter(name: () -> String, progress: Float) {
|
||||
if (DEBUG) {
|
||||
Trace.setCounter(name(), (progress * 100).toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val TAG = "UnfoldRemoteFilter"
|
||||
private val DEBUG = false
|
||||
Reference in New Issue
Block a user