Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 61e549a2 authored by Nick Chameyev's avatar Nick Chameyev
Browse files

[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
parent 67c61bf5
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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.
+2 −0
Original line number Diff line number Diff line
@@ -92,5 +92,7 @@ interface SysUIUnfoldComponent {

    fun getUnfoldTransitionWallpaperController(): UnfoldTransitionWallpaperController

    fun getUnfoldHapticsPlayer(): UnfoldHapticsPlayer

    fun getUnfoldLightRevealOverlayAnimation(): UnfoldLightRevealOverlayAnimation
}
+93 −0
Original line number Diff line number Diff line
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
+32 −0
Original line number Diff line number Diff line
@@ -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<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 {
+1 −0
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ android_library {
        "dagger2",
        "jsr330",
    ],
    kotlincflags: ["-Xjvm-default=enable"],
    java_version: "1.8",
    min_sdk_version: "current",
    plugins: ["dagger2-compiler"],
Loading