diff options
7 files changed, 285 insertions, 11 deletions
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiverTest.kt index 0e7e039e69e2..4989a21791fb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiverTest.kt @@ -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) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt index f6532070d720..1ce25725b298 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt @@ -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) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt new file mode 100644 index 000000000000..f14009aad033 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt @@ -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) + } + } +} diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldRemoteModule.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldRemoteModule.kt index b395d9c07662..a639df539cb9 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldRemoteModule.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldRemoteModule.kt @@ -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 } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UseReceivingFilter.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UseReceivingFilter.kt new file mode 100644 index 000000000000..60e9307e8457 --- /dev/null +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UseReceivingFilter.kt @@ -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 diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiver.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiver.kt index 5e4bcc97520e..b2c26c455b76 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiver.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/RemoteUnfoldTransitionReceiver.kt @@ -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 diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt new file mode 100644 index 000000000000..0b019d1285e3 --- /dev/null +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/UnfoldRemoteFilter.kt @@ -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 |