1 /*
2  * Copyright 2023 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 androidx.core.haptics.signal
18 
19 import android.os.Build
20 import androidx.annotation.FloatRange
21 import androidx.annotation.RequiresApi
22 import androidx.core.haptics.VibrationWrapper
23 import androidx.core.haptics.device.HapticDeviceProfile
24 import androidx.core.haptics.impl.HapticSignalConverter
25 import androidx.core.haptics.signal.WaveformSignal.ConstantVibrationAtom.Companion.DEFAULT_AMPLITUDE
26 import java.util.Objects
27 import kotlin.time.Duration
28 import kotlin.time.Duration.Companion.milliseconds
29 import kotlin.time.toKotlinDuration
30 
31 /**
32  * A haptic signal where the vibration parameters change over time.
33  *
34  * Waveform signals may be used to describe step waveforms, defined by a sequence of constant
35  * vibrations played at different strengths. They can also be combined to define a
36  * [RepeatingWaveformSignal], which is an [InfiniteSignal] that repeats a waveform until the
37  * vibration is canceled.
38  *
39  * @sample androidx.core.haptics.samples.AmplitudeWaveform
40  * @sample androidx.core.haptics.samples.PatternThenRepeatAmplitudeWaveform
41  */
42 public class WaveformSignal(
43 
44     /** The waveform signal atoms that describes the vibration parameters over time. */
45     public val atoms: List<Atom>,
46 ) : FiniteSignal() {
47     init {
<lambda>null48         require(atoms.isNotEmpty()) { "Haptic signals cannot be empty" }
49     }
50 
51     public companion object {
52 
53         /**
54          * Returns a [WaveformSignal] created with given waveform atoms.
55          *
56          * Use [on] and [off] to create atoms.
57          *
58          * @sample androidx.core.haptics.samples.AmplitudeWaveform
59          * @param atoms The [WaveformSignal.Atom] instances that define the [WaveformSignal].
60          */
61         @JvmStatic
waveformOfnull62         public fun waveformOf(vararg atoms: Atom): WaveformSignal = WaveformSignal(atoms.toList())
63 
64         /**
65          * Returns a [RepeatingWaveformSignal] created with given waveform atoms.
66          *
67          * Repeating waveforms should include any desired loop delay as an [off] atom at the end of
68          * the atom list.
69          *
70          * @sample androidx.core.haptics.samples.RepeatingAmplitudeWaveform
71          * @param atoms The [WaveformSignal.Atom] instances that define the
72          *   [RepeatingWaveformSignal].
73          */
74         @JvmStatic
75         public fun repeatingWaveformOf(vararg atoms: Atom): RepeatingWaveformSignal =
76             waveformOf(*atoms).repeat()
77 
78         /**
79          * Returns a [WaveformSignal.Atom] that turns off the vibrator for the specified duration.
80          *
81          * @sample androidx.core.haptics.samples.PatternWaveform
82          * @param duration The duration the vibrator should be turned off.
83          */
84         @RequiresApi(Build.VERSION_CODES.O)
85         @JvmStatic
86         public fun off(duration: java.time.Duration): ConstantVibrationAtom =
87             ConstantVibrationAtom(duration.toKotlinDuration(), amplitude = 0f)
88 
89         /**
90          * Returns a [WaveformSignal.Atom] that turns off the vibrator for the specified duration.
91          *
92          * @sample androidx.core.haptics.samples.PatternWaveform
93          * @param durationMillis The duration the vibrator should be turned off, in milliseconds.
94          */
95         @JvmStatic
96         public fun off(durationMillis: Long): ConstantVibrationAtom =
97             ConstantVibrationAtom(durationMillis.milliseconds, amplitude = 0f)
98 
99         /**
100          * Returns a [WaveformSignal.Atom] that turns on the vibrator for the specified duration at
101          * a device-specific default amplitude.
102          *
103          * @sample androidx.core.haptics.samples.PatternWaveform
104          * @param duration The duration for the vibration.
105          */
106         @RequiresApi(Build.VERSION_CODES.O)
107         @JvmStatic
108         public fun on(duration: java.time.Duration): ConstantVibrationAtom =
109             ConstantVibrationAtom(duration.toKotlinDuration(), DEFAULT_AMPLITUDE)
110 
111         /**
112          * Returns a [WaveformSignal.Atom] that turns on the vibrator for the specified duration at
113          * a device-specific default amplitude.
114          *
115          * @sample androidx.core.haptics.samples.PatternWaveform
116          * @param durationMillis The duration for the vibration, in milliseconds.
117          */
118         @JvmStatic
119         public fun on(durationMillis: Long): ConstantVibrationAtom =
120             ConstantVibrationAtom(durationMillis.milliseconds, DEFAULT_AMPLITUDE)
121 
122         /**
123          * Returns a [WaveformSignal.Atom] that turns on the vibrator for the specified duration at
124          * the specified amplitude.
125          *
126          * @sample androidx.core.haptics.samples.AmplitudeWaveform
127          * @param duration The duration for the vibration.
128          * @param amplitude The vibration strength, with 1 representing maximum amplitude, and 0
129          *   representing off - equivalent to calling [off].
130          */
131         @RequiresApi(Build.VERSION_CODES.O)
132         @JvmStatic
133         public fun on(
134             duration: java.time.Duration,
135             @FloatRange(from = 0.0, to = 1.0) amplitude: Float
136         ): ConstantVibrationAtom = ConstantVibrationAtom(duration.toKotlinDuration(), amplitude)
137 
138         /**
139          * Returns a [WaveformSignal.Atom] that turns on the vibrator for the specified duration at
140          * the specified amplitude.
141          *
142          * @sample androidx.core.haptics.samples.AmplitudeWaveform
143          * @param durationMillis The duration for the vibration, in milliseconds.
144          * @param amplitude The vibration strength, with 1 representing maximum amplitude, and 0
145          *   representing off - equivalent to calling [off].
146          */
147         @JvmStatic
148         public fun on(
149             durationMillis: Long,
150             @FloatRange(from = 0.0, to = 1.0) amplitude: Float
151         ): ConstantVibrationAtom = ConstantVibrationAtom(durationMillis.milliseconds, amplitude)
152     }
153 
154     /**
155      * Returns a [RepeatingWaveformSignal] to play this waveform on repeat until it's canceled.
156      *
157      * @sample androidx.core.haptics.samples.PatternWaveformRepeat
158      */
159     public fun repeat(): RepeatingWaveformSignal =
160         RepeatingWaveformSignal(initialWaveform = null, repeatingWaveform = this)
161 
162     /**
163      * Returns a [RepeatingWaveformSignal] that starts with this waveform signal then plays the
164      * given waveform signal on repeat until the vibration is canceled.
165      *
166      * @sample androidx.core.haptics.samples.PatternThenRepeatExistingWaveform
167      * @param waveformToRepeat The waveform to be played on repeat after this waveform.
168      */
169     public fun thenRepeat(waveformToRepeat: WaveformSignal): RepeatingWaveformSignal =
170         RepeatingWaveformSignal(initialWaveform = this, repeatingWaveform = waveformToRepeat)
171 
172     /**
173      * Returns a [RepeatingWaveformSignal] that starts with this waveform signal then plays the
174      * given waveform atoms on repeat until the vibration is canceled.
175      *
176      * @sample androidx.core.haptics.samples.PatternThenRepeatAmplitudeWaveform
177      * @param atoms The [WaveformSignal.Atom] instances that define the repeating [WaveformSignal]
178      *   to be played after this waveform.
179      */
180     public fun thenRepeat(vararg atoms: Atom): RepeatingWaveformSignal =
181         thenRepeat(waveformOf(*atoms))
182 
183     override fun equals(other: Any?): Boolean {
184         if (this === other) return true
185         if (other !is WaveformSignal) return false
186         if (atoms != other.atoms) return false
187         return true
188     }
189 
hashCodenull190     override fun hashCode(): Int {
191         return atoms.hashCode()
192     }
193 
toStringnull194     override fun toString(): String {
195         return "WaveformSignal(${atoms.joinToString()})"
196     }
197 
toVibrationnull198     override fun toVibration(): VibrationWrapper? =
199         HapticSignalConverter.toVibration(initialWaveform = this, repeatingWaveform = null)
200 
201     override fun isSupportedBy(deviceProfile: HapticDeviceProfile): Boolean =
202         atoms.all { it.isSupportedBy(deviceProfile) }
203 
204     /**
205      * A [WaveformSignal.Atom] is a building block for creating a [WaveformSignal].
206      *
207      * Waveform signal atoms describe how vibration parameters change over time. They can describe a
208      * constant vibration sustained for a fixed duration, for example, which can be used to create a
209      * step waveform. They can also be used to describe simpler on-off vibration patterns.
210      *
211      * @sample androidx.core.haptics.samples.PatternWaveform
212      * @sample androidx.core.haptics.samples.AmplitudeWaveform
213      */
214     public abstract class Atom internal constructor() {
215 
216         /** Returns true if the device vibrator can play this atom as intended, false otherwise. */
isSupportedBynull217         internal abstract fun isSupportedBy(deviceProfile: HapticDeviceProfile): Boolean
218     }
219 
220     /**
221      * A [ConstantVibrationAtom] plays a constant vibration for the specified period of time.
222      *
223      * Constant vibrations can be played in sequence to create custom waveform signals.
224      *
225      * The amplitude determines the strength of the vibration, defined as a value in the range
226      * [0f..1f]. Zero amplitude implies the vibrator motor should be off. The amplitude can also be
227      * defined by [DEFAULT_AMPLITUDE], which will vibrate constantly at a hardware-specific default
228      * vibration strength.
229      *
230      * @sample androidx.core.haptics.samples.PatternWaveform
231      * @sample androidx.core.haptics.samples.AmplitudeWaveform
232      */
233     public class ConstantVibrationAtom
234     internal constructor(
235         duration: Duration,
236 
237         /**
238          * The vibration strength.
239          *
240          * Zero amplitude turns the vibrator off for the specified duration, and [DEFAULT_AMPLITUDE]
241          * uses a hardware-specific default vibration strength.
242          */
243         public val amplitude: Float,
244     ) : Atom() {
245         /** The duration to sustain the constant vibration, in milliseconds. */
246         public val durationMillis: Long
247 
248         init {
249             require(duration.isFinite() && !duration.isNegative()) {
250                 "Constant vibration duration must be finite and non-negative: $duration"
251             }
252             require(amplitude in (0.0..1.0) || amplitude == DEFAULT_AMPLITUDE) {
253                 "Constant vibration amplitude must be in [0,1]: $amplitude"
254             }
255             durationMillis = duration.inWholeMilliseconds
256         }
257 
258         public companion object {
259             /**
260              * The [amplitude] value that represents a hardware-specific default vibration strength.
261              */
262             public const val DEFAULT_AMPLITUDE: Float = -1f
263         }
264 
265         override fun equals(other: Any?): Boolean {
266             if (this === other) return true
267             if (other !is ConstantVibrationAtom) return false
268             if (durationMillis != other.durationMillis) return false
269             if (amplitude != other.amplitude) return false
270             return true
271         }
272 
273         override fun hashCode(): Int {
274             return Objects.hash(durationMillis, amplitude)
275         }
276 
277         override fun toString(): String {
278             return "ConstantVibrationAtom(durationMillis=$durationMillis" +
279                 ", amplitude=${if (amplitude == DEFAULT_AMPLITUDE) "default" else amplitude})"
280         }
281 
282         override fun isSupportedBy(deviceProfile: HapticDeviceProfile): Boolean =
283             deviceProfile.isAmplitudeControlSupported || hasPatternAmplitude()
284 
285         /** Returns true if amplitude is 0, 1 or [DEFAULT_AMPLITUDE]. */
286         internal fun hasPatternAmplitude(): Boolean =
287             (amplitude == 0f) || (amplitude == 1f) || (amplitude == DEFAULT_AMPLITUDE)
288     }
289 }
290 
291 /**
292  * A [RepeatingWaveformSignal] describes an infinite haptic signal where a waveform signal is played
293  * on repeat until canceled.
294  *
295  * A repeating waveform signal has an optional initial [WaveformSignal] that plays once before the
296  * repeating waveform signal is played on repeat until the vibration is canceled.
297  *
298  * @sample androidx.core.haptics.samples.RepeatingAmplitudeWaveform
299  */
300 public class RepeatingWaveformSignal
301 internal constructor(
302 
303     /** The optional initial waveform signal to be played once at the beginning of the vibration. */
304     public val initialWaveform: WaveformSignal?,
305 
306     /** The waveform signal to be repeated after the initial waveform. */
307     public val repeatingWaveform: WaveformSignal,
308 ) : InfiniteSignal() {
309 
equalsnull310     override fun equals(other: Any?): Boolean {
311         if (this === other) return true
312         if (other !is RepeatingWaveformSignal) return false
313         if (initialWaveform != other.initialWaveform) return false
314         if (repeatingWaveform != other.repeatingWaveform) return false
315         return true
316     }
317 
hashCodenull318     override fun hashCode(): Int {
319         return Objects.hash(initialWaveform, repeatingWaveform)
320     }
321 
toStringnull322     override fun toString(): String {
323         return "RepeatingWaveformSignal(initial=$initialWaveform, repeating=$repeatingWaveform)"
324     }
325 
toVibrationnull326     override fun toVibration(): VibrationWrapper? =
327         HapticSignalConverter.toVibration(
328             initialWaveform = initialWaveform,
329             repeatingWaveform = repeatingWaveform,
330         )
331 
332     override fun isSupportedBy(deviceProfile: HapticDeviceProfile): Boolean =
333         initialWaveform?.isSupportedBy(deviceProfile) != false &&
334             repeatingWaveform.isSupportedBy(deviceProfile)
335 }
336