diff options
9 files changed, 161 insertions, 2 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 6dc4f5c60108..68f4dbe53500 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -125,10 +125,10 @@ public interface SysUIComponent { default void init() { // Initialize components that have no direct tie to the dagger dependency graph, // but are critical to this component's operation - // TODO(b/205034537): I think this is a good idea? getSysUIUnfoldComponent().ifPresent(c -> { c.getUnfoldLightRevealOverlayAnimation().init(); c.getUnfoldTransitionWallpaperController().init(); + c.getUnfoldHapticsPlayer(); }); getNaturalRotationUnfoldProgressProvider().ifPresent(o -> o.init()); // No init method needed, just needs to be gotten so that it's created. diff --git a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt index 13ac39c77c93..209d93f786a2 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt @@ -92,5 +92,7 @@ interface SysUIUnfoldComponent { fun getUnfoldTransitionWallpaperController(): UnfoldTransitionWallpaperController + fun getUnfoldHapticsPlayer(): UnfoldHapticsPlayer + fun getUnfoldLightRevealOverlayAnimation(): UnfoldLightRevealOverlayAnimation } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt new file mode 100644 index 000000000000..7726d09cf971 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt @@ -0,0 +1,93 @@ +package com.android.systemui.unfold + +import android.os.SystemProperties +import android.os.VibrationEffect +import android.os.Vibrator +import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener +import javax.inject.Inject + +/** + * Class that plays a haptics effect during unfolding a foldable device + */ +@SysUIUnfoldScope +class UnfoldHapticsPlayer +@Inject +constructor( + unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider, + private val vibrator: Vibrator? +) : TransitionProgressListener { + + init { + if (vibrator != null) { + // We don't need to remove the callback because we should listen to it + // the whole time when SystemUI process is alive + unfoldTransitionProgressProvider.addCallback(this) + } + } + + private var lastTransitionProgress = TRANSITION_PROGRESS_FULL_OPEN + + override fun onTransitionStarted() { + lastTransitionProgress = TRANSITION_PROGRESS_CLOSED + } + + override fun onTransitionProgress(progress: Float) { + lastTransitionProgress = progress + } + + override fun onTransitionFinishing() { + // Run haptics only if the animation is long enough to notice + if (lastTransitionProgress < TRANSITION_NOTICEABLE_THRESHOLD) { + playHaptics() + } + } + + override fun onTransitionFinished() { + lastTransitionProgress = TRANSITION_PROGRESS_FULL_OPEN + } + + private fun playHaptics() { + vibrator?.vibrate(effect) + } + + private val hapticsScale: Float + get() { + val intensityString = SystemProperties.get("persist.unfold.haptics_scale", "0.1") + return intensityString.toFloatOrNull() ?: 0.1f + } + + private val hapticsScaleTick: Float + get() { + val intensityString = + SystemProperties.get("persist.unfold.haptics_scale_end_tick", "0.6") + return intensityString.toFloatOrNull() ?: 0.6f + } + + private val primitivesCount: Int + get() { + val count = SystemProperties.get("persist.unfold.primitives_count", "18") + return count.toIntOrNull() ?: 18 + } + + private val effect: VibrationEffect by lazy { + val composition = + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0F, 0) + + repeat(primitivesCount) { + composition.addPrimitive( + VibrationEffect.Composition.PRIMITIVE_LOW_TICK, + hapticsScale, + 0 + ) + } + + composition + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, hapticsScaleTick) + .compose() + } +} + +private const val TRANSITION_PROGRESS_CLOSED = 0f +private const val TRANSITION_PROGRESS_FULL_OPEN = 1f +private const val TRANSITION_NOTICEABLE_THRESHOLD = 0.9f diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt index 03fd624a8c1d..abbdab0d41f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt @@ -69,6 +69,22 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() { } @Test + fun testUnfold_emitsFinishingEvent() { + 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()) { + assertHasSingleFinishingEvent() + } + } + + @Test fun testUnfold_screenAvailableOnlyAfterFullUnfold_emitsIncreasingTransitionEvents() { runOnMainThreadWithInterval( { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_OPENING) }, @@ -157,6 +173,12 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() { currentRecording!!.addProgress(progress) } + override fun onTransitionFinishing() { + assertWithMessage("Received transition finishing event when it's not started") + .that(currentRecording).isNotNull() + currentRecording!!.onFinishing() + } + override fun onTransitionFinished() { assertWithMessage("Received transition finish event when it's not started") .that(currentRecording).isNotNull() @@ -171,6 +193,7 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() { class UnfoldTransitionRecording { private val progressHistory: MutableList<Float> = arrayListOf() + private var finishingInvocations: Int = 0 fun addProgress(progress: Float) { assertThat(progress).isAtMost(1.0f) @@ -179,6 +202,10 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() { progressHistory += progress } + fun onFinishing() { + finishingInvocations++ + } + fun assertIncreasingProgress() { assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS) assertThat(progressHistory).isInOrder() @@ -206,6 +233,11 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() { .isInOrder(Comparator.reverseOrder<Float>()) assertThat(progressHistory.last()).isEqualTo(0.0f) } + + fun assertHasSingleFinishingEvent() { + assertWithMessage("onTransitionFinishing callback should be invoked exactly " + + "one time").that(finishingInvocations).isEqualTo(1) + } } private companion object { diff --git a/packages/SystemUI/unfold/Android.bp b/packages/SystemUI/unfold/Android.bp index 108295b90e58..180b611aa13b 100644 --- a/packages/SystemUI/unfold/Android.bp +++ b/packages/SystemUI/unfold/Android.bp @@ -33,6 +33,7 @@ android_library { "dagger2", "jsr330", ], + kotlincflags: ["-Xjvm-default=enable"], java_version: "1.8", min_sdk_version: "current", plugins: ["dagger2-compiler"], diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt index 7117aafba54a..fee485d97afa 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt @@ -34,8 +34,28 @@ interface UnfoldTransitionProgressProvider : CallbackController<TransitionProgre fun destroy() interface TransitionProgressListener { + /** Called when transition is started */ + @JvmDefault fun onTransitionStarted() {} - fun onTransitionFinished() {} + + /** + * Called whenever transition progress is updated, [progress] is a value of the animation + * where 0 is fully folded, 1 is fully unfolded + */ + @JvmDefault fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {} + + /** + * Called when the progress provider determined that the transition is about to finish soon. + * + * For example, in [PhysicsBasedUnfoldTransitionProgressProvider] this could happen when the + * animation is not tied to the hinge angle anymore and it is about to run fixed animation. + */ + @JvmDefault + fun onTransitionFinishing() {} + + /** Called when transition is completely finished */ + @JvmDefault + fun onTransitionFinished() {} } } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt index 4c85b055aeae..fa59cb4d12be 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt @@ -88,6 +88,7 @@ internal class FixedTimingTransitionProgressProvider( override fun onAnimationStart(animator: Animator) { listeners.forEach { it.onTransitionStarted() } + listeners.forEach { it.onTransitionFinishing() } } override fun onAnimationEnd(animator: Animator) { diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt index b56818693124..24394ed02823 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt @@ -124,6 +124,10 @@ class PhysicsBasedUnfoldTransitionProgressProvider( private fun cancelTransition(endValue: Float, animate: Boolean) { if (isTransitionRunning && animate) { + if (endValue == 1.0f && !isAnimatedCancelRunning) { + listeners.forEach { it.onTransitionFinishing() } + } + isAnimatedCancelRunning = true springAnimation.animateToFinalPosition(endValue) } else { diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProvider.kt index 8491f832b740..b7bab3e5ed5a 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProvider.kt @@ -110,6 +110,12 @@ constructor(source: UnfoldTransitionProgressProvider? = null) : lastTransitionProgress = progress } + override fun onTransitionFinishing() { + if (isReadyToHandleTransition) { + listeners.forEach { it.onTransitionFinishing() } + } + } + override fun onTransitionFinished() { if (isReadyToHandleTransition) { listeners.forEach { it.onTransitionFinished() } |