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