/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.os; import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.ContentResolver; import android.content.Context; import android.hardware.vibrator.V1_0.EffectStrength; import android.hardware.vibrator.V1_3.Effect; import android.net.Uri; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.RampSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationEffectSegment; import android.util.MathUtils; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; /** * A VibrationEffect describes a haptic effect to be performed by a {@link Vibrator}. * * These effects may be any number of things, from single shot vibrations to complex waveforms. */ public abstract class VibrationEffect implements Parcelable { // Stevens' coefficient to scale the perceived vibration intensity. private static final float SCALE_GAMMA = 0.65f; /** * The default vibration strength of the device. */ public static final int DEFAULT_AMPLITUDE = -1; /** * The maximum amplitude value * @hide */ public static final int MAX_AMPLITUDE = 255; /** * A click effect. Use this effect as a baseline, as it's the most common type of click effect. */ public static final int EFFECT_CLICK = Effect.CLICK; /** * A double click effect. */ public static final int EFFECT_DOUBLE_CLICK = Effect.DOUBLE_CLICK; /** * A tick effect. This effect is less strong compared to {@link #EFFECT_CLICK}. */ public static final int EFFECT_TICK = Effect.TICK; /** * A thud effect. * @see #get(int) * @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @TestApi public static final int EFFECT_THUD = Effect.THUD; /** * A pop effect. * @see #get(int) * @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @TestApi public static final int EFFECT_POP = Effect.POP; /** * A heavy click effect. This effect is stronger than {@link #EFFECT_CLICK}. */ public static final int EFFECT_HEAVY_CLICK = Effect.HEAVY_CLICK; /** * A texture effect meant to replicate soft ticks. * * Unlike normal effects, texture effects are meant to be called repeatedly, generally in * response to some motion, in order to replicate the feeling of some texture underneath the * user's fingers. * * @see #get(int) * @hide */ @TestApi public static final int EFFECT_TEXTURE_TICK = Effect.TEXTURE_TICK; /** {@hide} */ @TestApi public static final int EFFECT_STRENGTH_LIGHT = EffectStrength.LIGHT; /** {@hide} */ @TestApi public static final int EFFECT_STRENGTH_MEDIUM = EffectStrength.MEDIUM; /** {@hide} */ @TestApi public static final int EFFECT_STRENGTH_STRONG = EffectStrength.STRONG; /** * Ringtone patterns. They may correspond with the device's ringtone audio, or may just be a * pattern that can be played as a ringtone with any audio, depending on the device. * * @see #get(Uri, Context) * @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @TestApi public static final int[] RINGTONES = { Effect.RINGTONE_1, Effect.RINGTONE_2, Effect.RINGTONE_3, Effect.RINGTONE_4, Effect.RINGTONE_5, Effect.RINGTONE_6, Effect.RINGTONE_7, Effect.RINGTONE_8, Effect.RINGTONE_9, Effect.RINGTONE_10, Effect.RINGTONE_11, Effect.RINGTONE_12, Effect.RINGTONE_13, Effect.RINGTONE_14, Effect.RINGTONE_15 }; /** @hide */ @IntDef(prefix = { "EFFECT_" }, value = { EFFECT_TICK, EFFECT_CLICK, EFFECT_HEAVY_CLICK, EFFECT_DOUBLE_CLICK, }) @Retention(RetentionPolicy.SOURCE) public @interface EffectType {} /** @hide to prevent subclassing from outside of the framework */ public VibrationEffect() { } /** * Create a one shot vibration. * * One shot vibrations will vibrate constantly for the specified period of time at the * specified amplitude, and then stop. * * @param milliseconds The number of milliseconds to vibrate. This must be a positive number. * @param amplitude The strength of the vibration. This must be a value between 1 and 255, or * {@link #DEFAULT_AMPLITUDE}. * * @return The desired effect. */ public static VibrationEffect createOneShot(long milliseconds, int amplitude) { if (amplitude == 0) { throw new IllegalArgumentException( "amplitude must either be DEFAULT_AMPLITUDE, " + "or between 1 and 255 inclusive (amplitude=" + amplitude + ")"); } return createWaveform(new long[]{milliseconds}, new int[]{amplitude}, -1 /* repeat */); } /** * Create a waveform vibration. * * Waveform vibrations are a potentially repeating series of timing and amplitude pairs. For * each pair, the value in the amplitude array determines the strength of the vibration and the * value in the timing array determines how long it vibrates for. An amplitude of 0 implies no * vibration (i.e. off), and any pairs with a timing value of 0 will be ignored. *
* The amplitude array of the generated waveform will be the same size as the given * timing array with alternating values of 0 (i.e. off) and {@link #DEFAULT_AMPLITUDE}, * starting with 0. Therefore the first timing value will be the period to wait before turning * the vibrator on, the second value will be how long to vibrate at {@link #DEFAULT_AMPLITUDE} * strength, etc. *
* To cause the pattern to repeat, pass the index into the timings array at which to start the * repetition, or -1 to disable repeating. *
* * @param timings The pattern of alternating on-off timings, starting with off. Timing values * of 0 will cause the timing / amplitude pair to be ignored. * @param repeat The index into the timings array at which to repeat, or -1 if you you don't * want to repeat. * * @return The desired effect. */ public static VibrationEffect createWaveform(long[] timings, int repeat) { int[] amplitudes = new int[timings.length]; for (int i = 0; i < (timings.length / 2); i++) { amplitudes[i*2 + 1] = VibrationEffect.DEFAULT_AMPLITUDE; } return createWaveform(timings, amplitudes, repeat); } /** * Create a waveform vibration. * * Waveform vibrations are a potentially repeating series of timing and amplitude pairs. For * each pair, the value in the amplitude array determines the strength of the vibration and the * value in the timing array determines how long it vibrates for, in milliseconds. Amplitude * values must be between 0 and 255, and an amplitude of 0 implies no vibration (i.e. off). Any * pairs with a timing value of 0 will be ignored. ** To cause the pattern to repeat, pass the index into the timings array at which to start the * repetition, or -1 to disable repeating. *
* * @param timings The timing values, in milliseconds, of the timing / amplitude pairs. Timing * values of 0 will cause the pair to be ignored. * @param amplitudes The amplitude values of the timing / amplitude pairs. Amplitude values * must be between 0 and 255, or equal to {@link #DEFAULT_AMPLITUDE}. An * amplitude value of 0 implies the motor is off. * @param repeat The index into the timings array at which to repeat, or -1 if you you don't * want to repeat. * * @return The desired effect. */ public static VibrationEffect createWaveform(long[] timings, int[] amplitudes, int repeat) { if (timings.length != amplitudes.length) { throw new IllegalArgumentException( "timing and amplitude arrays must be of equal length" + " (timings.length=" + timings.length + ", amplitudes.length=" + amplitudes.length + ")"); } ListThe waveform builder offers more flexibility for creating waveform vibrations, allowing * control over vibration frequency and ramping up or down the vibration amplitude, frequency or * both. * *
For simpler waveform patterns see {@link #createWaveform} methods.
*
* @hide
* @see VibrationEffect.WaveformBuilder
*/
@TestApi
@NonNull
public static WaveformBuilder startWaveform() {
return new WaveformBuilder();
}
@Override
public int describeContents() {
return 0;
}
/** @hide */
public abstract void validate();
/**
* Gets the estimated duration of the vibration in milliseconds.
*
* For effects without a defined end (e.g. a Waveform with a non-negative repeat index), this
* returns Long.MAX_VALUE. For effects with an unknown duration (e.g. Prebaked effects where
* the length is device and potentially run-time dependent), this returns -1.
*
* @hide
*/
@TestApi
public abstract long getDuration();
/**
* Resolve default values into integer amplitude numbers.
*
* @param defaultAmplitude the default amplitude to apply, must be between 0 and
* MAX_AMPLITUDE
* @return this if amplitude value is already set, or a copy of this effect with given default
* amplitude otherwise
*
* @hide
*/
public abstract Similar to {@link #addEffect(VibrationEffect, int)} , but with no delay applied.
*
* @param effect The effect to add to this composition as a primitive
* @return The {@link Composition} object to enable adding multiple primitives in one chain.
* @hide
*/
@TestApi
@NonNull
public Composition addEffect(@NonNull VibrationEffect effect) {
return addEffect(effect, /* delay= */ 0);
}
/**
* Add a haptic effect to the end of the current composition.
*
* @param effect The effect to add to this composition as a primitive
* @param delay The amount of time in milliseconds to wait before playing this primitive
* @return The {@link Composition} object to enable adding multiple primitives in one chain.
* @hide
*/
@TestApi
@NonNull
public Composition addEffect(@NonNull VibrationEffect effect,
@IntRange(from = 0) int delay) {
Preconditions.checkArgumentNonnegative(delay);
if (delay > 0) {
// Created a segment sustaining the zero amplitude to represent the delay.
addSegment(new StepSegment(/* amplitude= */ 0, /* frequency= */ 0,
/* duration= */ delay));
}
return addSegments(effect);
}
/**
* Add a haptic primitive to the end of the current composition.
*
* Similar to {@link #addPrimitive(int, float, int)}, but with no delay and a
* default scale applied.
*
* @param primitiveId The primitive to add
*
* @return The {@link Composition} object to enable adding multiple primitives in one chain.
*/
@NonNull
public Composition addPrimitive(@PrimitiveType int primitiveId) {
return addPrimitive(primitiveId, /*scale*/ 1.0f, /*delay*/ 0);
}
/**
* Add a haptic primitive to the end of the current composition.
*
* Similar to {@link #addPrimitive(int, float, int)}, but with no delay.
*
* @param primitiveId The primitive to add
* @param scale The scale to apply to the intensity of the primitive.
*
* @return The {@link Composition} object to enable adding multiple primitives in one chain.
*/
@NonNull
public Composition addPrimitive(@PrimitiveType int primitiveId,
@FloatRange(from = 0f, to = 1f) float scale) {
return addPrimitive(primitiveId, scale, /*delay*/ 0);
}
/**
* Add a haptic primitive to the end of the current composition.
*
* @param primitiveId The primitive to add
* @param scale The scale to apply to the intensity of the primitive.
* @param delay The amount of time in milliseconds to wait before playing this primitive,
* starting at the time the previous element in this composition is finished.
* @return The {@link Composition} object to enable adding multiple primitives in one chain.
*/
@NonNull
public Composition addPrimitive(@PrimitiveType int primitiveId,
@FloatRange(from = 0f, to = 1f) float scale, @IntRange(from = 0) int delay) {
PrimitiveSegment primitive = new PrimitiveSegment(primitiveId, scale,
delay);
primitive.validate();
return addSegment(primitive);
}
private Composition addSegment(VibrationEffectSegment segment) {
if (mRepeatIndex >= 0) {
throw new IllegalStateException(
"Composition already have a repeating effect so any new primitive would be"
+ " unreachable.");
}
mSegments.add(segment);
return this;
}
private Composition addSegments(VibrationEffect effect) {
if (mRepeatIndex >= 0) {
throw new IllegalStateException(
"Composition already have a repeating effect so any new primitive would be"
+ " unreachable.");
}
Composed composed = (Composed) effect;
if (composed.getRepeatIndex() >= 0) {
// Start repeating from the index relative to the composed waveform.
mRepeatIndex = mSegments.size() + composed.getRepeatIndex();
}
mSegments.addAll(composed.getSegments());
return this;
}
/**
* Compose all of the added primitives together into a single {@link VibrationEffect}.
*
* The {@link Composition} object is still valid after this call, so you can continue adding
* more primitives to it and generating more {@link VibrationEffect}s by calling this method
* again.
*
* @return The {@link VibrationEffect} resulting from the composition of the primitives.
*/
@NonNull
public VibrationEffect compose() {
if (mSegments.isEmpty()) {
throw new IllegalStateException(
"Composition must have at least one element to compose.");
}
VibrationEffect effect = new Composed(mSegments, mRepeatIndex);
effect.validate();
return effect;
}
/**
* Convert the primitive ID to a human readable string for debugging
* @param id The ID to convert
* @return The ID in a human readable format.
* @hide
*/
public static String primitiveToString(@PrimitiveType int id) {
switch (id) {
case PRIMITIVE_NOOP:
return "PRIMITIVE_NOOP";
case PRIMITIVE_CLICK:
return "PRIMITIVE_CLICK";
case PRIMITIVE_THUD:
return "PRIMITIVE_THUD";
case PRIMITIVE_SPIN:
return "PRIMITIVE_SPIN";
case PRIMITIVE_QUICK_RISE:
return "PRIMITIVE_QUICK_RISE";
case PRIMITIVE_SLOW_RISE:
return "PRIMITIVE_SLOW_RISE";
case PRIMITIVE_QUICK_FALL:
return "PRIMITIVE_QUICK_FALL";
case PRIMITIVE_TICK:
return "PRIMITIVE_TICK";
case PRIMITIVE_LOW_TICK:
return "PRIMITIVE_LOW_TICK";
default:
return Integer.toString(id);
}
}
}
/**
* A builder for waveform haptic effects.
*
* Waveform vibrations constitute of one or more timed segments where the vibration
* amplitude, frequency or both can linearly ramp to new values.
*
* Waveform segments may have zero duration, which represent a jump to new vibration
* amplitude and/or frequency values.
*
* Waveform segments may have the same start and end vibration amplitude and frequency,
* which represent a step where the amplitude and frequency are maintained for that duration.
*
* @hide
* @see VibrationEffect#startWaveform()
*/
@TestApi
public static final class WaveformBuilder {
private ArrayList If the duration is zero the vibrator will jump to new amplitude.
*
* @param amplitude The amplitude for this step
* @param duration The duration of this step in milliseconds
* @return The {@link WaveformBuilder} object to enable adding multiple steps in chain.
*/
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public WaveformBuilder addStep(@FloatRange(from = 0f, to = 1f) float amplitude,
@IntRange(from = 0) int duration) {
return addStep(amplitude, getPreviousFrequency(), duration);
}
/**
* Vibrate with given amplitude for the given duration, in millis, keeping the previous
* vibration frequency the same.
*
* If the duration is zero the vibrator will jump to new amplitude.
*
* @param amplitude The amplitude for this step
* @param frequency The frequency for this step
* @param duration The duration of this step in milliseconds
* @return The {@link WaveformBuilder} object to enable adding multiple steps in chain.
*/
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public WaveformBuilder addStep(@FloatRange(from = 0f, to = 1f) float amplitude,
@FloatRange(from = -1f, to = 1f) float frequency,
@IntRange(from = 0) int duration) {
mSegments.add(new StepSegment(amplitude, frequency, duration));
return this;
}
/**
* Ramp vibration linearly for the given duration, in millis, from previous amplitude value
* to the given one, keeping previous frequency.
*
* If the duration is zero the vibrator will jump to new amplitude.
*
* @param amplitude The final amplitude this ramp should reach
* @param duration The duration of this ramp in milliseconds
* @return The {@link WaveformBuilder} object to enable adding multiple steps in chain.
*/
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public WaveformBuilder addRamp(@FloatRange(from = 0f, to = 1f) float amplitude,
@IntRange(from = 0) int duration) {
return addRamp(amplitude, getPreviousFrequency(), duration);
}
/**
* Ramp vibration linearly for the given duration, in millis, from previous amplitude and
* frequency values to the given ones.
*
* If the duration is zero the vibrator will jump to new amplitude and frequency.
*
* @param amplitude The final amplitude this ramp should reach
* @param frequency The final frequency this ramp should reach
* @param duration The duration of this ramp in milliseconds
* @return The {@link WaveformBuilder} object to enable adding multiple steps in chain.
*/
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public WaveformBuilder addRamp(@FloatRange(from = 0f, to = 1f) float amplitude,
@FloatRange(from = -1f, to = 1f) float frequency,
@IntRange(from = 0) int duration) {
mSegments.add(new RampSegment(getPreviousAmplitude(), amplitude, getPreviousFrequency(),
frequency, duration));
return this;
}
/**
* Compose all of the steps together into a single {@link VibrationEffect}.
*
* The {@link WaveformBuilder} object is still valid after this call, so you can
* continue adding more primitives to it and generating more {@link VibrationEffect}s by
* calling this method again.
*
* @return The {@link VibrationEffect} resulting from the composition of the steps.
*/
@NonNull
public VibrationEffect build() {
return build(/* repeat= */ -1);
}
/**
* Compose all of the steps together into a single {@link VibrationEffect}.
*
* To cause the pattern to repeat, pass the index at which to start the repetition
* (starting at 0), or -1 to disable repeating.
*
* The {@link WaveformBuilder} object is still valid after this call, so you can
* continue adding more primitives to it and generating more {@link VibrationEffect}s by
* calling this method again.
*
* @return The {@link VibrationEffect} resulting from the composition of the steps.
*/
@NonNull
public VibrationEffect build(int repeat) {
if (mSegments.isEmpty()) {
throw new IllegalStateException(
"WaveformBuilder must have at least one element to build.");
}
VibrationEffect effect = new Composed(mSegments, repeat);
effect.validate();
return effect;
}
private float getPreviousFrequency() {
if (!mSegments.isEmpty()) {
VibrationEffectSegment segment = mSegments.get(mSegments.size() - 1);
if (segment instanceof StepSegment) {
return ((StepSegment) segment).getFrequency();
} else if (segment instanceof RampSegment) {
return ((RampSegment) segment).getEndFrequency();
}
}
return 0;
}
private float getPreviousAmplitude() {
if (!mSegments.isEmpty()) {
VibrationEffectSegment segment = mSegments.get(mSegments.size() - 1);
if (segment instanceof StepSegment) {
return ((StepSegment) segment).getAmplitude();
} else if (segment instanceof RampSegment) {
return ((RampSegment) segment).getEndAmplitude();
}
}
return 0;
}
}
@NonNull
public static final Parcelable.Creator