diff options
| author | 2024-01-23 16:08:22 +0000 | |
|---|---|---|
| committer | 2024-01-23 16:08:22 +0000 | |
| commit | a04ccd195fd4db19a5da0549eee83df3352507ac (patch) | |
| tree | 67a9ef4345bb31582f9c64b09aeb9ee7a5c0faa9 | |
| parent | 6c7dbe9a5a7420b3557d2dd04845aa99df02fc15 (diff) | |
| parent | 86b52bf84cc53ae05660d7e6dded8cf65c8626a7 (diff) | |
Merge "Introducing the SeekableSliderHapticPlugin for volume slider haptics." into main
7 files changed, 493 insertions, 7 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt new file mode 100644 index 000000000000..ea766f8ea9bb --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt @@ -0,0 +1,156 @@ +/* + * 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. + * 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.haptics.slider + +import android.widget.SeekBar +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +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 +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +@SmallTest +@RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) +class SeekableSliderHapticPluginTest : SysuiTestCase() { + + private val kosmos = Kosmos() + + @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule() + @Mock private lateinit var vibratorHelper: VibratorHelper + private val seekBar = SeekBar(mContext) + private lateinit var plugin: SeekableSliderHapticPlugin + + @Before + fun setup() { + whenever(vibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(intArrayOf(0)) + } + + @Test + fun start_beginsTrackingSlider() = runOnStartedPlugin { assertThat(plugin.isTracking).isTrue() } + + @Test + fun stop_stopsTrackingSlider() = runOnStartedPlugin { + // WHEN called to stop + plugin.stop() + + // THEN stops tracking + assertThat(plugin.isTracking).isFalse() + } + + @Test + fun start_afterStop_startsTheTrackingAgain() = runOnStartedPlugin { + // WHEN the plugin is restarted + plugin.stop() + plugin.start() + + // THEN the tracking begins again + assertThat(plugin.isTracking).isTrue() + } + + @Test + fun onKeyDown_startsWaiting() = runOnStartedPlugin { + // WHEN a keyDown event is recorded + plugin.onKeyDown() + + // THEN the timer starts waiting + assertThat(plugin.isKeyUpTimerWaiting).isTrue() + } + + @Test + fun keyUpWaitComplete_triggersOnArrowUp() = runOnStartedPlugin { + // GIVEN an onKeyDown that starts the wait and a program progress change that advances the + // slider state to ARROW_HANDLE_MOVED_ONCE + plugin.onKeyDown() + plugin.onProgressChanged(seekBar, 50, false) + testScheduler.runCurrent() + assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE) + + // WHEN the key-up wait completes after the timeout plus a small buffer + advanceTimeBy(KEY_UP_TIMEOUT + 10L) + + // THEN the onArrowUp event is delivered causing the slider tracker to move to IDLE + assertThat(plugin.trackerState).isEqualTo(SliderState.IDLE) + assertThat(plugin.isKeyUpTimerWaiting).isFalse() + } + + @Test + fun onKeyDown_whileWaiting_restartsWait() = runOnStartedPlugin { + // GIVEN an onKeyDown that starts the wait and a program progress change that advances the + // slider state to ARROW_HANDLE_MOVED_ONCE + plugin.onKeyDown() + plugin.onProgressChanged(seekBar, 50, false) + testScheduler.runCurrent() + assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE) + + // WHEN half the timeout period has elapsed and a new keyDown event occurs + advanceTimeBy(KEY_UP_TIMEOUT / 2) + plugin.onKeyDown() + + // AFTER advancing by a period of time that should have complete the original wait + advanceTimeBy(KEY_UP_TIMEOUT / 2 + 10L) + + // THEN the timer is still waiting and the slider tracker remains on ARROW_HANDLE_MOVED_ONCE + assertThat(plugin.isKeyUpTimerWaiting).isTrue() + assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE) + } + + private fun runOnStartedPlugin(test: suspend TestScope.() -> Unit) = + with(kosmos) { + testScope.runTest { + createPlugin(this, UnconfinedTestDispatcher(testScheduler)) + // GIVEN that the plugin is started + plugin.start() + + // THEN run the test + test() + } + } + + private fun createPlugin(scope: CoroutineScope, dispatcher: CoroutineDispatcher) { + plugin = + SeekableSliderHapticPlugin( + vibratorHelper, + kosmos.fakeSystemClock, + dispatcher, + scope, + ) + } + + companion object { + private const val KEY_UP_TIMEOUT = 100L + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java index 3d9645a3d983..b1736b16875d 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java @@ -227,5 +227,10 @@ public interface VolumeDialogController { void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkBeforeSwitch); // requires version 2 void onShowCsdWarning(@AudioManager.CsdWarning int csdWarning, int durationMs); + + /** + * Callback function for when the volume changed due to a physical key press. + */ + void onVolumeChangedFromKey(); } } diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt new file mode 100644 index 000000000000..58fb6a95b872 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt @@ -0,0 +1,171 @@ +/* + * 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. + * 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.haptics.slider + +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 +import kotlinx.coroutines.launch + +/** + * A plugin added to a manager of a [android.widget.SeekBar] that adds dynamic haptic feedback. + * + * 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 SeekableSliderHapticPlugin +@JvmOverloads +constructor( + vibratorHelper: VibratorHelper, + systemClock: SystemClock, + @Main private val mainDispatcher: CoroutineDispatcher, + @Application private val applicationScope: CoroutineScope, + sliderHapticFeedbackConfig: SliderHapticFeedbackConfig = SliderHapticFeedbackConfig(), + sliderTrackerConfig: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(), +) { + + private val velocityTracker = VelocityTracker.obtain() + + private val sliderEventProducer = SeekableSliderEventProducer() + + private val sliderHapticFeedbackProvider = + SliderHapticFeedbackProvider( + vibratorHelper, + velocityTracker, + sliderHapticFeedbackConfig, + systemClock, + ) + + private val sliderTracker = + SeekableSliderTracker( + sliderHapticFeedbackProvider, + sliderEventProducer, + mainDispatcher, + sliderTrackerConfig, + ) + + val isTracking: Boolean + get() = sliderTracker.isTracking + + val trackerState: SliderState + get() = sliderTracker.currentState + + /** + * A waiting [Job] for a timer that estimates the key-up event when a key-down event is + * received. + * + * This is useful for the cases where the slider is being operated by an external key, but the + * release of the key is not easily accessible (e.g., the volume keys) + */ + private var keyUpJob: Job? = null + + @VisibleForTesting + val isKeyUpTimerWaiting: Boolean + get() = keyUpJob != null && keyUpJob?.isActive == true + + /** + * Start the plugin. + * + * This starts the tracking of slider states, events and triggering of haptic feedback. + */ + fun start() { + if (!isTracking) { + sliderTracker.startTracking() + } + } + + /** + * Stop the plugin + * + * This stops the tracking of slider states, events and triggers of haptic feedback. + */ + fun stop() = sliderTracker.stopTracking() + + /** React to a touch event */ + fun onTouchEvent(event: MotionEvent?) { + when (event?.actionMasked) { + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> velocityTracker.clear() + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_MOVE -> velocityTracker.addMovement(event) + } + } + + /** onStartTrackingTouch event from the slider's [android.widget.SeekBar] */ + fun onStartTrackingTouch(seekBar: SeekBar) { + if (isTracking) { + sliderEventProducer.onStartTrackingTouch(seekBar) + } + } + + /** onProgressChanged event from the slider's [android.widget.SeekBar] */ + fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + if (isTracking) { + sliderEventProducer.onProgressChanged(seekBar, progress, fromUser) + } + } + + /** onStopTrackingTouch event from the slider's [android.widget.SeekBar] */ + fun onStopTrackingTouch(seekBar: SeekBar) { + if (isTracking) { + sliderEventProducer.onStopTrackingTouch(seekBar) + } + } + + /** onArrowUp event recorded */ + fun onArrowUp() { + if (isTracking) { + sliderEventProducer.onArrowUp() + } + } + + /** + * 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. + */ + fun onKeyDown() { + if (!isTracking) return + + if (isKeyUpTimerWaiting) { + // Cancel the ongoing wait + keyUpJob?.cancel() + } + keyUpJob = + applicationScope.launch { + delay(KEY_UP_TIMEOUT) + onArrowUp() + } + } + + companion object { + const val KEY_UP_TIMEOUT = 100L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index 9ee3d220a79b..aee441a13a5d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -535,6 +535,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } if (changed && fromKey) { Events.writeEvent(Events.EVENT_KEY, stream, lastAudibleStreamVolume); + mCallbacks.onVolumeChangedFromKey(); } return changed; } @@ -1030,6 +1031,18 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } @Override + public void onVolumeChangedFromKey() { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onVolumeChangedFromKey(); + } + }); + } + } + + @Override public void onAccessibilityModeChanged(Boolean showA11yStream) { boolean show = showA11yStream != null && showA11yStream; for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 404621d1fe81..b127c5160bba 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -34,6 +34,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL; import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder; +import static com.android.systemui.Flags.hapticVolumeSlider; import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED; import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED; @@ -117,7 +118,11 @@ import com.android.internal.view.RotationPolicy; import com.android.settingslib.Utils; import com.android.systemui.Dumpable; import com.android.systemui.Prefs; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin; +import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.VolumeDialog; @@ -125,6 +130,7 @@ import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.plugins.VolumeDialogController.State; import com.android.systemui.plugins.VolumeDialogController.StreamState; import com.android.systemui.res.R; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DevicePostureController; @@ -140,6 +146,9 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.CoroutineScope; + /** * Visual presentation of the volume dialog. * @@ -303,6 +312,10 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private int mOrientation; private final Lazy<SecureSettings> mSecureSettings; private int mDialogTimeoutMillis; + private final CoroutineDispatcher mMainDispatcher; + private final CoroutineScope mApplicationScope; + private final VibratorHelper mVibratorHelper; + private final com.android.systemui.util.time.SystemClock mSystemClock; public VolumeDialogImpl( Context context, @@ -319,11 +332,18 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, DevicePostureController devicePostureController, Looper looper, DumpManager dumpManager, - Lazy<SecureSettings> secureSettings) { + Lazy<SecureSettings> secureSettings, + VibratorHelper vibratorHelper, + @Main CoroutineDispatcher mainDispatcher, + @Application CoroutineScope applicationScope, + com.android.systemui.util.time.SystemClock systemClock) { mContext = new ContextThemeWrapper(context, R.style.volume_dialog_theme); mHandler = new H(looper); - + mMainDispatcher = mainDispatcher; + mApplicationScope = applicationScope; + mVibratorHelper = vibratorHelper; + mSystemClock = systemClock; mShouldListenForJank = shouldListenForJank; mController = volumeDialogController; mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); @@ -839,6 +859,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, row.header.setFilters(new InputFilter[] {new InputFilter.LengthFilter(13)}); } row.slider = row.view.findViewById(R.id.volume_row_slider); + row.createPlugin(mVibratorHelper, mSystemClock, mMainDispatcher, mApplicationScope); row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row)); row.number = row.view.findViewById(R.id.volume_number); @@ -1480,6 +1501,12 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, mController.getCaptionsComponentState(false); checkODICaptionsTooltip(false); updateBackgroundForDrawerClosedAmount(); + for (int i = 0; i < mRows.size(); i++) { + VolumeRow row = mRows.get(i); + if (row.slider.getVisibility() == VISIBLE) { + row.addHaptics(); + } + } Trace.endSection(); } @@ -1532,7 +1559,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, protected void dismissH(int reason) { Trace.beginSection("VolumeDialogImpl#dismissH"); - + for (int i = 0; i < mRows.size(); i++) { + mRows.get(i).removeHaptics(); + } Log.i(TAG, "mDialog.dismiss() reason: " + Events.DISMISS_REASONS[reason] + " from: " + Debug.getCaller()); @@ -2358,6 +2387,14 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, public void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkForSwitchState) { updateCaptionsEnabledH(isEnabled, checkForSwitchState); } + + @Override + public void onVolumeChangedFromKey() { + VolumeRow activeRow = getActiveRow(); + if (activeRow.mHapticPlugin != null) { + activeRow.mHapticPlugin.onKeyDown(); + } + } }; @VisibleForTesting void onPostureChanged(int posture) { @@ -2459,6 +2496,15 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (mRow.ss == null) return; + if (getActiveRow().equals(mRow) + && mRow.slider.getVisibility() == VISIBLE + && mRow.mHapticPlugin != null) { + mRow.mHapticPlugin.onProgressChanged(seekBar, progress, fromUser); + if (!fromUser) { + // Consider a change from program as the volume key being continuously pressed + mRow.mHapticPlugin.onKeyDown(); + } + } if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream) + " onProgressChanged " + progress + " fromUser=" + fromUser); if (!fromUser) return; @@ -2485,6 +2531,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, @Override public void onStartTrackingTouch(SeekBar seekBar) { if (D.BUG) Log.d(TAG, "onStartTrackingTouch"+ " " + mRow.stream); + if (mRow.mHapticPlugin != null) { + mRow.mHapticPlugin.onStartTrackingTouch(seekBar); + } mController.setActiveStream(mRow.stream); mRow.tracking = true; } @@ -2492,6 +2541,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, @Override public void onStopTrackingTouch(SeekBar seekBar) { if (D.BUG) Log.d(TAG, "onStopTrackingTouch"+ " " + mRow.stream); + if (mRow.mHapticPlugin != null) { + mRow.mHapticPlugin.onStopTrackingTouch(seekBar); + } mRow.tracking = false; mRow.userAttempt = SystemClock.uptimeMillis(); final int userLevel = getImpliedLevel(seekBar, seekBar.getProgress()); @@ -2524,6 +2576,22 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, } private static class VolumeRow { + private static final SliderHapticFeedbackConfig sSliderHapticFeedbackConfig = + new SliderHapticFeedbackConfig( + /* velocityInterpolatorFactor= */ 1f, + /* progressInterpolatorFactor= */ 1f, + /* progressBasedDragMinScale= */ 0f, + /* progressBasedDragMaxScale= */ 0.2f, + /* additionalVelocityMaxBump= */ 0.15f, + /* deltaMillisForDragInterval= */ 0f, + /* deltaProgressForDragThreshold= */ 0.015f, + /* numberOfLowTicks= */ 5, + /* maxVelocityToScale= */ 300f, + /* velocityAxis= */ MotionEvent.AXIS_Y, + /* upperBookendScale= */ 1f, + /* lowerBookendScale= */ 0.05f, + /* exponent= */ 1f / 0.89f); + private View view; private TextView header; private ImageButton icon; @@ -2544,6 +2612,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private ObjectAnimator anim; // slider progress animation for non-touch-related updates private int animTargetProgress; private int lastAudibleLevel = 1; + private SeekableSliderHapticPlugin mHapticPlugin; void setIcon(int iconRes, Resources.Theme theme) { if (icon != null) { @@ -2554,6 +2623,50 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, sliderProgressIcon.setDrawable(view.getResources().getDrawable(iconRes, theme)); } } + + void createPlugin( + VibratorHelper vibratorHelper, + com.android.systemui.util.time.SystemClock systemClock, + CoroutineDispatcher mainDispatcher, + CoroutineScope applicationScope) { + if (!hapticVolumeSlider() || mHapticPlugin != null) return; + + mHapticPlugin = new SeekableSliderHapticPlugin( + vibratorHelper, + systemClock, + mainDispatcher, + applicationScope, + sSliderHapticFeedbackConfig); + } + + + @SuppressLint("ClickableViewAccessibility") + void addTouchListener() { + slider.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + if (mHapticPlugin != null) { + mHapticPlugin.onTouchEvent(motionEvent); + } + return false; + } + }); + } + + void addHaptics() { + if (mHapticPlugin != null) { + addTouchListener(); + mHapticPlugin.start(); + } + } + + @SuppressLint("ClickableViewAccessibility") + void removeHaptics() { + slider.setOnTouchListener(null); + if (mHapticPlugin != null) { + mHapticPlugin.stop(); + } + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java index 497c4cb070f0..f180a942af70 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -22,16 +22,20 @@ import android.os.Looper; import com.android.internal.jank.InteractionJankMonitor; import com.android.systemui.CoreStartable; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.VolumeDialog; import com.android.systemui.plugins.VolumeDialogController; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.util.time.SystemClock; import com.android.systemui.volume.CsdWarningDialog; import com.android.systemui.volume.VolumeComponent; import com.android.systemui.volume.VolumeDialogComponent; @@ -49,6 +53,9 @@ import dagger.multibindings.ClassKey; import dagger.multibindings.IntoMap; import dagger.multibindings.IntoSet; +import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.CoroutineScope; + /** Dagger Module for code in the volume package. */ @Module( subcomponents = { @@ -90,7 +97,11 @@ public interface VolumeModule { CsdWarningDialog.Factory csdFactory, DevicePostureController devicePostureController, DumpManager dumpManager, - Lazy<SecureSettings> secureSettings) { + Lazy<SecureSettings> secureSettings, + VibratorHelper vibratorHelper, + @Main CoroutineDispatcher mainDispatcher, + @Application CoroutineScope applicationScope, + SystemClock systemClock) { VolumeDialogImpl impl = new VolumeDialogImpl( context, volumeDialogController, @@ -106,7 +117,11 @@ public interface VolumeModule { devicePostureController, Looper.getMainLooper(), dumpManager, - secureSettings); + secureSettings, + vibratorHelper, + mainDispatcher, + applicationScope, + systemClock); impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false); impl.setAutomute(true); impl.setSilentMode(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index 8c823b2376c3..d839da1d6e1b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -32,6 +32,7 @@ import static junit.framework.Assert.assertTrue; import static org.junit.Assume.assumeNotNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -63,6 +64,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.testing.UiEventLoggerFake; +import com.android.keyguard.TestScopeProvider; import com.android.systemui.Prefs; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.AnimatorTestRule; @@ -72,6 +74,7 @@ import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.plugins.VolumeDialogController.State; import com.android.systemui.res.R; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DevicePostureController; @@ -79,6 +82,7 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.FakeConfigurationController; import com.android.systemui.util.settings.FakeSettings; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.util.time.FakeSystemClock; import dagger.Lazy; @@ -97,6 +101,8 @@ import org.mockito.MockitoAnnotations; import java.util.Arrays; import java.util.function.Predicate; +import kotlinx.coroutines.Dispatchers; + @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -138,7 +144,6 @@ public class VolumeDialogImplTest extends SysuiTestCase { DevicePostureController mPostureController; @Mock private Lazy<SecureSettings> mLazySecureSettings; - private final CsdWarningDialog.Factory mCsdWarningDialogFactory = new CsdWarningDialog.Factory() { @Override @@ -146,6 +151,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { return mCsdWarningDialog; } }; + @Mock + private VibratorHelper mVibratorHelper; private int mLongestHideShowAnimationDuration = 250; private FakeSettings mSecureSettings; @@ -180,6 +187,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { when(mLazySecureSettings.get()).thenReturn(mSecureSettings); + when(mVibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(new int[]{0}); + mDialog = new VolumeDialogImpl( getContext(), mVolumeDialogController, @@ -195,7 +204,11 @@ public class VolumeDialogImplTest extends SysuiTestCase { mPostureController, mTestableLooper.getLooper(), mDumpManager, - mLazySecureSettings); + mLazySecureSettings, + mVibratorHelper, + Dispatchers.getUnconfined(), + TestScopeProvider.getTestScope(), + new FakeSystemClock()); mDialog.init(0, null); State state = createShellState(); mDialog.onStateChangedH(state); |