diff options
| -rw-r--r-- | packages/SystemUI/res/drawable/ic_rear_display_slider.xml | 29 | ||||
| -rw-r--r-- | packages/SystemUI/res/drawable/rear_display_dialog_seekbar.xml | 32 | ||||
| -rw-r--r-- | packages/SystemUI/res/drawable/rear_display_dialog_seekbar_progress.xml | 40 | ||||
| -rw-r--r-- | packages/SystemUI/res/layout/activity_rear_display_enabled.xml (renamed from packages/SystemUI/res/layout/activity_rear_display_front_screen_on.xml) | 36 | ||||
| -rw-r--r-- | packages/SystemUI/res/values/dimens.xml | 10 | ||||
| -rw-r--r-- | packages/SystemUI/res/values/strings.xml | 2 | ||||
| -rw-r--r-- | packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt | 111 | ||||
| -rw-r--r-- | packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt | 68 |
8 files changed, 300 insertions, 28 deletions
diff --git a/packages/SystemUI/res/drawable/ic_rear_display_slider.xml b/packages/SystemUI/res/drawable/ic_rear_display_slider.xml new file mode 100644 index 000000000000..8e5da81dd418 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_rear_display_slider.xml @@ -0,0 +1,29 @@ +<!-- + Copyright (C) 2025 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?android:attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <group + android:translateX="-0" + android:translateY="960"> + <path + android:fillColor="?android:attr/colorPrimary" + android:pathData="M200 -120q-33 0 -56.5 -23.5T120 -200v-160h80v160h560v-560H200v160h-80v-160q0 -33 23.5 -56.5T200 -840h560q33 0 56.5 23.5T840 -760v560q0 33 -23.5 56.5T760 -120H200Zm220 -160l-56 -58 102 -102H120v-80h346L364 -622l56 -58 200 200 -200 200Z" /> + </group> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/rear_display_dialog_seekbar.xml b/packages/SystemUI/res/drawable/rear_display_dialog_seekbar.xml new file mode 100644 index 000000000000..73704f823033 --- /dev/null +++ b/packages/SystemUI/res/drawable/rear_display_dialog_seekbar.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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 + ~ 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. + --> + +<!-- SeekBar drawable for the rear display cancellation slider. --> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:paddingMode="stack"> + <item android:gravity="center" + android:height="2dp" + android:width="@dimen/rear_display_progress_width"> + <shape android:shape="rectangle"> + <solid android:color="@androidprv:color/materialColorSurfaceContainer" /> + </shape> + </item> + <item + android:id="@android:id/progress" + android:gravity="center_vertical|fill_horizontal"> + <com.android.systemui.util.RoundedCornerProgressDrawable android:drawable="@drawable/rear_display_dialog_seekbar_progress" /> + </item> +</layer-list>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/rear_display_dialog_seekbar_progress.xml b/packages/SystemUI/res/drawable/rear_display_dialog_seekbar_progress.xml new file mode 100644 index 000000000000..e00bda0e91b4 --- /dev/null +++ b/packages/SystemUI/res/drawable/rear_display_dialog_seekbar_progress.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 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 + ~ 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. + --> + +<!-- SeekBar drawable for volume rows. This contains a background layer (with a solid round rect, + and a bottom-aligned icon) and a progress layer (with an accent-colored round rect and icon) + that moves up and down with the progress value. --> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:autoMirrored="true"> + <item android:id="@+id/rear_display_dialog_progress_solid"> + <shape> + <size android:height="@dimen/rear_display_dialog_slider_height" /> + <solid android:color="?android:attr/colorAccent" /> + <corners android:radius="@dimen/rear_display_dialog_slider_corner_radius"/> + </shape> + </item> + <item + android:id="@+id/rear_display_dialog_seekbar_progress_icon" + android:gravity="center_vertical|right" + android:height="@dimen/rounded_slider_icon_size" + android:width="@dimen/rounded_slider_icon_size" + android:right="@dimen/rear_display_dialog_slider_icon_inset"> + <com.android.systemui.util.AlphaTintDrawableWrapper + android:drawable="@drawable/ic_rear_display_slider" + android:tint="?androidprv:attr/colorAccentPrimaryVariant" /> + </item> +</layer-list>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/activity_rear_display_front_screen_on.xml b/packages/SystemUI/res/layout/activity_rear_display_enabled.xml index a8d4d2ece07f..f900626b4da6 100644 --- a/packages/SystemUI/res/layout/activity_rear_display_front_screen_on.xml +++ b/packages/SystemUI/res/layout/activity_rear_display_enabled.xml @@ -52,27 +52,25 @@ android:text="@string/rear_display_unfolded_front_screen_on" android:textAppearance="@style/TextAppearance.Dialog.Title" android:lineSpacingExtra="2sp" - android:translationY="-1.24sp" + android:paddingBottom="@dimen/rear_display_title_bottom_padding" android:gravity="center_horizontal" /> - <!-- Buttons --> - <LinearLayout - android:layout_width="match_parent" + <TextView + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="horizontal" - android:layout_marginTop="36dp"> - <Space - android:layout_width="0dp" - android:layout_height="match_parent" - android:layout_weight="1"/> - <TextView - android:id="@+id/button_cancel" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_weight="0" - android:layout_gravity="start" - android:text="@string/cancel" - style="@style/Widget.Dialog.Button.BorderButton" /> - </LinearLayout> + android:text="@string/rear_display_unfolded_front_screen_on_slide_to_cancel" + android:textAppearance="@style/TextAppearance.Dialog.Body" + android:lineSpacingExtra="2sp" + android:paddingBottom="@dimen/rear_display_title_bottom_padding" + android:gravity="center_horizontal" /> + + <SeekBar + android:id="@+id/seekbar" + android:layout_width="@dimen/rear_display_animation_width_opened" + android:layout_height="wrap_content" + android:progressDrawable="@drawable/rear_display_dialog_seekbar" + android:thumb="@null" + android:background="@null" + android:gravity="center_horizontal" /> </LinearLayout> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index d93716b03685..c9c4f8cc56e0 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -2159,4 +2159,14 @@ <!-- Gradient color wallpaper start --> <dimen name="gradient_color_wallpaper_center_offset">128dp</dimen> <!-- Gradient color wallpaper end --> + + <!-- Rear display mode --> + <dimen name="rear_display_dialog_slider_height">42dp</dimen> + <dimen name="rear_display_dialog_slider_corner_radius">21dp</dimen> + <!-- (rear_display_dialog_slider_height - rounded_slider_icon_size) / 2 --> + <dimen name="rear_display_dialog_slider_icon_inset">11dp</dimen> + <!-- rear_display_animation_width_opened - 2 * rear_display_dialog_slider_corner_radius --> + <dimen name="rear_display_progress_width">231dp</dimen> + <!-- Rear display mode end --> + </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 35fea8f20b76..d18a90a17abe 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3656,6 +3656,8 @@ <string name="rear_display_accessibility_unfolded_animation">Foldable device being flipped around</string> <!-- Text for a dialog telling the user that the front screen is turned on. [CHAR_LIMIT=NONE] --> <string name="rear_display_unfolded_front_screen_on">Front screen turned on</string> + <!-- Text for a dialog telling the user that sliding the progress bar cancels rear display mode, bringing the system contents back to the inner display. [CHAR_LIMIT=NONE] --> + <string name="rear_display_unfolded_front_screen_on_slide_to_cancel">Slide to use inner screen</string> <!-- QuickSettings: Additional label for the auto-rotation quicksettings tile indicating that the setting corresponds to the folded posture for a foldable device [CHAR LIMIT=32] --> <string name="quick_settings_rotation_posture_folded">folded</string> diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt index 1355ba8bdfd4..f5facf42ee67 100644 --- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt @@ -16,11 +16,21 @@ package com.android.systemui.reardisplay +import android.annotation.SuppressLint import android.content.Context import android.os.Bundle -import android.view.View +import android.view.MotionEvent +import android.widget.SeekBar +import com.android.systemui.haptics.slider.HapticSlider +import com.android.systemui.haptics.slider.HapticSliderPlugin +import com.android.systemui.haptics.slider.HapticSliderViewBinder +import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig +import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig import com.android.systemui.res.R +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.util.time.SystemClock +import com.google.android.msdl.domain.MSDLPlayer import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -35,9 +45,38 @@ class RearDisplayInnerDialogDelegate internal constructor( private val systemUIDialogFactory: SystemUIDialog.Factory, @Assisted private val rearDisplayContext: Context, + private val vibratorHelper: VibratorHelper, + private val msdlPlayer: MSDLPlayer, + private val systemClock: SystemClock, @Assisted private val onCanceledRunnable: Runnable, ) : SystemUIDialog.Delegate { + private val sliderHapticFeedbackConfig = + SliderHapticFeedbackConfig( + /* velocityInterpolatorFactor = */ 1f, + /* progressInterpolatorFactor = */ 1f, + /* progressBasedDragMinScale = */ 0f, + /* progressBasedDragMaxScale = */ 0.2f, + /* additionalVelocityMaxBump = */ 0.25f, + /* deltaMillisForDragInterval = */ 0f, + /* deltaProgressForDragThreshold = */ 0.05f, + /* numberOfLowTicks = */ 5, + /* maxVelocityToScale = */ 200f, + /* velocityAxis = */ MotionEvent.AXIS_X, + /* upperBookendScale = */ 1f, + /* lowerBookendScale = */ 0.05f, + /* exponent = */ 1f / 0.89f, + /* sliderStepSize = */ 0f, + ) + + private val sliderTrackerConfig = + SeekableSliderTrackerConfig( + /* waitTimeMillis = */ 100, + /* jumpThreshold = */ 0.02f, + /* lowerBookendThreshold = */ 0.01f, + /* upperBookendThreshold = */ 0.99f, + ) + @AssistedFactory interface Factory { fun create( @@ -54,13 +93,79 @@ internal constructor( ) } + @SuppressLint("ClickableViewAccessibility") override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { dialog.apply { - setContentView(R.layout.activity_rear_display_front_screen_on) + setContentView(R.layout.activity_rear_display_enabled) setCanceledOnTouchOutside(false) - requireViewById<View>(R.id.button_cancel).setOnClickListener { + + requireViewById<SeekBar>(R.id.seekbar).let { it -> + // Create and bind the HapticSliderPlugin + val hapticSliderPlugin = + HapticSliderPlugin( + vibratorHelper, + msdlPlayer, + systemClock, + HapticSlider.SeekBar(it), + sliderHapticFeedbackConfig, + sliderTrackerConfig, + ) + HapticSliderViewBinder.bind(it, hapticSliderPlugin) + + // Send MotionEvents to the plugin, so that it can compute velocity, which is + // used during the computation of haptic effect + it.setOnTouchListener { _, motionEvent -> + hapticSliderPlugin.onTouchEvent(motionEvent) + false + } + + // Respond to SeekBar events, for both: + // 1) Deciding if RDM should be terminated, etc, and + // 2) Sending SeekBar events to the HapticSliderPlugin, so that the events + // are also used to compute the haptic effect + it.setOnSeekBarChangeListener( + SeekBarListener(hapticSliderPlugin, onCanceledRunnable) + ) + } + } + } + + class SeekBarListener( + private val hapticSliderPlugin: HapticSliderPlugin, + private val onCanceledRunnable: Runnable, + ) : SeekBar.OnSeekBarChangeListener { + + var lastProgress = 0 + var secondLastProgress = 0 + + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + hapticSliderPlugin.onProgressChanged(progress, fromUser) + + // Simple heuristic checking that the user did in fact slide the + // SeekBar, instead of accidentally touching it at 100% + if (progress == 100 && lastProgress != 0) { onCanceledRunnable.run() } + + secondLastProgress = lastProgress + lastProgress = progress + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) { + hapticSliderPlugin.onStartTrackingTouch() + } + + override fun onStopTrackingTouch(seekBar: SeekBar?) { + hapticSliderPlugin.onStopTrackingTouch() + + // If secondLastProgress is 0, it means the user immediately touched + // the 100% location. We need two last values, because + // onStopTrackingTouch is always after onProgressChanged + if (lastProgress < 100 || secondLastProgress == 0) { + lastProgress = 0 + secondLastProgress = 0 + seekBar?.progress = 0 + } } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt index 60588802ffa9..fc7661666825 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt @@ -17,21 +17,28 @@ package com.android.systemui.reardisplay import android.testing.TestableLooper -import android.view.View +import android.widget.SeekBar import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.haptics.msdl.msdlPlayer +import com.android.systemui.haptics.slider.HapticSliderPlugin +import com.android.systemui.haptics.vibratorHelper +import com.android.systemui.reardisplay.RearDisplayInnerDialogDelegate.SeekBarListener import com.android.systemui.res.R import com.android.systemui.statusbar.phone.systemUIDialogDotFactory import com.android.systemui.testKosmos +import com.android.systemui.util.time.systemClock import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import org.junit.Test import org.mockito.Mockito.verify +import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never /** atest SystemUITests:com.android.systemui.reardisplay.RearDisplayInnerDialogDelegateTest */ @SmallTest -@TestableLooper.RunWithLooper +@TestableLooper.RunWithLooper(setAsMainLooper = true) class RearDisplayInnerDialogDelegateTest : SysuiTestCase() { private val kosmos = testKosmos() @@ -39,7 +46,13 @@ class RearDisplayInnerDialogDelegateTest : SysuiTestCase() { @Test fun testShowAndDismissDialog() { val dialogDelegate = - RearDisplayInnerDialogDelegate(kosmos.systemUIDialogDotFactory, mContext) {} + RearDisplayInnerDialogDelegate( + kosmos.systemUIDialogDotFactory, + mContext, + kosmos.vibratorHelper, + kosmos.msdlPlayer, + kosmos.systemClock, + ) {} val dialog = dialogDelegate.createDialog() dialog.show() @@ -50,16 +63,59 @@ class RearDisplayInnerDialogDelegateTest : SysuiTestCase() { } @Test - fun testCancel() { + fun testProgressSlidesToCompletion_callbackInvoked() { val mockCallback = mock<Runnable>() - RearDisplayInnerDialogDelegate(kosmos.systemUIDialogDotFactory, mContext) { + RearDisplayInnerDialogDelegate( + kosmos.systemUIDialogDotFactory, + mContext, + kosmos.vibratorHelper, + kosmos.msdlPlayer, + kosmos.systemClock, + ) { mockCallback.run() } .createDialog() .apply { show() - findViewById<View>(R.id.button_cancel).performClick() + val seekbar = findViewById<SeekBar>(R.id.seekbar) + seekbar.progress = 50 + seekbar.progress = 100 verify(mockCallback).run() } } + + @Test + fun testProgressImmediatelyCompletes_callbackNotInvoked() { + val mockCallback = mock<Runnable>() + RearDisplayInnerDialogDelegate( + kosmos.systemUIDialogDotFactory, + mContext, + kosmos.vibratorHelper, + kosmos.msdlPlayer, + kosmos.systemClock, + ) { + mockCallback.run() + } + .createDialog() + .apply { + show() + val seekbar = findViewById<SeekBar>(R.id.seekbar) + seekbar.progress = 100 + verify(mockCallback, never()).run() + } + } + + @Test + fun testProgressResetsWhenStoppingBeforeCompletion() { + val mockCallback = mock<Runnable>() + val mockSeekbar = mock<SeekBar>() + val seekBarListener = SeekBarListener(mock<HapticSliderPlugin>(), mockCallback) + + seekBarListener.onStartTrackingTouch(mockSeekbar) + seekBarListener.onProgressChanged(mockSeekbar, 50 /* progress */, true /* fromUser */) + seekBarListener.onStopTrackingTouch(mockSeekbar) + + // Progress is reset + verify(mockSeekbar).setProgress(eq(0)) + } } |