summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/drawable/ic_rear_display_slider.xml29
-rw-r--r--packages/SystemUI/res/drawable/rear_display_dialog_seekbar.xml32
-rw-r--r--packages/SystemUI/res/drawable/rear_display_dialog_seekbar_progress.xml40
-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.xml10
-rw-r--r--packages/SystemUI/res/values/strings.xml2
-rw-r--r--packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt111
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt68
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))
+ }
}