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