diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProvider.kt new file mode 100644 index 0000000000000..495d3a13d9612 --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProvider.kt @@ -0,0 +1,63 @@ +/* + * 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.util + +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.unfold.UnfoldTransitionProgressProvider +import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener +import com.android.systemui.unfold.updates.FoldProvider +import com.android.systemui.unfold.updates.FoldProvider.FoldCallback +import java.util.concurrent.Executor + +/** + * [UnfoldTransitionProgressProvider] that emits transition progress only when unfolding but not + * when folding, so we can play the animation only one way but not the other way. + */ +class UnfoldOnlyProgressProvider( + foldProvider: FoldProvider, + @Main private val executor: Executor, + private val sourceProvider: UnfoldTransitionProgressProvider, + private val scopedProvider: ScopedUnfoldTransitionProgressProvider = + ScopedUnfoldTransitionProgressProvider(sourceProvider) +) : UnfoldTransitionProgressProvider by scopedProvider { + + private var isFolded = false + + init { + foldProvider.registerCallback(FoldListener(), executor) + sourceProvider.addCallback(SourceTransitionListener()) + } + + private inner class SourceTransitionListener : TransitionProgressListener { + override fun onTransitionFinished() { + // Disable scoped progress provider after the first unfold animation, so fold animation + // will not be propagated. It will be re-enabled after folding so we can play + // the unfold animation again. + if (!isFolded) { + scopedProvider.setReadyToHandleTransition(false) + } + } + } + + private inner class FoldListener : FoldCallback { + override fun onFoldUpdated(isFolded: Boolean) { + if (isFolded) { + scopedProvider.setReadyToHandleTransition(true) + } + + this@UnfoldOnlyProgressProvider.isFolded = isFolded + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt index 2709da38a7d83..992b0221068ca 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt @@ -18,16 +18,19 @@ package com.android.systemui.unfold import android.content.Context import android.hardware.devicestate.DeviceStateManager +import android.os.SystemProperties import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.LifecycleScreenStatusProvider import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.system.SystemUnfoldSharedModule +import com.android.systemui.unfold.updates.FoldProvider import com.android.systemui.unfold.updates.FoldStateProvider import com.android.systemui.unfold.updates.RotationChangeProvider import com.android.systemui.unfold.updates.screen.ScreenStatusProvider import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider +import com.android.systemui.unfold.util.UnfoldOnlyProgressProvider import com.android.systemui.unfold.util.UnfoldTransitionATracePrefix import com.android.systemui.util.time.SystemClockImpl import com.android.wm.shell.unfold.ShellUnfoldProgressProvider @@ -37,6 +40,7 @@ import dagger.Provides import java.util.Optional import java.util.concurrent.Executor import javax.inject.Named +import javax.inject.Provider import javax.inject.Singleton @Module(includes = [UnfoldSharedModule::class, SystemUnfoldSharedModule::class]) @@ -90,6 +94,18 @@ class UnfoldTransitionModule { NaturalRotationUnfoldProgressProvider(context, rotationChangeProvider, provider) } + @Provides + @Singleton + @Named(UNFOLD_ONLY_PROVIDER) + fun provideUnfoldOnlyProvider( + foldProvider: FoldProvider, + @Main executor: Executor, + sourceProvider: Optional + ): Optional = + sourceProvider.map { provider -> + UnfoldOnlyProgressProvider(foldProvider, executor, provider) + } + @Provides @Named(UNFOLD_STATUS_BAR) @Singleton @@ -102,16 +118,35 @@ class UnfoldTransitionModule { @Singleton fun provideShellProgressProvider( config: UnfoldTransitionConfig, - provider: Optional - ): ShellUnfoldProgressProvider = - if (config.isEnabled && provider.isPresent) { - UnfoldProgressProvider(provider.get()) - } else { - ShellUnfoldProgressProvider.NO_PROVIDER - } + provider: Provider>, + @Named(UNFOLD_ONLY_PROVIDER) + unfoldOnlyProvider: Provider> + ): ShellUnfoldProgressProvider { + val resultingProvider = + if (config.isEnabled) { + // Return unfold only provider to the shell if we don't want to animate tasks during + // folding. Shell provider listeners are responsible for animating task bounds. + if (ENABLE_FOLD_TASK_ANIMATIONS) { + provider + } else { + unfoldOnlyProvider + } + } else { + null + } + + return resultingProvider?.get()?.orElse(null)?.let(::UnfoldProgressProvider) + ?: ShellUnfoldProgressProvider.NO_PROVIDER + } @Provides fun screenStatusProvider(impl: LifecycleScreenStatusProvider): ScreenStatusProvider = impl } const val UNFOLD_STATUS_BAR = "unfold_status_bar" +const val UNFOLD_ONLY_PROVIDER = "unfold_only_provider" + +// TODO: b/265764985 - tracking bug to clean-up the flag +// FeatureFlags are not accessible here because it's a global submodule (see GlobalModule.java) +private val ENABLE_FOLD_TASK_ANIMATIONS = + SystemProperties.getBoolean("persist.unfold.enable_fold_tasks_animation", false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt index 4a28cd1de255e..56c6245659711 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt @@ -4,7 +4,7 @@ import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionPr class TestUnfoldTransitionProvider : UnfoldTransitionProgressProvider, TransitionProgressListener { - private val listeners = arrayListOf() + private val listeners = mutableListOf() override fun destroy() { listeners.clear() diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt index d3fdbd94a5ac3..3dec45b4ff9f3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt @@ -20,10 +20,9 @@ import android.os.Vibrator import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.unfold.updates.FoldProvider +import com.android.systemui.unfold.util.TestFoldProvider import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock -import java.util.concurrent.Executor import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -115,20 +114,4 @@ class UnfoldHapticsPlayerTest : SysuiTestCase() { verify(vibrator).vibrate(any()) } - - private class TestFoldProvider : FoldProvider { - private val listeners = arrayListOf() - - override fun registerCallback(callback: FoldProvider.FoldCallback, executor: Executor) { - listeners += callback - } - - override fun unregisterCallback(callback: FoldProvider.FoldCallback) { - listeners -= callback - } - - fun onFoldUpdate(isFolded: Boolean) { - listeners.forEach { it.onFoldUpdated(isFolded) } - } - } } 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 e2aef31b4f10d..e461e3f7fb1ae 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 @@ -74,6 +74,11 @@ class TestUnfoldProgressListener : UnfoldTransitionProgressProvider.TransitionPr currentRecording?.assertLastProgress(progress) ?: error("unfold not in progress.") } + fun clear() { + currentRecording = null + recordings.clear() + } + class UnfoldTransitionRecording { private val progressHistory: MutableList = arrayListOf() private var finishingInvocations: Int = 0 diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/TestFoldProvider.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/TestFoldProvider.kt new file mode 100644 index 0000000000000..35df35ccfe9cb --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/TestFoldProvider.kt @@ -0,0 +1,34 @@ +/* + * 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.util + +import com.android.systemui.unfold.updates.FoldProvider +import java.util.concurrent.Executor + +class TestFoldProvider : FoldProvider { + private val listeners = arrayListOf() + + override fun registerCallback(callback: FoldProvider.FoldCallback, executor: Executor) { + listeners += callback + } + + override fun unregisterCallback(callback: FoldProvider.FoldCallback) { + listeners -= callback + } + + fun onFoldUpdate(isFolded: Boolean) { + listeners.forEach { it.onFoldUpdated(isFolded) } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProviderTest.kt new file mode 100644 index 0000000000000..4a38fc069d9f4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProviderTest.kt @@ -0,0 +1,128 @@ +/* + * 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.util + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.unfold.TestUnfoldTransitionProvider +import com.android.systemui.unfold.progress.TestUnfoldProgressListener +import com.google.common.util.concurrent.MoreExecutors +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class UnfoldOnlyProgressProviderTest : SysuiTestCase() { + + private val listener = TestUnfoldProgressListener() + private val sourceProvider = TestUnfoldTransitionProvider() + + private val foldProvider = TestFoldProvider() + + private lateinit var progressProvider: UnfoldOnlyProgressProvider + + @Before + fun setUp() { + progressProvider = + UnfoldOnlyProgressProvider(foldProvider, MoreExecutors.directExecutor(), sourceProvider) + + progressProvider.addCallback(listener) + } + + @Test + fun unfolded_unfoldAnimationFinished_propagatesEvents() { + foldProvider.onFoldUpdate(isFolded = true) + foldProvider.onFoldUpdate(isFolded = false) + + // Unfold animation + sourceProvider.onTransitionStarted() + sourceProvider.onTransitionProgress(0.5f) + sourceProvider.onTransitionFinished() + + with(listener.ensureTransitionFinished()) { + assertLastProgress(0.5f) + } + } + + @Test + fun unfoldedWithAnimation_foldAnimation_doesNotPropagateEvents() { + foldProvider.onFoldUpdate(isFolded = true) + foldProvider.onFoldUpdate(isFolded = false) + // Unfold animation + sourceProvider.onTransitionStarted() + sourceProvider.onTransitionProgress(0.5f) + sourceProvider.onTransitionFinished() + listener.clear() + + // Fold animation + sourceProvider.onTransitionStarted() + sourceProvider.onTransitionProgress(0.2f) + sourceProvider.onTransitionFinished() + + listener.assertNotStarted() + } + + @Test + fun unfoldedWithAnimation_foldAnimationSeveralTimes_doesNotPropagateEvents() { + foldProvider.onFoldUpdate(isFolded = true) + foldProvider.onFoldUpdate(isFolded = false) + // Unfold animation + sourceProvider.onTransitionStarted() + sourceProvider.onTransitionProgress(0.5f) + sourceProvider.onTransitionFinished() + listener.clear() + + // Start and stop fold animation several times + repeat(3) { + sourceProvider.onTransitionStarted() + sourceProvider.onTransitionProgress(0.2f) + sourceProvider.onTransitionFinished() + } + + listener.assertNotStarted() + } + + @Test + fun unfoldedAgainAfterFolding_propagatesEvents() { + foldProvider.onFoldUpdate(isFolded = true) + foldProvider.onFoldUpdate(isFolded = false) + + // Unfold animation + sourceProvider.onTransitionStarted() + sourceProvider.onTransitionProgress(0.5f) + sourceProvider.onTransitionFinished() + + // Fold animation + sourceProvider.onTransitionStarted() + sourceProvider.onTransitionProgress(0.2f) + sourceProvider.onTransitionFinished() + foldProvider.onFoldUpdate(isFolded = true) + + listener.clear() + + // Second unfold animation after folding + foldProvider.onFoldUpdate(isFolded = false) + sourceProvider.onTransitionStarted() + sourceProvider.onTransitionProgress(0.1f) + sourceProvider.onTransitionFinished() + + with(listener.ensureTransitionFinished()) { + assertLastProgress(0.1f) + } + } +}