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

Commit 3068b00e authored by Juan Sebastian Martinez's avatar Juan Sebastian Martinez
Browse files

Introducing haptic components to the brightness slider.

The components come in the form of a slider state tracker, a slider
event producer, and a slider state listener with concrete
implementations for the purpose of haptic feedback based on the slider
progress and velocity. The components are incorporated as a bundle
inside a BrightnessSliderHaptic plugin added to the
BrightnessSliderController. The new feature is protected by a flag.

Test: atest BrightnessSliderHapticPluginImplTest
Test: atest BrightnessSlidercontrollerTest
Bug: 295932558
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:9f288982db8e863b49c6d249c168202516874a2f)

Change-Id: I52b954b8a108a66feee524e152bcc1f882669dd0
parent 91af11ff
Loading
Loading
Loading
Loading
+65 −3
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.settings.brightness;

import static com.android.systemui.flags.Flags.HAPTIC_BRIGHTNESS_SLIDER;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@@ -30,12 +32,19 @@ import com.android.settingslib.RestrictedLockUtils;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.R;
import com.android.systemui.classifier.Classifier;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlagsClassic;
import com.android.systemui.haptics.slider.SeekableSliderEventProducer;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
import com.android.systemui.util.ViewController;
import com.android.systemui.util.time.SystemClock;

import javax.inject.Inject;

import kotlinx.coroutines.CoroutineDispatcher;

/**
 * {@code ViewController} for a {@code BrightnessSliderView}
 *
@@ -55,12 +64,21 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV
    private final FalsingManager mFalsingManager;
    private final UiEventLogger mUiEventLogger;

    private final BrightnessSliderHapticPlugin mBrightnessSliderHapticPlugin;

    private final Gefingerpoken mOnInterceptListener = new Gefingerpoken() {
        @Override
        public boolean onInterceptTouchEvent(MotionEvent 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;
@@ -75,10 +93,12 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV
    BrightnessSliderController(
            BrightnessSliderView brightnessSliderView,
            FalsingManager falsingManager,
            UiEventLogger uiEventLogger) {
            UiEventLogger uiEventLogger,
            BrightnessSliderHapticPlugin brightnessSliderHapticPlugin) {
        super(brightnessSliderView);
        mFalsingManager = falsingManager;
        mUiEventLogger = uiEventLogger;
        mBrightnessSliderHapticPlugin = brightnessSliderHapticPlugin;
    }

    /**
@@ -93,6 +113,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV
    protected void onViewAttached() {
        mView.setOnSeekBarChangeListener(mSeekListener);
        mView.setOnInterceptListener(mOnInterceptListener);
        mBrightnessSliderHapticPlugin.start();
    }

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

    @Override
@@ -204,6 +226,11 @@ 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) {
                    eventProducer.onProgressChanged(seekBar, progress, fromUser);
                }
            }
        }

@@ -213,6 +240,11 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV
            mUiEventLogger.log(BrightnessSliderEvent.SLIDER_STARTED_TRACKING_TOUCH);
            if (mListener != null) {
                mListener.onChanged(mTracking, getValue(), false);
                SeekableSliderEventProducer eventProducer =
                        mBrightnessSliderHapticPlugin.getSeekableSliderEventProducer();
                if (eventProducer != null) {
                    eventProducer.onStartTrackingTouch(seekBar);
                }
            }

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

            if (mMirrorController != null) {
@@ -242,11 +279,26 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV

        private final FalsingManager mFalsingManager;
        private final UiEventLogger mUiEventLogger;
        private final FeatureFlagsClassic mFeatureFlags;
        private final VibratorHelper mVibratorHelper;
        private final SystemClock mSystemClock;
        private final CoroutineDispatcher mMainDispatcher;

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

        /**
@@ -262,7 +314,17 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV
            int layout = getLayout();
            BrightnessSliderView root = (BrightnessSliderView) LayoutInflater.from(context)
                    .inflate(layout, viewRoot, false);
            return new BrightnessSliderController(root, mFalsingManager, mUiEventLogger);
            BrightnessSliderHapticPlugin plugin;
            if (mFeatureFlags.isEnabled(HAPTIC_BRIGHTNESS_SLIDER)) {
                plugin = new BrightnessSliderHapticPluginImpl(
                    mVibratorHelper,
                    mSystemClock,
                    mMainDispatcher
                );
            } else {
                plugin = new BrightnessSliderHapticPlugin() {};
            }
            return new BrightnessSliderController(root, mFalsingManager, mUiEventLogger, plugin);
        }

        /** Get the layout to inflate based on what slider to use */
+46 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.settings.brightness

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

    /**
     * Start the plugin.
     *
     * This starts the tracking of slider states, events and triggering of haptic feedback.
     */
    fun start() {}

    /**
     * Stop the plugin
     *
     * This stops the tracking of slider states, events and triggers of haptic feedback.
     */
    fun stop() {}
}
+69 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.settings.brightness

import android.view.VelocityTracker
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.haptics.slider.SeekableSliderEventProducer
import com.android.systemui.haptics.slider.SeekableSliderTracker
import com.android.systemui.haptics.slider.SliderHapticFeedbackProvider
import com.android.systemui.haptics.slider.SliderTracker
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.time.SystemClock
import kotlinx.coroutines.CoroutineDispatcher

/**
 * Implementation of the [BrightnessSliderHapticPlugin].
 *
 * For the specifics of the brightness slider in System UI, a [SeekableSliderEventProducer] is used
 * as the producer of slider events, a [SliderHapticFeedbackProvider] is used as the listener of
 * slider states to play haptic feedback depending on the state, and a [SeekableSliderTracker] is
 * used as the state machine handler that tracks and manipulates the slider state.
 */
class BrightnessSliderHapticPluginImpl
@JvmOverloads
constructor(
    vibratorHelper: VibratorHelper,
    systemClock: SystemClock,
    @Main mainDispatcher: CoroutineDispatcher,
    override val velocityTracker: VelocityTracker = VelocityTracker.obtain(),
    override val seekableSliderEventProducer: SeekableSliderEventProducer =
        SeekableSliderEventProducer(),
) : BrightnessSliderHapticPlugin {

    private val sliderHapticFeedbackProvider: SliderHapticFeedbackProvider =
        SliderHapticFeedbackProvider(vibratorHelper, velocityTracker, clock = systemClock)
    private val sliderTracker: SliderTracker =
        SeekableSliderTracker(
            sliderHapticFeedbackProvider,
            seekableSliderEventProducer,
            mainDispatcher,
        )

    val isTracking: Boolean
        get() = sliderTracker.isTracking

    override fun start() {
        if (!sliderTracker.isTracking) {
            sliderTracker.startTracking()
        }
    }

    override fun stop() {
        sliderTracker.stopTracking()
    }
}
+10 −2
Original line number Diff line number Diff line
@@ -40,7 +40,6 @@ import org.mockito.Mock
import org.mockito.Mockito.isNull
import org.mockito.Mockito.never
import org.mockito.Mockito.notNull
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.Mockito.`when` as whenever
@@ -61,6 +60,8 @@ class BrightnessSliderControllerTest : SysuiTestCase() {
    private lateinit var motionEvent: MotionEvent
    @Mock
    private lateinit var listener: ToggleSlider.Listener
    @Mock
    private lateinit var mBrightnessSliderHapticPlugin: BrightnessSliderHapticPlugin

    @Captor
    private lateinit var seekBarChangeCaptor: ArgumentCaptor<SeekBar.OnSeekBarChangeListener>
@@ -79,7 +80,12 @@ class BrightnessSliderControllerTest : SysuiTestCase() {
        whenever(motionEvent.copy()).thenReturn(motionEvent)

        mController =
            BrightnessSliderController(brightnessSliderView, mFalsingManager, uiEventLogger)
            BrightnessSliderController(
                brightnessSliderView,
                mFalsingManager,
                uiEventLogger,
                mBrightnessSliderHapticPlugin,
            )
        mController.init()
        mController.setOnChangedListener(listener)
    }
@@ -94,6 +100,7 @@ class BrightnessSliderControllerTest : SysuiTestCase() {
        mController.onViewAttached()

        verify(brightnessSliderView).setOnSeekBarChangeListener(notNull())
        verify(mBrightnessSliderHapticPlugin).start()
    }

    @Test
@@ -103,6 +110,7 @@ class BrightnessSliderControllerTest : SysuiTestCase() {

        verify(brightnessSliderView).setOnSeekBarChangeListener(isNull())
        verify(brightnessSliderView).setOnDispatchTouchEventListener(isNull())
        verify(mBrightnessSliderHapticPlugin).stop()
    }

    @Test
+102 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.settings.brightness

import android.view.VelocityTracker
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.haptics.slider.SeekableSliderEventProducer
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.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.MockitoAnnotations

@SmallTest
@RunWith(AndroidJUnit4::class)
class BrightnessSliderHapticPluginImplTest : SysuiTestCase() {

    @Mock private lateinit var vibratorHelper: VibratorHelper
    @Mock private lateinit var velocityTracker: VelocityTracker
    @Mock private lateinit var mainDispatcher: CoroutineDispatcher

    private val systemClock = FakeSystemClock()
    private val sliderEventProducer = SeekableSliderEventProducer()

    private lateinit var plugin: BrightnessSliderHapticPluginImpl

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        whenever(vibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(intArrayOf(0))
    }

    @Test
    fun start_beginsTrackingSlider() = runTest {
        createPlugin(UnconfinedTestDispatcher(testScheduler))
        plugin.start()

        assertThat(plugin.isTracking).isTrue()
    }

    @Test
    fun stop_stopsTrackingSlider() = runTest {
        createPlugin(UnconfinedTestDispatcher(testScheduler))
        // GIVEN that the plugin started the tracking component
        plugin.start()

        // WHEN called to stop
        plugin.stop()

        // THEN the tracking component stops
        assertThat(plugin.isTracking).isFalse()
    }

    @Test
    fun start_afterStop_startsTheTrackingAgain() = runTest {
        createPlugin(UnconfinedTestDispatcher(testScheduler))
        // GIVEN that the plugin started the tracking component
        plugin.start()

        // WHEN the plugin is restarted
        plugin.stop()
        plugin.start()

        // THEN the tracking begins again
        assertThat(plugin.isTracking).isTrue()
    }

    private fun createPlugin(dispatcher: CoroutineDispatcher) {
        plugin =
            BrightnessSliderHapticPluginImpl(
                vibratorHelper,
                systemClock,
                dispatcher,
                velocityTracker,
                sliderEventProducer,
            )
    }
}