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

Commit e932b949 authored by Juan Sebastian Martinez's avatar Juan Sebastian Martinez Committed by Android (Google) Code Review
Browse files

Merge "View-Binder architecture on haptic sliders." into main

parents cdae86f3 8cd665b9
Loading
Loading
Loading
Loading
+6 −8
Original line number Diff line number Diff line
@@ -26,7 +26,6 @@ import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
@@ -75,7 +74,7 @@ class SeekableSliderHapticPluginTest : SysuiTestCase() {
    fun start_afterStop_startsTheTrackingAgain() = runOnStartedPlugin {
        // WHEN the plugin is restarted
        plugin.stop()
        plugin.start()
        plugin.startInScope(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))

        // THEN the tracking begins again
        assertThat(plugin.isTracking).isTrue()
@@ -131,22 +130,21 @@ class SeekableSliderHapticPluginTest : SysuiTestCase() {
    private fun runOnStartedPlugin(test: suspend TestScope.() -> Unit) =
        with(kosmos) {
            testScope.runTest {
                createPlugin(this, UnconfinedTestDispatcher(testScheduler))
                // GIVEN that the plugin is started
                plugin.start()
                val pluginScope = CoroutineScope(UnconfinedTestDispatcher(testScheduler))
                createPlugin()
                // GIVEN that the plugin is started in a test scope
                plugin.startInScope(pluginScope)

                // THEN run the test
                test()
            }
        }

    private fun createPlugin(scope: CoroutineScope, dispatcher: CoroutineDispatcher) {
    private fun createPlugin() {
        plugin =
            SeekableSliderHapticPlugin(
                vibratorHelper,
                kosmos.fakeSystemClock,
                dispatcher,
                scope,
            )
    }

+40 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 * Copyright (C) 2024 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.
@@ -14,33 +14,27 @@
 * limitations under the License.
 */

package com.android.systemui.settings.brightness
package com.android.systemui.haptics.slider

import android.view.VelocityTracker
import com.android.systemui.haptics.slider.SeekableSliderEventProducer

/** Plugin component for the System UI brightness slider to incorporate dynamic haptics */
interface BrightnessSliderHapticPlugin {

    /** Finger velocity tracker */
    val velocityTracker: VelocityTracker?
        get() = null

    /** Producer of slider events from the underlying [android.widget.SeekBar] */
    val seekableSliderEventProducer: SeekableSliderEventProducer?
        get() = null
import android.view.View
import androidx.lifecycle.lifecycleScope
import com.android.systemui.lifecycle.repeatWhenAttached
import kotlinx.coroutines.awaitCancellation

object HapticSliderViewBinder {
    /**
     * Start the plugin.
     *
     * This starts the tracking of slider states, events and triggering of haptic feedback.
     * Binds a [SeekableSliderHapticPlugin] to a [View]. The binded view should be a
     * [android.widget.SeekBar] or a container of a [android.widget.SeekBar]
     */
    fun start() {}

    /**
     * Stop the plugin
     *
     * This stops the tracking of slider states, events and triggers of haptic feedback.
     */
    fun stop() {}
    @JvmStatic
    fun bind(view: View?, plugin: SeekableSliderHapticPlugin) {
        view?.repeatWhenAttached {
            plugin.startInScope(lifecycleScope)
            try {
                awaitCancellation()
            } finally {
                plugin.stop()
            }
        }
    }
}
+25 −28
Original line number Diff line number Diff line
@@ -20,11 +20,8 @@ import android.view.MotionEvent
import android.view.VelocityTracker
import android.widget.SeekBar
import androidx.annotation.VisibleForTesting
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.time.SystemClock
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -43,10 +40,8 @@ class SeekableSliderHapticPlugin
constructor(
    vibratorHelper: VibratorHelper,
    systemClock: SystemClock,
    @Main private val mainDispatcher: CoroutineDispatcher,
    @Application private val applicationScope: CoroutineScope,
    sliderHapticFeedbackConfig: SliderHapticFeedbackConfig = SliderHapticFeedbackConfig(),
    sliderTrackerConfig: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
    private val sliderTrackerConfig: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
) {

    private val velocityTracker = VelocityTracker.obtain()
@@ -61,19 +56,15 @@ constructor(
            systemClock,
        )

    private val sliderTracker =
        SeekableSliderTracker(
            sliderHapticFeedbackProvider,
            sliderEventProducer,
            mainDispatcher,
            sliderTrackerConfig,
        )
    private var sliderTracker: SeekableSliderTracker? = null

    private var pluginScope: CoroutineScope? = null

    val isTracking: Boolean
        get() = sliderTracker.isTracking
        get() = sliderTracker?.isTracking == true

    val trackerState: SliderState
        get() = sliderTracker.currentState
    val trackerState: SliderState?
        get() = sliderTracker?.currentState

    /**
     * A waiting [Job] for a timer that estimates the key-up event when a key-down event is
@@ -89,14 +80,20 @@ constructor(
        get() = keyUpJob != null && keyUpJob?.isActive == true

    /**
     * Start the plugin.
     *
     * This starts the tracking of slider states, events and triggering of haptic feedback.
     * Specify the scope for the plugin's operations and start the slider tracker in this scope.
     * This also involves the key-up timer job.
     */
    fun start() {
        if (!isTracking) {
            sliderTracker.startTracking()
        }
    fun startInScope(scope: CoroutineScope) {
        if (sliderTracker != null) stop()
        sliderTracker =
            SeekableSliderTracker(
                sliderHapticFeedbackProvider,
                sliderEventProducer,
                scope,
                sliderTrackerConfig,
            )
        pluginScope = scope
        sliderTracker?.startTracking()
    }

    /**
@@ -104,7 +101,7 @@ constructor(
     *
     * This stops the tracking of slider states, events and triggers of haptic feedback.
     */
    fun stop() = sliderTracker.stopTracking()
    fun stop() = sliderTracker?.stopTracking()

    /** React to a touch event */
    fun onTouchEvent(event: MotionEvent?) {
@@ -147,9 +144,9 @@ constructor(
    /**
     * An external key was pressed (e.g., a volume key).
     *
     * This event is used to estimate the key-up event based on by running a timer as a waiting
     * coroutine in the [keyUpTimerScope]. A key-up event in a slider corresponds to an onArrowUp
     * event. Therefore, [onArrowUp] must be called after the timeout.
     * This event is used to estimate the key-up event based on a running a timer as a waiting
     * coroutine in the [pluginScope]. A key-up event in a slider corresponds to an onArrowUp event.
     * Therefore, [onArrowUp] must be called after the timeout.
     */
    fun onKeyDown() {
        if (!isTracking) return
@@ -159,7 +156,7 @@ constructor(
            keyUpJob?.cancel()
        }
        keyUpJob =
            applicationScope.launch {
            pluginScope?.launch {
                delay(KEY_UP_TIMEOUT)
                onArrowUp()
            }
+5 −8
Original line number Diff line number Diff line
@@ -17,9 +17,7 @@
package com.android.systemui.haptics.slider

import androidx.annotation.VisibleForTesting
import com.android.systemui.dagger.qualifiers.Main
import kotlin.math.abs
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -31,21 +29,20 @@ import kotlinx.coroutines.launch
 *
 * The tracker runs a state machine to execute actions on touch-based events typical of a seekable
 * slider such as [android.widget.SeekBar]. Coroutines responsible for running the state machine,
 * collecting slider events and maintaining waiting states are run on the main thread via the
 * [com.android.systemui.dagger.qualifiers.Main] coroutine dispatcher.
 * collecting slider events and maintaining waiting states are run on the provided [CoroutineScope].
 *
 * @param[sliderStateListener] Listener of the slider state.
 * @param[sliderEventProducer] Producer of slider events arising from the slider.
 * @param[mainDispatcher] [CoroutineDispatcher] used to launch coroutines for the collection of
 *   slider events and the launch of timer jobs.
 * @param[trackerScope] [CoroutineScope] used to launch coroutines for the collection of slider
 *   events and the launch of timer jobs.
 * @property[config] Configuration parameters of the slider tracker.
 */
class SeekableSliderTracker(
    sliderStateListener: SliderStateListener,
    sliderEventProducer: SliderEventProducer,
    @Main mainDispatcher: CoroutineDispatcher,
    trackerScope: CoroutineScope,
    private val config: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
) : SliderTracker(CoroutineScope(mainDispatcher), sliderStateListener, sliderEventProducer) {
) : SliderTracker(trackerScope, sliderStateListener, sliderEventProducer) {

    // History of the latest progress collected from slider events
    private var latestProgress = 0f
+14 −42
Original line number Diff line number Diff line
@@ -31,8 +31,8 @@ import com.android.internal.logging.UiEventLogger;
import com.android.settingslib.RestrictedLockUtils;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.classifier.Classifier;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.haptics.slider.SeekableSliderEventProducer;
import com.android.systemui.haptics.slider.HapticSliderViewBinder;
import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.VibratorHelper;
@@ -42,8 +42,6 @@ import com.android.systemui.util.time.SystemClock;

import javax.inject.Inject;

import kotlinx.coroutines.CoroutineDispatcher;

/**
 * {@code ViewController} for a {@code BrightnessSliderView}
 *
@@ -63,23 +61,16 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV
    private final FalsingManager mFalsingManager;
    private final UiEventLogger mUiEventLogger;

    private final BrightnessSliderHapticPlugin mBrightnessSliderHapticPlugin;
    private final SeekableSliderHapticPlugin mBrightnessSliderHapticPlugin;

    private final Gefingerpoken mOnInterceptListener = new Gefingerpoken() {
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            mBrightnessSliderHapticPlugin.onTouchEvent(ev);
            int action = ev.getActionMasked();
            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
                mFalsingManager.isFalseTouch(Classifier.BRIGHTNESS_SLIDER);
                if (mBrightnessSliderHapticPlugin.getVelocityTracker() != null) {
                    mBrightnessSliderHapticPlugin.getVelocityTracker().clear();
                }
            } else if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) {
                if (mBrightnessSliderHapticPlugin.getVelocityTracker() != null) {
                    mBrightnessSliderHapticPlugin.getVelocityTracker().addMovement(ev);
                }
            }

            return false;
        }

@@ -93,7 +84,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV
            BrightnessSliderView brightnessSliderView,
            FalsingManager falsingManager,
            UiEventLogger uiEventLogger,
            BrightnessSliderHapticPlugin brightnessSliderHapticPlugin) {
            SeekableSliderHapticPlugin brightnessSliderHapticPlugin) {
        super(brightnessSliderView);
        mFalsingManager = falsingManager;
        mUiEventLogger = uiEventLogger;
@@ -112,7 +103,6 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV
    protected void onViewAttached() {
        mView.setOnSeekBarChangeListener(mSeekListener);
        mView.setOnInterceptListener(mOnInterceptListener);
        mBrightnessSliderHapticPlugin.start();
    }

    @Override
@@ -120,7 +110,6 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV
        mView.setOnSeekBarChangeListener(null);
        mView.setOnDispatchTouchEventListener(null);
        mView.setOnInterceptListener(null);
        mBrightnessSliderHapticPlugin.stop();
    }

    @Override
@@ -225,10 +214,8 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            if (mListener != null) {
                mListener.onChanged(mTracking, progress, false);
                SeekableSliderEventProducer eventProducer =
                        mBrightnessSliderHapticPlugin.getSeekableSliderEventProducer();
                if (eventProducer != null && fromUser) {
                    eventProducer.onProgressChanged(seekBar, progress, fromUser);
                if (fromUser) {
                    mBrightnessSliderHapticPlugin.onProgressChanged(seekBar, progress, fromUser);
                }
            }
        }
@@ -239,11 +226,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV
            mUiEventLogger.log(BrightnessSliderEvent.BRIGHTNESS_SLIDER_STARTED_TRACKING_TOUCH);
            if (mListener != null) {
                mListener.onChanged(mTracking, getValue(), false);
                SeekableSliderEventProducer eventProducer =
                        mBrightnessSliderHapticPlugin.getSeekableSliderEventProducer();
                if (eventProducer != null) {
                    eventProducer.onStartTrackingTouch(seekBar);
                }
                mBrightnessSliderHapticPlugin.onStartTrackingTouch(seekBar);
            }

            if (mMirrorController != null) {
@@ -258,11 +241,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV
            mUiEventLogger.log(BrightnessSliderEvent.BRIGHTNESS_SLIDER_STOPPED_TRACKING_TOUCH);
            if (mListener != null) {
                mListener.onChanged(mTracking, getValue(), true);
                SeekableSliderEventProducer eventProducer =
                        mBrightnessSliderHapticPlugin.getSeekableSliderEventProducer();
                if (eventProducer != null) {
                    eventProducer.onStopTrackingTouch(seekBar);
                }
                mBrightnessSliderHapticPlugin.onStopTrackingTouch(seekBar);
            }

            if (mMirrorController != null) {
@@ -280,21 +259,18 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV
        private final UiEventLogger mUiEventLogger;
        private final VibratorHelper mVibratorHelper;
        private final SystemClock mSystemClock;
        private final CoroutineDispatcher mMainDispatcher;

        @Inject
        public Factory(
                FalsingManager falsingManager,
                UiEventLogger uiEventLogger,
                VibratorHelper vibratorHelper,
                SystemClock clock,
                @Main CoroutineDispatcher mainDispatcher
                SystemClock clock
        ) {
            mFalsingManager = falsingManager;
            mUiEventLogger = uiEventLogger;
            mVibratorHelper = vibratorHelper;
            mSystemClock = clock;
            mMainDispatcher = mainDispatcher;
        }

        /**
@@ -310,15 +286,11 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV
            int layout = getLayout();
            BrightnessSliderView root = (BrightnessSliderView) LayoutInflater.from(context)
                    .inflate(layout, viewRoot, false);
            BrightnessSliderHapticPlugin plugin;
            if (hapticBrightnessSlider()) {
                plugin = new BrightnessSliderHapticPluginImpl(
            SeekableSliderHapticPlugin plugin = new SeekableSliderHapticPlugin(
                    mVibratorHelper,
                    mSystemClock,
                    mMainDispatcher
                );
            } else {
                plugin = new BrightnessSliderHapticPlugin() {};
                    mSystemClock);
            if (hapticBrightnessSlider()) {
                HapticSliderViewBinder.bind(viewRoot, plugin);
            }
            return new BrightnessSliderController(root, mFalsingManager, mUiEventLogger, plugin);
        }
Loading