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