1 /* 2 * 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.haptics.qs 18 19 import android.os.VibrationEffect 20 import androidx.annotation.VisibleForTesting 21 import com.android.systemui.animation.Expandable 22 import com.android.systemui.plugins.qs.QSTile 23 import com.android.systemui.statusbar.VibratorHelper 24 import com.android.systemui.statusbar.policy.KeyguardStateController 25 import javax.inject.Inject 26 27 /** 28 * A class that handles the long press visuo-haptic effect for a QS tile. 29 * 30 * The class can contain references to a [QSTile] and an [Expandable] to perform clicks and 31 * long-clicks on the tile. The class also provides a [State] tha can be used to determine the 32 * current state of the long press effect. 33 * 34 * @property[vibratorHelper] The [VibratorHelper] to deliver haptic effects. 35 * @property[effectDuration] The duration of the effect in ms. 36 */ 37 // TODO(b/332902869): In addition from being injectable, we can consider making it a singleton 38 class QSLongPressEffect 39 @Inject 40 constructor( 41 private val vibratorHelper: VibratorHelper?, 42 private val keyguardStateController: KeyguardStateController, 43 ) { 44 45 var effectDuration = 0 46 private set 47 48 /** Current state */ 49 var state = State.IDLE 50 private set 51 52 /** Callback object for effect actions */ 53 var callback: Callback? = null 54 55 /** The [QSTile] and [Expandable] used to perform a long-click and click actions */ 56 var qsTile: QSTile? = null 57 var expandable: Expandable? = null 58 59 /** Haptic effects */ 60 private val durations = 61 vibratorHelper?.getPrimitiveDurations( 62 VibrationEffect.Composition.PRIMITIVE_LOW_TICK, 63 VibrationEffect.Composition.PRIMITIVE_SPIN 64 ) 65 66 private var longPressHint: VibrationEffect? = null 67 68 private val snapEffect = LongPressHapticBuilder.createSnapEffect() 69 70 val hasInitialized: Boolean 71 get() = longPressHint != null 72 73 @VisibleForTesting setStatenull74 fun setState(newState: State) { 75 state = newState 76 } 77 playReverseHapticsnull78 fun playReverseHaptics(pausedProgress: Float) { 79 val effect = 80 LongPressHapticBuilder.createReversedEffect( 81 pausedProgress, 82 durations?.get(0) ?: 0, 83 effectDuration, 84 ) 85 vibratorHelper?.cancel() 86 vibrate(effect) 87 } 88 vibratenull89 private fun vibrate(effect: VibrationEffect?) { 90 if (vibratorHelper != null && effect != null) { 91 vibratorHelper.vibrate(effect) 92 } 93 } 94 handleActionDownnull95 fun handleActionDown() { 96 when (state) { 97 State.IDLE -> { 98 setState(State.TIMEOUT_WAIT) 99 } 100 State.RUNNING_BACKWARDS -> callback?.onCancelAnimator() 101 else -> {} 102 } 103 } 104 handleActionUpnull105 fun handleActionUp() { 106 if (state == State.RUNNING_FORWARD) { 107 setState(State.RUNNING_BACKWARDS) 108 callback?.onReverseAnimator() 109 } 110 } 111 handleActionCancelnull112 fun handleActionCancel() { 113 when (state) { 114 State.TIMEOUT_WAIT -> setState(State.IDLE) 115 State.RUNNING_FORWARD -> { 116 setState(State.RUNNING_BACKWARDS) 117 callback?.onReverseAnimator() 118 } 119 else -> {} 120 } 121 } 122 handleAnimationStartnull123 fun handleAnimationStart() { 124 vibrate(longPressHint) 125 setState(State.RUNNING_FORWARD) 126 } 127 128 /** This function is called both when an animator completes or gets cancelled */ handleAnimationCompletenull129 fun handleAnimationComplete() { 130 if (state == State.RUNNING_FORWARD) { 131 setState(State.IDLE) 132 vibrate(snapEffect) 133 if (keyguardStateController.isUnlocked) { 134 callback?.onPrepareForLaunch() 135 qsTile?.longClick(expandable) 136 } else { 137 callback?.onResetProperties() 138 qsTile?.longClick(expandable) 139 } 140 } 141 if (state != State.TIMEOUT_WAIT) { 142 // This will happen if the animator did not finish by being cancelled 143 setState(State.IDLE) 144 } 145 } 146 handleAnimationCancelnull147 fun handleAnimationCancel() { 148 setState(State.TIMEOUT_WAIT) 149 } 150 handleTimeoutCompletenull151 fun handleTimeoutComplete() { 152 if (state == State.TIMEOUT_WAIT) { 153 callback?.onStartAnimator() 154 } 155 } 156 onTileClicknull157 fun onTileClick(): Boolean { 158 if (state == State.TIMEOUT_WAIT) { 159 setState(State.IDLE) 160 qsTile?.let { 161 it.click(expandable) 162 return true 163 } 164 } 165 return false 166 } 167 168 /** 169 * Reset the effect with a new effect duration. 170 * 171 * @param[duration] New duration for the long-press effect 172 * @return true if the effect initialized correctly 173 */ initializeEffectnull174 fun initializeEffect(duration: Int): Boolean { 175 // The effect can't initialize with a negative duration 176 if (duration <= 0) return false 177 178 // There is no need to re-initialize if the duration has not changed 179 if (duration == effectDuration) return true 180 181 effectDuration = duration 182 longPressHint = 183 LongPressHapticBuilder.createLongPressHint( 184 durations?.get(0) ?: LongPressHapticBuilder.INVALID_DURATION, 185 durations?.get(1) ?: LongPressHapticBuilder.INVALID_DURATION, 186 effectDuration 187 ) 188 setState(State.IDLE) 189 return true 190 } 191 192 enum class State { 193 IDLE, /* The effect is idle waiting for touch input */ 194 TIMEOUT_WAIT, /* The effect is waiting for a [PRESSED_TIMEOUT] period */ 195 RUNNING_FORWARD, /* The effect is running normally */ 196 RUNNING_BACKWARDS, /* The effect was interrupted and is now running backwards */ 197 } 198 199 /** Callbacks to notify view and animator actions */ 200 interface Callback { 201 202 /** Prepare for an activity launch */ onPrepareForLaunchnull203 fun onPrepareForLaunch() 204 205 /** Reset the tile visual properties */ 206 fun onResetProperties() 207 208 /** Start the effect animator */ 209 fun onStartAnimator() 210 211 /** Reverse the effect animator */ 212 fun onReverseAnimator() 213 214 /** Cancel the effect animator */ 215 fun onCancelAnimator() 216 } 217 } 218