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.haptics.qs
18 
19 import android.os.VibrationEffect
20 import android.util.Log
21 import kotlin.math.max
22 
23 object LongPressHapticBuilder {
24 
25     const val INVALID_DURATION = 0 /* in ms */
26 
27     private const val TAG = "LongPressHapticBuilder"
28     private const val SPIN_SCALE = 0.2f
29     private const val CLICK_SCALE = 0.5f
30     private const val LOW_TICK_SCALE = 0.08f
31     private const val WARMUP_TIME = 75 /* in ms */
32     private const val DAMPING_TIME = 24 /* in ms */
33 
34     /** Create the signal that indicates that a long-press action is available. */
35     fun createLongPressHint(
36         lowTickDuration: Int,
37         spinDuration: Int,
38         effectDuration: Int
39     ): VibrationEffect? {
40         if (lowTickDuration == 0 || spinDuration == 0) {
41             Log.d(
42                 TAG,
43                 "The LOW_TICK and/or SPIN primitives are not supported. No signal created.",
44             )
45             return null
46         }
47         if (effectDuration < WARMUP_TIME + spinDuration + DAMPING_TIME) {
48             Log.d(
49                 TAG,
50                 "Cannot fit long-press hint signal in the effect duration. No signal created",
51             )
52             return null
53         }
54 
55         val nLowTicks = WARMUP_TIME / lowTickDuration
56         val rampDownLowTicks = DAMPING_TIME / lowTickDuration
57         val composition = VibrationEffect.startComposition()
58 
59         // Warmup low ticks
60         repeat(nLowTicks) {
61             composition.addPrimitive(
62                 VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
63                 LOW_TICK_SCALE,
64                 0,
65             )
66         }
67 
68         // Spin effect
69         composition.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, SPIN_SCALE, 0)
70 
71         // Damping low ticks
72         repeat(rampDownLowTicks) { i ->
73             composition.addPrimitive(
74                 VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
75                 LOW_TICK_SCALE / (i + 1),
76                 0,
77             )
78         }
79 
80         return composition.compose()
81     }
82 
83     /** Create a "snapping" effect that triggers at the end of a long-press gesture */
84     fun createSnapEffect(): VibrationEffect? =
85         VibrationEffect.startComposition()
86             .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, CLICK_SCALE, 0)
87             .compose()
88 
89     /** Creates a signal that indicates the reversal of the long-press animation. */
90     fun createReversedEffect(
91         pausedProgress: Float,
92         lowTickDuration: Int,
93         effectDuration: Int,
94     ): VibrationEffect? {
95         val duration = pausedProgress * effectDuration
96         if (duration == 0f) return null
97 
98         if (lowTickDuration == 0) {
99             Log.d(TAG, "Cannot play reverse haptics because LOW_TICK is not supported")
100             return null
101         }
102 
103         val nLowTicks = (duration / lowTickDuration).toInt()
104         if (nLowTicks == 0) return null
105 
106         val composition = VibrationEffect.startComposition()
107         var scale: Float
108         val step = LOW_TICK_SCALE / nLowTicks
109         repeat(nLowTicks) { i ->
110             scale = max(LOW_TICK_SCALE - step * i, 0f)
111             composition.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, scale, 0)
112         }
113         return composition.compose()
114     }
115 }
116