From 61e549a2322b9dde025ee92a7f1460f9da05811c Mon Sep 17 00:00:00 2001 From: Nick Chameyev Date: Mon, 7 Nov 2022 12:11:23 +0000 Subject: [PATCH] [Unfold transition] Add haptics effect Adds haptics effect when the animation is about to end. The effect is played when the animation is cancelled because of timeout or when the device is unfolded quickly. Bug: 200555479 Test: atest com.android.systemui.unfold.progress.PhysicsBasedUnfoldTransitionProgressProviderTest Test: manual test Change-Id: I0d6c8098b1c86e37547793644bf450d69c166c50 --- .../systemui/dagger/SysUIComponent.java | 2 +- .../systemui/unfold/SysUIUnfoldModule.kt | 2 + .../systemui/unfold/UnfoldHapticsPlayer.kt | 93 +++++++++++++++++++ ...sedUnfoldTransitionProgressProviderTest.kt | 32 +++++++ packages/SystemUI/unfold/Android.bp | 1 + .../UnfoldTransitionProgressProvider.kt | 22 ++++- .../FixedTimingTransitionProgressProvider.kt | 1 + ...csBasedUnfoldTransitionProgressProvider.kt | 4 + .../ScopedUnfoldTransitionProgressProvider.kt | 6 ++ 9 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 6dc4f5c601088..68f4dbe535007 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 13ac39c77c938..209d93f786a29 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 0000000000000..7726d09cf971e --- /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 03fd624a8c1de..abbdab0d41f54 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 @@ -68,6 +68,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( @@ -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 = 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()) 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 108295b90e58d..180b611aa13bb 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 7117aafba54a9..fee485d97afa3 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