• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.reardisplay
18 
19 import android.annotation.SuppressLint
20 import android.content.Context
21 import android.os.Bundle
22 import android.view.MotionEvent
23 import android.view.View
24 import android.widget.Button
25 import android.widget.SeekBar
26 import android.widget.TextView
27 import com.android.systemui.haptics.slider.HapticSlider
28 import com.android.systemui.haptics.slider.HapticSliderPlugin
29 import com.android.systemui.haptics.slider.HapticSliderViewBinder
30 import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig
31 import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig
32 import com.android.systemui.res.R
33 import com.android.systemui.statusbar.VibratorHelper
34 import com.android.systemui.statusbar.phone.SystemUIDialog
35 import com.android.systemui.util.time.SystemClock
36 import com.google.android.msdl.domain.MSDLPlayer
37 import dagger.assisted.Assisted
38 import dagger.assisted.AssistedFactory
39 import dagger.assisted.AssistedInject
40 
41 /**
42  * A {@link com.android.systemui.statusbar.phone.SystemUIDialog.Delegate} providing a dialog which
43  * lets the user know that the Rear Display Mode is active, and that the content has moved to the
44  * outer display.
45  */
46 class RearDisplayInnerDialogDelegate
47 @AssistedInject
48 internal constructor(
49     private val systemUIDialogFactory: SystemUIDialog.Factory,
50     @Assisted private val rearDisplayContext: Context,
51     @Assisted private val touchExplorationEnabled: Boolean,
52     private val vibratorHelper: VibratorHelper,
53     private val msdlPlayer: MSDLPlayer,
54     private val systemClock: SystemClock,
55     @Assisted private val onCanceledRunnable: Runnable,
56 ) : SystemUIDialog.Delegate {
57 
58     private val sliderHapticFeedbackConfig =
59         SliderHapticFeedbackConfig(
60             /* velocityInterpolatorFactor = */ 1f,
61             /* progressInterpolatorFactor = */ 1f,
62             /* progressBasedDragMinScale = */ 0f,
63             /* progressBasedDragMaxScale = */ 0.2f,
64             /* additionalVelocityMaxBump = */ 0.25f,
65             /* deltaMillisForDragInterval = */ 0f,
66             /* deltaProgressForDragThreshold = */ 0.05f,
67             /* numberOfLowTicks = */ 5,
68             /* maxVelocityToScale = */ 200f,
69             /* velocityAxis = */ MotionEvent.AXIS_X,
70             /* upperBookendScale = */ 1f,
71             /* lowerBookendScale = */ 0.05f,
72             /* exponent = */ 1f / 0.89f,
73             /* sliderStepSize = */ 0f,
74         )
75 
76     private val sliderTrackerConfig =
77         SeekableSliderTrackerConfig(
78             /* waitTimeMillis = */ 100,
79             /* jumpThreshold = */ 0.02f,
80             /* lowerBookendThreshold = */ 0.01f,
81             /* upperBookendThreshold = */ 0.99f,
82         )
83 
84     @AssistedFactory
85     interface Factory {
86         fun create(
87             rearDisplayContext: Context,
88             onCanceledRunnable: Runnable,
89             touchExplorationEnabled: Boolean,
90         ): RearDisplayInnerDialogDelegate
91     }
92 
93     override fun createDialog(): SystemUIDialog {
94         return systemUIDialogFactory.create(
95             this,
96             rearDisplayContext,
97             false, /* shouldAcsdDismissDialog */
98         )
99     }
100 
101     @SuppressLint("ClickableViewAccessibility")
102     override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
103 
104         dialog.apply {
105             setContentView(R.layout.activity_rear_display_enabled)
106             setCanceledOnTouchOutside(false)
107 
108             requireViewById<Button>(R.id.cancel_button).let { it ->
109                 if (!touchExplorationEnabled) {
110                     return@let
111                 }
112 
113                 it.visibility = View.VISIBLE
114                 it.setOnClickListener { onCanceledRunnable.run() }
115             }
116 
117             requireViewById<TextView>(R.id.seekbar_instructions).let { it ->
118                 if (touchExplorationEnabled) {
119                     it.visibility = View.GONE
120                 }
121             }
122 
123             requireViewById<SeekBar>(R.id.seekbar).let { it ->
124                 if (touchExplorationEnabled) {
125                     it.visibility = View.GONE
126                     return@let
127                 }
128 
129                 // Create and bind the HapticSliderPlugin
130                 val hapticSliderPlugin =
131                     HapticSliderPlugin(
132                         vibratorHelper,
133                         msdlPlayer,
134                         systemClock,
135                         HapticSlider.SeekBar(it),
136                         sliderHapticFeedbackConfig,
137                         sliderTrackerConfig,
138                     )
139                 HapticSliderViewBinder.bind(it, hapticSliderPlugin)
140 
141                 // Send MotionEvents to the plugin, so that it can compute velocity, which is
142                 // used during the computation of haptic effect
143                 it.setOnTouchListener { _, motionEvent ->
144                     hapticSliderPlugin.onTouchEvent(motionEvent)
145                     false
146                 }
147 
148                 // Respond to SeekBar events, for both:
149                 // 1) Deciding if RDM should be terminated, etc, and
150                 // 2) Sending SeekBar events to the HapticSliderPlugin, so that the events
151                 //    are also used to compute the haptic effect
152                 it.setOnSeekBarChangeListener(
153                     SeekBarListener(hapticSliderPlugin, onCanceledRunnable)
154                 )
155             }
156         }
157     }
158 
159     class SeekBarListener(
160         private val hapticSliderPlugin: HapticSliderPlugin,
161         private val onCanceledRunnable: Runnable,
162     ) : SeekBar.OnSeekBarChangeListener {
163 
164         var lastProgress = 0
165         var secondLastProgress = 0
166 
167         override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
168             hapticSliderPlugin.onProgressChanged(progress, fromUser)
169 
170             // Simple heuristic checking that the user did in fact slide the
171             // SeekBar, instead of accidentally touching it at 100%
172             if (progress == 100 && lastProgress != 0) {
173                 onCanceledRunnable.run()
174             }
175 
176             secondLastProgress = lastProgress
177             lastProgress = progress
178         }
179 
180         override fun onStartTrackingTouch(seekBar: SeekBar?) {
181             hapticSliderPlugin.onStartTrackingTouch()
182         }
183 
184         override fun onStopTrackingTouch(seekBar: SeekBar?) {
185             hapticSliderPlugin.onStopTrackingTouch()
186 
187             // If secondLastProgress is 0, it means the user immediately touched
188             // the 100% location. We need two last values, because
189             // onStopTrackingTouch is always after onProgressChanged
190             if (lastProgress < 100 || secondLastProgress == 0) {
191                 lastProgress = 0
192                 secondLastProgress = 0
193                 seekBar?.progress = 0
194             }
195         }
196     }
197 }
198