/* * 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 static android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS; import android.annotation.DurationMillisLong; import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.ContentResolver; import android.content.Context; import android.hardware.vibrator.IVibrator; import android.hardware.vibrator.V1_0.EffectStrength; import android.hardware.vibrator.V1_3.Effect; import android.net.Uri; import android.os.vibrator.BasicPwleSegment; import android.os.vibrator.Flags; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.PwleSegment; import android.os.vibrator.RampSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationEffectSegment; import android.os.vibrator.VibratorEnvelopeEffectInfo; import android.os.vibrator.VibratorFrequencyProfileLegacy; import android.util.MathUtils; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.StringJoiner; import java.util.function.BiFunction; import java.util.function.Function; /** * 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 { private static final int PARCEL_TOKEN_COMPOSED = 1; private static final int PARCEL_TOKEN_VENDOR_EFFECT = 2; // Stevens' coefficient to scale the perceived vibration intensity. private static final float SCALE_GAMMA = 0.65f; // If a vibration is playing for longer than 1s, it's probably not haptic feedback private static final long MAX_HAPTIC_FEEDBACK_DURATION = 1000; // If a vibration is playing more than 3 constants, it's probably not haptic feedback private static final long MAX_HAPTIC_FEEDBACK_COMPOSITION_SIZE = 3; /** * 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, using only off/on transitions at the provided time intervals, * and potentially repeating. * *
In effect, the timings array represents the number of milliseconds before turning * the vibrator on, followed by the number of milliseconds to keep the vibrator on, then * the number of milliseconds turned off, and so on. Consequently, the first timing value will * often be 0, so that the effect will start vibrating immediately. * *
This method is equivalent to calling {@link #createWaveform(long[], int[], int)} with * corresponding amplitude values alternating between 0 and {@link #DEFAULT_AMPLITUDE}, * beginning with 0. * *
To cause the pattern to repeat, pass the index into the timings array at which to start * the repetition, or -1 to disable repeating. Repeating effects will be played indefinitely * and should be cancelled via {@link Vibrator#cancel()}. * * @param timings The pattern of alternating on-off timings, starting with an 'off' timing, and * representing the length of time to sustain the individual item (not * cumulative). * @param repeat The index into the timings array at which to repeat, or -1 if you don't * want to repeat indefinitely. * * @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); } /** * Computes a legacy vibration pattern (i.e. a pattern with duration values for "off/on" * vibration components) that is equivalent to this VibrationEffect. * *
All non-repeating effects created with {@link #createWaveform(long[], int)} are * convertible into an equivalent vibration pattern with this method. It is not guaranteed that * an effect created with other means becomes converted into an equivalent legacy vibration * pattern, even if it has an equivalent vibration pattern. If this method is unable to create * an equivalent vibration pattern for such effects, it will return {@code null}. * *
Note that a valid equivalent long[] pattern cannot be created for an effect that has any * form of repeating behavior, regardless of how the effect was created. For repeating effects, * the method will always return {@code null}. * * @return a long array representing a vibration pattern equivalent to the VibrationEffect, if * the method successfully derived a vibration pattern equivalent to the effect * (this will always be the case if the effect was created via * {@link #createWaveform(long[], int)} and is non-repeating). Otherwise, returns * {@code null}. * @hide */ @TestApi @Nullable public abstract long[] computeCreateWaveformOffOnTimingsOrNull(); /** * Create a waveform vibration. * *
Waveform vibrations are a potentially repeating series of timing and amplitude pairs, * provided in separate arrays. 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. * *
To cause the pattern to repeat, pass the index into the timings array at which to start
* the repetition, or -1 to disable repeating. Repeating effects will be played indefinitely
* and should be cancelled via {@link Vibrator#cancel()}.
*
* @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 don't
* want to repeat indefinitely.
*
* @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 + ")");
}
List Predefined effects are a set of common vibration effects that should be identical,
* regardless of the app they come from, in order to provide a cohesive experience for users
* across the entire device. They also may be custom tailored to the device hardware in order to
* provide a better experience than you could otherwise build using the generic building
* blocks.
*
* This will fallback to a generic pattern if one exists and there does not exist a
* hardware-specific implementation of the effect.
*
* @param effectId The ID of the effect to perform:
* {@link #EFFECT_CLICK}, {@link #EFFECT_DOUBLE_CLICK}, {@link #EFFECT_TICK}
*
* @return The desired effect.
*/
@NonNull
public static VibrationEffect createPredefined(@EffectType int effectId) {
return get(effectId, true);
}
/**
* Create a vendor-defined vibration effect.
*
* Vendor effects offer more flexibility for accessing vendor-specific vibrator capabilities,
* enabling control over any vibration parameter and more generic vibration waveforms for apps
* provided by the device vendor.
*
* This requires hardware-specific implementation of the effect and will not have any
* platform fallback support.
*
* @param effect An opaque representation of the vibration effect which can also be serialized.
* @return The desired effect.
* @hide
*/
@NonNull
@SystemApi
@FlaggedApi(FLAG_VENDOR_VIBRATION_EFFECTS)
@RequiresPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS)
public static VibrationEffect createVendorEffect(@NonNull PersistableBundle effect) {
VibrationEffect vendorEffect = new VendorEffect(effect, VendorEffect.DEFAULT_STRENGTH,
VendorEffect.DEFAULT_SCALE, VendorEffect.DEFAULT_SCALE);
vendorEffect.validate();
return vendorEffect;
}
/**
* Get a predefined vibration effect.
*
* Predefined effects are a set of common vibration effects that should be identical,
* regardless of the app they come from, in order to provide a cohesive experience for users
* across the entire device. They also may be custom tailored to the device hardware in order to
* provide a better experience than you could otherwise build using the generic building
* blocks.
*
* This will fallback to a generic pattern if one exists and there does not exist a
* hardware-specific implementation of the effect.
*
* @param effectId The ID of the effect to perform:
* {@link #EFFECT_CLICK}, {@link #EFFECT_DOUBLE_CLICK}, {@link #EFFECT_TICK}
*
* @return The desired effect.
* @hide
*/
@TestApi
public static VibrationEffect get(int effectId) {
return get(effectId, PrebakedSegment.DEFAULT_SHOULD_FALLBACK);
}
/**
* Get a predefined vibration effect.
*
* Predefined effects are a set of common vibration effects that should be identical,
* regardless of the app they come from, in order to provide a cohesive experience for users
* across the entire device. They also may be custom tailored to the device hardware in order to
* provide a better experience than you could otherwise build using the generic building
* blocks.
*
* Some effects you may only want to play if there's a hardware specific implementation
* because they may, for example, be too disruptive to the user without tuning. The
* {@code fallback} parameter allows you to decide whether you want to fallback to the generic
* implementation or only play if there's a tuned, hardware specific one available.
*
* @param effectId The ID of the effect to perform:
* {@link #EFFECT_CLICK}, {@link #EFFECT_DOUBLE_CLICK}, {@link #EFFECT_TICK}
* @param fallback Whether to fall back to a generic pattern if a hardware specific
* implementation doesn't exist.
*
* @return The desired effect.
* @hide
*/
@TestApi
public static VibrationEffect get(int effectId, boolean fallback) {
VibrationEffect effect = new Composed(
new PrebakedSegment(effectId, fallback, PrebakedSegment.DEFAULT_STRENGTH));
effect.validate();
return effect;
}
/**
* Get a predefined vibration effect associated with a given URI.
*
* Predefined effects are a set of common vibration effects that should be identical,
* regardless of the app they come from, in order to provide a cohesive experience for users
* across the entire device. They also may be custom tailored to the device hardware in order to
* provide a better experience than you could otherwise build using the generic building
* blocks.
*
* @param uri The URI associated with the haptic effect.
* @param context The context used to get the URI to haptic effect association.
*
* @return The desired effect, or {@code null} if there's no associated effect.
*
* @hide
*/
@TestApi
@Nullable
public static VibrationEffect get(Uri uri, Context context) {
String[] uris = context.getResources().getStringArray(
com.android.internal.R.array.config_ringtoneEffectUris);
// Skip doing any IPC if we don't have any effects configured.
if (uris.length == 0) {
return null;
}
final ContentResolver cr = context.getContentResolver();
Uri uncanonicalUri = cr.uncanonicalize(uri);
if (uncanonicalUri == null) {
// If we already had an uncanonical URI, it's possible we'll get null back here. In
// this case, just use the URI as passed in since it wasn't canonicalized in the first
// place.
uncanonicalUri = uri;
}
for (int i = 0; i < uris.length && i < RINGTONES.length; i++) {
if (uris[i] == null) {
continue;
}
Uri mappedUri = cr.uncanonicalize(Uri.parse(uris[i]));
if (mappedUri == null) {
continue;
}
if (mappedUri.equals(uncanonicalUri)) {
return get(RINGTONES[i]);
}
}
return null;
}
/**
* Start composing a haptic effect.
*
* @see VibrationEffect.Composition
*/
@NonNull
public static Composition startComposition() {
return new Composition();
}
/**
* Start building a waveform vibration.
*
* The waveform builder offers more flexibility for creating waveform vibrations, allowing
* control over vibration amplitude and frequency via smooth transitions between values.
*
* The waveform will start the first transition from the vibrator off state, with the
* resonant frequency by default. To provide an initial state, use
* {@link #startWaveform(VibrationEffect.VibrationParameter)}.
*
* @see VibrationEffect.WaveformBuilder
* @hide
*/
@TestApi
@NonNull
public static WaveformBuilder startWaveform() {
return new WaveformBuilder();
}
/**
* Start building a waveform vibration with an initial state specified by a
* {@link VibrationEffect.VibrationParameter}.
*
* The waveform builder offers more flexibility for creating waveform vibrations, allowing
* control over vibration amplitude and frequency via smooth transitions between values.
*
* @param initialParameter The initial {@link VibrationEffect.VibrationParameter} value to be
* applied at the beginning of the vibration.
* @return The {@link VibrationEffect.WaveformBuilder} started with the initial parameters.
*
* @see VibrationEffect.WaveformBuilder
* @hide
*/
@TestApi
@NonNull
public static WaveformBuilder startWaveform(@NonNull VibrationParameter initialParameter) {
WaveformBuilder builder = startWaveform();
builder.addTransition(Duration.ZERO, initialParameter);
return builder;
}
/**
* Start building a waveform vibration with an initial state specified by two
* {@link VibrationEffect.VibrationParameter VibrationParameters}.
*
* The waveform builder offers more flexibility for creating waveform vibrations, allowing
* control over vibration amplitude and frequency via smooth transitions between values.
*
* @param initialParameter1 The initial {@link VibrationEffect.VibrationParameter} value to be
* applied at the beginning of the vibration.
* @param initialParameter2 The initial {@link VibrationEffect.VibrationParameter} value to be
* applied at the beginning of the vibration, must be a different type
* of parameter than the one specified by the first argument.
* @return The {@link VibrationEffect.WaveformBuilder} started with the initial parameters.
*
* @see VibrationEffect.WaveformBuilder
* @hide
*/
@TestApi
@NonNull
public static WaveformBuilder startWaveform(@NonNull VibrationParameter initialParameter1,
@NonNull VibrationParameter initialParameter2) {
WaveformBuilder builder = startWaveform();
builder.addTransition(Duration.ZERO, initialParameter1, initialParameter2);
return builder;
}
@Override
public int describeContents() {
return 0;
}
/** @hide */
public abstract void validate();
/**
* If supported, truncate the length of this vibration effect to the provided length and return
* the result. Will always return null for repeating effects.
*
* @return The desired effect, or {@code null} if truncation is not applicable.
* @hide
*/
@Nullable
public abstract VibrationEffect cropToLengthOrNull(int length);
/**
* 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. predefined effects where
* the length is device and potentially run-time dependent), this returns -1.
*
* @hide
*/
@TestApi
public abstract long getDuration();
/**
* Gets the estimated duration of the segment for given vibrator, in milliseconds.
*
* For effects with hardware-dependent constants (e.g. primitive compositions), this returns
* the estimated duration based on the given {@link VibratorInfo}. For all other effects this
* will return the same as {@link #getDuration()}.
*
* @hide
*/
public long getDuration(@Nullable VibratorInfo vibratorInfo) {
return getDuration();
}
/**
* Checks if a vibrator with a given {@link VibratorInfo} can play this effect as intended.
*
* See {@link VibratorInfo#areVibrationFeaturesSupported(VibrationEffect)} for more
* information about what counts as supported by a vibrator, and what counts as not.
*
* @hide
*/
public abstract boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo);
/**
* Returns true if this effect could represent a touch haptic feedback.
*
* It is strongly recommended that an instance of {@link VibrationAttributes} is specified
* for each vibration, with the correct usage. When a vibration is played with usage UNKNOWN,
* then this method will be used to classify the most common use case and make sure they are
* covered by the user settings for "Touch feedback".
*
* @hide
*/
public boolean isHapticFeedbackCandidate() {
return false;
}
/**
* 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
*/
@NonNull
public abstract VibrationEffect resolve(int defaultAmplitude);
/**
* Applies given effect strength to predefined and vendor-specific effects.
*
* @param effectStrength new effect strength to be applied, one of
* VibrationEffect.EFFECT_STRENGTH_*.
* @return this if there is no change, or a copy of this effect with new strength otherwise
* @hide
*/
@NonNull
public abstract VibrationEffect applyEffectStrength(int effectStrength);
/**
* Scale the vibration effect intensity with the given constraints.
*
* @param scaleFactor scale factor to be applied to the intensity. Values within [0,1) will
* scale down the intensity, values larger than 1 will scale up
* @return this if there is no scaling to be done, or a copy of this effect with scaled
* vibration intensity otherwise
*
* @hide
*/
@NonNull
public abstract VibrationEffect scale(float scaleFactor);
/**
* Performs a linear scaling on the effect intensity with the given factor.
*
* @param scaleFactor scale factor to be applied to the intensity. Values within [0,1) will
* scale down the intensity, values larger than 1 will scale up
* @return this if there is no scaling to be done, or a copy of this effect with scaled
* vibration intensity otherwise
* @hide
*/
@NonNull
public abstract VibrationEffect applyAdaptiveScale(float scaleFactor);
/**
* Ensures that the effect is repeating indefinitely or not. This is a lossy operation and
* should only be applied once to an original effect - it shouldn't be applied to the
* result of this method.
*
* Non-repeating effects will be made repeating by looping the entire effect with the
* specified delay between each loop. The delay is added irrespective of whether the effect
* already has a delay at the beginning or end.
*
* Repeating effects will be left with their native repeating portion if it should be
* repeating, and otherwise the loop index is removed, so that the entire effect plays once.
*
* @param wantRepeating Whether the effect is required to be repeating or not.
* @param loopDelayMs The milliseconds to pause between loops, if repeating is to be added to
* the effect. Ignored if {@code repeating==false} or the effect is already
* repeating itself. No delay is added if <= 0.
* @return this if the effect already satisfies the repeating requirement, or a copy of this
* adjusted to repeat or not repeat as appropriate.
* @hide
*/
@NonNull
public abstract VibrationEffect applyRepeatingIndefinitely(
boolean wantRepeating, int loopDelayMs);
/**
* Scale given vibration intensity by the given factor.
*
* This scale is not necessarily linear and may apply a gamma correction to the scale
* factor before using it.
*
* @param intensity relative intensity of the effect, must be between 0 and 1
* @param scaleFactor scale factor to be applied to the intensity. Values within [0,1) will
* scale down the intensity, values larger than 1 will scale up
* @return the scaled intensity which will be values within [0, 1].
*
* @hide
*/
public static float scale(float intensity, float scaleFactor) {
if (Flags.hapticsScaleV2Enabled()) {
if (Float.compare(scaleFactor, 1) <= 0 || Float.compare(intensity, 0) == 0) {
// Scaling down or scaling zero intensity is straightforward.
return scaleFactor * intensity;
}
// Using S * x / (1 + (S - 1) * x^2) as the scale up function to converge to 1.0.
return (scaleFactor * intensity) / (1 + (scaleFactor - 1) * intensity * intensity);
}
// Applying gamma correction to the scale factor, which is the same as encoding the input
// value, scaling it, then decoding the scaled value.
float scale = MathUtils.pow(scaleFactor, 1f / SCALE_GAMMA);
if (scaleFactor <= 1) {
// Scale down is simply a gamma corrected application of scaleFactor to the intensity.
// Scale up requires a different curve to ensure the intensity will not become > 1.
return intensity * scale;
}
// Apply the scale factor a few more times to make the ramp curve closer to the raw scale.
float extraScale = MathUtils.pow(scaleFactor, 4f - scaleFactor);
float x = intensity * scale * extraScale;
float maxX = scale * extraScale; // scaled x for intensity == 1
float expX = MathUtils.exp(x);
float expMaxX = MathUtils.exp(maxX);
// Using f = tanh as the scale up function so the max value will converge.
// a = 1/f(maxX), used to scale f so that a*f(maxX) = 1 (the value will converge to 1).
float a = (expMaxX + 1f) / (expMaxX - 1f);
float fx = (expX - 1f) / (expX + 1f);
return MathUtils.constrain(a * fx, 0f, 1f);
}
/**
* Performs a linear scaling on the given vibration intensity by the given factor.
*
* @param intensity relative intensity of the effect, must be between 0 and 1.
* @param scaleFactor scale factor to be applied to the intensity. Values within [0,1) will
* scale down the intensity, values larger than 1 will scale up.
* @return the scaled intensity which will be values within [0, 1].
*
* @hide
*/
public static float scaleLinearly(float intensity, float scaleFactor) {
return MathUtils.constrain(intensity * scaleFactor, 0f, 1f);
}
/**
* Returns a compact version of the {@link #toString()} result for debugging purposes.
*
* @hide
*/
public abstract String toDebugString();
/** @hide */
public static String effectIdToString(int effectId) {
return switch (effectId) {
case EFFECT_CLICK -> "CLICK";
case EFFECT_TICK -> "TICK";
case EFFECT_HEAVY_CLICK -> "HEAVY_CLICK";
case EFFECT_DOUBLE_CLICK -> "DOUBLE_CLICK";
case EFFECT_POP -> "POP";
case EFFECT_THUD -> "THUD";
case EFFECT_TEXTURE_TICK -> "TEXTURE_TICK";
default -> Integer.toString(effectId);
};
}
/** @hide */
public static String effectStrengthToString(int effectStrength) {
return switch (effectStrength) {
case EFFECT_STRENGTH_LIGHT -> "LIGHT";
case EFFECT_STRENGTH_MEDIUM -> "MEDIUM";
case EFFECT_STRENGTH_STRONG -> "STRONG";
default -> Integer.toString(effectStrength);
};
}
/**
* Transforms a {@link VibrationEffect} using a generic parameter.
*
* This can be used for scaling effects based on user settings or adapting them to the
* capabilities of a specific device vibrator.
*
* @param If the type isn't supported. The equality is done by {@link Object#equals(Object)}.
*/
private static boolean isPersistableBundleSupportedValueEquals(
Object first, Object second) {
if (first == second) {
return true;
} else if (first == null || second == null
|| !first.getClass().equals(second.getClass())) {
return false;
} else if (first instanceof PersistableBundle) {
return isPersistableBundleEquals(
(PersistableBundle) first, (PersistableBundle) second);
} else if (first instanceof int[]) {
return Arrays.equals((int[]) first, (int[]) second);
} else if (first instanceof long[]) {
return Arrays.equals((long[]) first, (long[]) second);
} else if (first instanceof double[]) {
return Arrays.equals((double[]) first, (double[]) second);
} else if (first instanceof boolean[]) {
return Arrays.equals((boolean[]) first, (boolean[]) second);
} else if (first instanceof String[]) {
return Arrays.equals((String[]) first, (String[]) second);
} else {
return Objects.equals(first, second);
}
}
@NonNull
public static final Creator The input vibration must not be a repeating vibration. If it is, an
* {@link IllegalArgumentException} will be thrown.
*
* @param effect The {@link VibrationEffect} that will be repeated.
* @return A {@link VibrationEffect} that repeats the effect indefinitely.
* @throws IllegalArgumentException if the effect is already a repeating vibration.
*/
@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
@NonNull
public static VibrationEffect createRepeatingEffect(@NonNull VibrationEffect effect) {
return VibrationEffect.startComposition()
.repeatEffectIndefinitely(effect)
.compose();
}
/**
* Creates a new {@link VibrationEffect} by merging the preamble and repeating vibration effect.
*
* Neither input vibration may already be repeating. An {@link IllegalArgumentException} will
* be thrown if either input vibration is set to repeat indefinitely.
*
* @param preamble The starting vibration effect, which must be finite.
* @param repeatingEffect The vibration effect to be repeated indefinitely after the preamble.
* @return A {@link VibrationEffect} that plays the preamble once followed by the
* `repeatingEffect` indefinitely.
* @throws IllegalArgumentException if either preamble or repeatingEffect is already a repeating
* vibration.
*/
@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
@NonNull
public static VibrationEffect createRepeatingEffect(@NonNull VibrationEffect preamble,
@NonNull VibrationEffect repeatingEffect) {
Preconditions.checkArgument(preamble.getDuration() < Long.MAX_VALUE,
"Can't repeat an indefinitely repeating effect.");
return VibrationEffect.startComposition()
.addEffect(preamble)
.repeatEffectIndefinitely(repeatingEffect)
.compose();
}
/**
* A composition of haptic elements that are combined to be playable as a single
* {@link VibrationEffect}.
*
* The haptic primitives are available as {@code Composition.PRIMITIVE_*} constants and
* can be added to a composition to create a custom vibration effect. Here is an example of an
* effect that grows in intensity and then dies off, with a longer rising portion for emphasis
* and an extra tick 100ms after:
*
* When choosing to play a composed effect, you should check that individual components are
* supported by the device by using {@link Vibrator#arePrimitivesSupported}.
*
* @see VibrationEffect#startComposition()
*/
public static final class Composition {
/** @hide */
@IntDef(prefix = { "PRIMITIVE_" }, value = {
PRIMITIVE_CLICK,
PRIMITIVE_THUD,
PRIMITIVE_SPIN,
PRIMITIVE_QUICK_RISE,
PRIMITIVE_SLOW_RISE,
PRIMITIVE_QUICK_FALL,
PRIMITIVE_TICK,
PRIMITIVE_LOW_TICK,
})
@Retention(RetentionPolicy.SOURCE)
public @interface PrimitiveType {
}
/** @hide */
@IntDef(prefix = { "DELAY_TYPE_" }, value = {
DELAY_TYPE_PAUSE,
DELAY_TYPE_RELATIVE_START_OFFSET,
})
@Retention(RetentionPolicy.SOURCE)
public @interface DelayType {
}
/**
* Exception thrown when adding an element to a {@link Composition} that already ends in an
* indefinitely repeating effect.
* @hide
*/
@TestApi
public static final class UnreachableAfterRepeatingIndefinitelyException
extends IllegalStateException {
UnreachableAfterRepeatingIndefinitelyException() {
super("Compositions ending in an indefinitely repeating effect can't be extended");
}
}
/**
* No haptic effect. Used to generate extended delays between primitives.
*
* @hide
*/
public static final int PRIMITIVE_NOOP = 0;
/**
* This effect should produce a sharp, crisp click sensation.
*/
public static final int PRIMITIVE_CLICK = 1;
/**
* A haptic effect that simulates downwards movement with gravity. Often
* followed by extra energy of hitting and reverberation to augment
* physicality.
*/
public static final int PRIMITIVE_THUD = 2;
/**
* A haptic effect that simulates spinning momentum.
*/
public static final int PRIMITIVE_SPIN = 3;
/**
* A haptic effect that simulates quick upward movement against gravity.
*/
public static final int PRIMITIVE_QUICK_RISE = 4;
/**
* A haptic effect that simulates slow upward movement against gravity.
*/
public static final int PRIMITIVE_SLOW_RISE = 5;
/**
* A haptic effect that simulates quick downwards movement with gravity.
*/
public static final int PRIMITIVE_QUICK_FALL = 6;
/**
* This very short effect should produce a light crisp sensation intended
* to be used repetitively for dynamic feedback.
*/
// Internally this maps to the HAL constant CompositePrimitive::LIGHT_TICK
public static final int PRIMITIVE_TICK = 7;
/**
* This very short low frequency effect should produce a light crisp sensation
* intended to be used repetitively for dynamic feedback.
*/
// Internally this maps to the HAL constant CompositePrimitive::LOW_TICK
public static final int PRIMITIVE_LOW_TICK = 8;
/**
* The delay represents a pause in the composition between the end of the previous primitive
* and the beginning of the next one.
*
* The primitive will start after the requested pause after the last primitive ended.
* The actual time the primitive will be played depends on the previous primitive's actual
* duration on the device hardware. This enables the combination of primitives to create
* more complex effects based on how close to each other they'll play. Here is an example:
*
* The primitive will start at the requested fixed time after the last primitive started,
* independently of that primitive's actual duration on the device hardware. This enables
* precise timings of primitives within a composition, ensuring they'll be played at the
* desired intervals. Here is an example:
*
* A primitive will be dropped from the composition if it overlaps with previous ones.
*/
@FlaggedApi(Flags.FLAG_PRIMITIVE_COMPOSITION_ABSOLUTE_DELAY)
public static final int DELAY_TYPE_RELATIVE_START_OFFSET = 1;
private final ArrayList If this effect is repeating (e.g. created by {@link VibrationEffect#createWaveform}
* with a non-negative repeat index, or created by another composition that has effects
* repeating indefinitely), then no more effects or primitives will be accepted by this
* composition after this method. Such effects should be cancelled via
* {@link Vibrator#cancel()}.
*
* @param effect The effect to add to the end of this composition.
* @return This {@link Composition} object to enable adding multiple elements in one chain.
*
* @throws UnreachableAfterRepeatingIndefinitelyException if the composition is currently
* ending with a repeating effect.
* @hide
*/
@TestApi
@NonNull
public Composition addEffect(@NonNull VibrationEffect effect) {
return addSegments(effect);
}
/**
* Add a haptic effect to the end of the current composition and play it on repeat,
* indefinitely.
*
* The entire effect will be played on repeat, indefinitely, after all other elements
* already added to this composition are played. No more effects or primitives will be
* accepted by this composition after this method. Such effects should be cancelled via
* {@link Vibrator#cancel()}.
*
* @param effect The effect to add to the end of this composition, must be finite.
* @return This {@link Composition} object to enable adding multiple elements in one chain,
* although only {@link #compose()} can follow this call.
*
* @throws IllegalArgumentException if the given effect is already repeating indefinitely.
* @throws UnreachableAfterRepeatingIndefinitelyException if the composition is currently
* ending with a repeating effect.
* @hide
*/
@TestApi
@NonNull
public Composition repeatEffectIndefinitely(@NonNull VibrationEffect effect) {
Preconditions.checkArgument(effect.getDuration() < Long.MAX_VALUE,
"Can't repeat an indefinitely repeating effect. Consider addEffect instead.");
int previousSegmentCount = mSegments.size();
addSegments(effect);
// Set repeat after segments were added, since addSegments checks this index.
mRepeatIndex = previousSegmentCount;
return this;
}
/**
* 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 This {@link Composition} object to enable adding multiple elements in one chain.
*/
@NonNull
public Composition addPrimitive(@PrimitiveType int primitiveId) {
return addPrimitive(primitiveId, PrimitiveSegment.DEFAULT_SCALE);
}
/**
* 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 This {@link Composition} object to enable adding multiple elements in one chain.
*/
@NonNull
public Composition addPrimitive(@PrimitiveType int primitiveId,
@FloatRange(from = 0f, to = 1f) float scale) {
return addPrimitive(primitiveId, scale, PrimitiveSegment.DEFAULT_DELAY_MILLIS);
}
/**
* Add a haptic primitive to the end of the current composition.
*
* Similar to {@link #addPrimitive(int, float, int, int)}, but default
* delay type applied is {@link #DELAY_TYPE_PAUSE}.
*
* @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 between the end of the last
* primitive and the beginning of this one (i.e. a pause in the composition).
* @return This {@link Composition} object to enable adding multiple elements in one chain.
*/
@NonNull
public Composition addPrimitive(@PrimitiveType int primitiveId,
@FloatRange(from = 0f, to = 1f) float scale, @IntRange(from = 0) int delay) {
return addPrimitive(primitiveId, scale, delay, PrimitiveSegment.DEFAULT_DELAY_TYPE);
}
/**
* 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,
* as defined by the given {@code delayType}.
* @param delayType The type of delay to be applied, e.g. a pause between last primitive and
* this one or a start offset.
* @return This {@link Composition} object to enable adding multiple elements in one chain.
*/
@FlaggedApi(Flags.FLAG_PRIMITIVE_COMPOSITION_ABSOLUTE_DELAY)
@NonNull
public Composition addPrimitive(@PrimitiveType int primitiveId,
@FloatRange(from = 0f, to = 1f) float scale, @IntRange(from = 0) int delay,
@DelayType int delayType) {
PrimitiveSegment primitive = new PrimitiveSegment(primitiveId, scale, delay, delayType);
primitive.validate();
return addSegment(primitive);
}
private Composition addSegment(VibrationEffectSegment segment) {
if (mRepeatIndex >= 0) {
throw new UnreachableAfterRepeatingIndefinitelyException();
}
mSegments.add(segment);
return this;
}
private Composition addSegments(VibrationEffect effect) {
if (mRepeatIndex >= 0) {
throw new UnreachableAfterRepeatingIndefinitelyException();
}
if (!(effect instanceof Composed composed)) {
throw new IllegalArgumentException("Can't add vendor effects to composition.");
}
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) {
return switch (id) {
case PRIMITIVE_NOOP -> "NOOP";
case PRIMITIVE_CLICK -> "CLICK";
case PRIMITIVE_THUD -> "THUD";
case PRIMITIVE_SPIN -> "SPIN";
case PRIMITIVE_QUICK_RISE -> "QUICK_RISE";
case PRIMITIVE_SLOW_RISE -> "SLOW_RISE";
case PRIMITIVE_QUICK_FALL -> "QUICK_FALL";
case PRIMITIVE_TICK -> "TICK";
case PRIMITIVE_LOW_TICK -> "LOW_TICK";
default -> Integer.toString(id);
};
}
/**
* Convert the delay type to a human readable string for debugging.
* @param type The delay type to convert
* @return The delay type in a human readable format.
* @hide
*/
public static String delayTypeToString(@DelayType int type) {
return switch (type) {
case DELAY_TYPE_PAUSE -> "PAUSE";
case DELAY_TYPE_RELATIVE_START_OFFSET -> "START_OFFSET";
default -> Integer.toString(type);
};
}
}
/**
* A builder for waveform effects described by its envelope.
*
* Waveform effect envelopes are defined by one or more control points describing a target
* vibration amplitude and frequency, and a duration to reach those targets. The vibrator
* will perform smooth transitions between control points.
*
* For example, the following code ramps a vibrator from off to full amplitude at 120Hz over
* 100ms, holds that state for 200ms, and then ramps back down over 100ms:
*
* The builder automatically starts all effects at 0 amplitude.
*
* It is crucial to ensure that the frequency range used in your effect is compatible with
* the device's capabilities. The framework will not play any frequencies that fall partially
* or completely outside the device's supported range. It will also not attempt to correct or
* modify these frequencies.
*
* Therefore, it is strongly recommended that you design your haptic effects with the
* device's frequency profile in mind. You can obtain the supported frequency range and other
* relevant frequency-related information by getting the
* {@link android.os.vibrator.VibratorFrequencyProfile} using the
* {@link Vibrator#getFrequencyProfile()} method.
*
* In addition to these limitations, when designing vibration patterns, it is important to
* consider the physical limitations of the vibration actuator. These limitations include
* factors such as the maximum number of control points allowed in an envelope effect, the
* minimum and maximum durations permitted for each control point, and the maximum overall
* duration of the effect. If a pattern exceeds the maximum number of allowed control points,
* the framework will automatically break down the effect to ensure it plays correctly.
*
* You can use the following APIs to obtain these limits:
* The effect will start vibrating at this frequency when it transitions to the
* amplitude and frequency defined by the first control point.
*
* The frequency must be greater than zero and within the supported range. To determine
* the supported range, use {@link Vibrator#getFrequencyProfile()}. Creating
* effects using frequencies outside this range will result in the vibration not playing.
*
* @param initialFrequencyHz The starting frequency of the vibration, in Hz. Must be
* greater than zero.
*/
@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
@SuppressWarnings("MissingGetterMatchingBuilder")// No getter to initial frequency once set.
@NonNull
public WaveformEnvelopeBuilder setInitialFrequencyHz(
@FloatRange(from = 0) float initialFrequencyHz) {
if (mSegments.isEmpty()) {
mLastFrequencyHz = initialFrequencyHz;
} else {
PwleSegment firstSegment = mSegments.getFirst();
mSegments.set(0, new PwleSegment(
firstSegment.getStartAmplitude(),
firstSegment.getEndAmplitude(),
initialFrequencyHz, // Update start frequency
firstSegment.getEndFrequencyHz(),
firstSegment.getDuration()));
}
return this;
}
/**
* Adds a new control point to the end of this waveform envelope.
*
* Amplitude defines the vibrator's strength at this frequency, ranging from 0 (off) to 1
* (maximum achievable strength). This value scales linearly with output strength, not
* perceived intensity. It's determined by the actuator response curve.
*
* Frequency must be greater than zero and within the supported range. To determine
* the supported range, use {@link Vibrator#getFrequencyProfile()}. Creating
* effects using frequencies outside this range will result in the vibration not playing.
*
* Time specifies the duration (in milliseconds) for the vibrator to smoothly transition
* from the previous control point to this new one. It must be greater than zero. To
* transition as quickly as possible, use
* {@link VibratorEnvelopeEffectInfo#getMinControlPointDurationMillis()}.
*
* @param amplitude The amplitude value between 0 and 1, inclusive. 0 represents the
* vibrator being off, and 1 represents the maximum achievable
* amplitude
* at this frequency.
* @param frequencyHz The frequency in Hz, must be greater than zero.
* @param durationMillis The transition time in milliseconds.
*/
@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
@SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created.
@NonNull
public WaveformEnvelopeBuilder addControlPoint(
@FloatRange(from = 0, to = 1) float amplitude,
@FloatRange(from = 0) float frequencyHz, @DurationMillisLong long durationMillis) {
if (Float.isNaN(mLastFrequencyHz)) {
mLastFrequencyHz = frequencyHz;
}
mSegments.add(new PwleSegment(mLastAmplitude, amplitude, mLastFrequencyHz, frequencyHz,
durationMillis));
mLastAmplitude = amplitude;
mLastFrequencyHz = frequencyHz;
return this;
}
/**
* Build the waveform as a single {@link VibrationEffect}.
*
* The {@link WaveformEnvelopeBuilder} 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 list of control points.
* @throws IllegalStateException if no control points were added to the builder.
*/
@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
@NonNull
public VibrationEffect build() {
if (mSegments.isEmpty()) {
throw new IllegalStateException(
"WaveformEnvelopeBuilder must have at least one control point to build.");
}
VibrationEffect effect = new Composed(mSegments, /* repeatIndex= */ -1);
effect.validate();
return effect;
}
}
/**
* A builder for waveform effects defined by their envelope, designed to provide a consistent
* haptic perception across devices with varying capabilities.
*
* This builder simplifies the creation of waveform effects by automatically adapting them
* to different devices based on their capabilities. Effects are defined by control points
* specifying target vibration intensity and sharpness, along with durations to reach those
* targets. The vibrator will smoothly transition between these control points.
*
* Intensity: Defines the overall strength of the vibration, ranging from
* 0 (off) to 1 (maximum achievable strength). Higher values result in stronger
* vibrations. Supported intensity values guarantee sensitivity levels (SL) above
* 10 dB SL to ensure human perception.
*
* Sharpness: Defines the crispness of the vibration, ranging from 0 to 1.
* Lower values produce smoother vibrations, while higher values create a sharper,
* more snappy sensation. Sharpness is mapped to its equivalent frequency within
* the device's supported frequency range.
*
* While this builder handles most of the adaptation logic, it does come with some
* limitations:
* The builder automatically starts all effects at 0 intensity.
*
* To avoid these limitations and to have more control over the effects output, use
* {@link WaveformEnvelopeBuilder}, where direct amplitude and frequency values can be used.
*
* For optimal cross-device consistency, it's recommended to limit the number of control
* points to a maximum of 16. However this is not mandatory, and if a pattern exceeds the
* maximum number of allowed control points, the framework will automatically break down the
* effect to ensure it plays correctly.
*
* For example, the following code creates a vibration effect that ramps up the intensity
* from a low-pitched to a high-pitched strong vibration over 500ms and then ramps it down to
* 0 (off) over 100ms:
*
* The effect will start vibrating at this sharpness when it transitions to the
* intensity and sharpness defined by the first control point.
*
* The sharpness defines the crispness of the vibration, ranging from 0 to 1. Lower
* values translate to smoother vibrations, while higher values create a sharper more snappy
* sensation. This value is mapped to the supported frequency range of the device.
*
* @param initialSharpness The starting sharpness of the vibration in the range of [0, 1].
*/
@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
@SuppressWarnings("MissingGetterMatchingBuilder")// No getter to initial sharpness once set.
@NonNull
public BasicEnvelopeBuilder setInitialSharpness(
@FloatRange(from = 0, to = 1) float initialSharpness) {
if (mSegments.isEmpty()) {
mLastSharpness = initialSharpness;
} else {
BasicPwleSegment firstSegment = mSegments.getFirst();
mSegments.set(0, new BasicPwleSegment(
firstSegment.getStartIntensity(),
firstSegment.getEndIntensity(),
initialSharpness, // Update start sharpness
firstSegment.getEndSharpness(),
firstSegment.getDuration()));
}
return this;
}
/**
* Adds a new control point to the end of this waveform envelope.
*
* Intensity defines the overall strength of the vibration, ranging from 0 (off) to 1
* (maximum achievable strength). Higher values translate to stronger vibrations.
*
* Sharpness defines the crispness of the vibration, ranging from 0 to 1. Lower
* values translate to smoother vibrations, while higher values create a sharper more snappy
* sensation. This value is mapped to the supported frequency range of the device.
*
* Time specifies the duration (in milliseconds) for the vibrator to smoothly transition
* from the previous control point to this new one. It must be greater than zero. To
* transition as quickly as possible, use
* {@link VibratorEnvelopeEffectInfo#getMinControlPointDurationMillis()}.
*
* @param intensity The target vibration intensity, ranging from 0 (off) to 1 (maximum
* strength).
* @param sharpness The target sharpness, ranging from 0 (smoothest) to 1 (sharpest).
* @param durationMillis The transition time in milliseconds.
*/
@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
@SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created.
@NonNull
public BasicEnvelopeBuilder addControlPoint(
@FloatRange(from = 0, to = 1) float intensity,
@FloatRange(from = 0, to = 1) float sharpness,
@DurationMillisLong long durationMillis) {
if (Float.isNaN(mLastSharpness)) {
mLastSharpness = sharpness;
}
mSegments.add(new BasicPwleSegment(mLastIntensity, intensity, mLastSharpness, sharpness,
durationMillis));
mLastIntensity = intensity;
mLastSharpness = sharpness;
return this;
}
/**
* Build the waveform as a single {@link VibrationEffect}.
*
* The {@link BasicEnvelopeBuilder} 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 list of control points.
* @throws IllegalStateException if the last control point does not end at zero intensity.
*/
@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
@NonNull
public VibrationEffect build() {
if (mSegments.isEmpty()) {
throw new IllegalStateException(
"BasicEnvelopeBuilder must have at least one control point to build.");
}
if (mSegments.getLast().getEndIntensity() != 0) {
throw new IllegalStateException(
"Basic envelope effects must end at a zero intensity control point.");
}
VibrationEffect effect = new Composed(mSegments, /* repeatIndex= */ -1);
effect.validate();
return effect;
}
}
/**
* A builder for waveform haptic effects.
*
* Waveform vibrations constitute of one or more timed transitions to new sets of vibration
* parameters. These parameters can be the vibration amplitude, frequency, or both.
*
* The following example ramps a vibrator turned off to full amplitude at 120Hz, over 100ms
* starting at 60Hz, then holds that state for 200ms and ramps back down again over 100ms:
*
* The initial state of the waveform can be set via
* {@link VibrationEffect#startWaveform(VibrationParameter)} or
* {@link VibrationEffect#startWaveform(VibrationParameter, VibrationParameter)}. If the initial
* parameters are not set then the {@link WaveformBuilder} will start with the vibrator off,
* represented by zero amplitude, at the vibrator's resonant frequency.
*
* Repeating waveforms can be created by building the repeating block separately and adding
* it to the end of a composition with
* {@link Composition#repeatEffectIndefinitely(VibrationEffect)}:
*
* Note that physical vibration actuators have different reaction times for changing
* amplitude and frequency. Durations specified here represent a timeline for the target
* parameters, and quality of effects may be improved if the durations allow time for a
* transition to be smoothly applied.
*
* The following example illustrates both an initial state and a repeating section, using
* a {@link VibrationEffect.Composition}. The resulting effect will have a tick followed by a
* repeated beating effect with a rise that stretches out and a sharp finish.
*
* The amplitude step waveforms that can be created via
* {@link VibrationEffect#createWaveform(long[], int[], int)} can also be created with
* {@link WaveformBuilder} by adding zero duration transitions:
*
* The duration represents how long the vibrator should take to smoothly transition to
* the new vibration parameter. If the duration is zero then the vibrator will jump to the
* new value as fast as possible.
*
* Vibration parameter values will be truncated to conform to the device capabilities
* according to the {@link VibratorFrequencyProfileLegacy}.
*
* @param duration The length of time this transition should take. Value must be
* non-negative and will be truncated to milliseconds.
* @param targetParameter The new target {@link VibrationParameter} value to be reached
* after the given duration.
* @return This {@link WaveformBuilder} object to enable adding multiple transitions in
* chain.
* @hide
*/
@TestApi
@SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created.
@NonNull
public WaveformBuilder addTransition(@NonNull Duration duration,
@NonNull VibrationParameter targetParameter) {
Preconditions.checkNotNull(duration, "Duration is null");
checkVibrationParameter(targetParameter, "targetParameter");
float amplitude = extractTargetAmplitude(targetParameter, /* target2= */ null);
float frequencyHz = extractTargetFrequency(targetParameter, /* target2= */ null);
addTransitionSegment(duration, amplitude, frequencyHz);
return this;
}
/**
* Add a transition to new vibration parameters to the end of this waveform.
*
* The duration represents how long the vibrator should take to smoothly transition to
* the new vibration parameters. If the duration is zero then the vibrator will jump to the
* new values as fast as possible.
*
* Vibration parameters values will be truncated to conform to the device capabilities
* according to the {@link VibratorFrequencyProfileLegacy}.
*
* @param duration The length of time this transition should take. Value must be
* non-negative and will be truncated to milliseconds.
* @param targetParameter1 The first target {@link VibrationParameter} value to be reached
* after the given duration.
* @param targetParameter2 The second target {@link VibrationParameter} value to be reached
* after the given duration, must be a different type of parameter
* than the one specified by the first argument.
* @return This {@link WaveformBuilder} object to enable adding multiple transitions in
* chain.
* @hide
*/
@TestApi
@SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created.
@NonNull
public WaveformBuilder addTransition(@NonNull Duration duration,
@NonNull VibrationParameter targetParameter1,
@NonNull VibrationParameter targetParameter2) {
Preconditions.checkNotNull(duration, "Duration is null");
checkVibrationParameter(targetParameter1, "targetParameter1");
checkVibrationParameter(targetParameter2, "targetParameter2");
Preconditions.checkArgument(
!Objects.equals(targetParameter1.getClass(), targetParameter2.getClass()),
"Parameter arguments must specify different parameter types");
float amplitude = extractTargetAmplitude(targetParameter1, targetParameter2);
float frequencyHz = extractTargetFrequency(targetParameter1, targetParameter2);
addTransitionSegment(duration, amplitude, frequencyHz);
return this;
}
/**
* Add a duration to sustain the last vibration parameters of this waveform.
*
* The duration represents how long the vibrator should sustain the last set of
* parameters provided to this builder.
*
* @param duration The length of time the last values should be sustained by the vibrator.
* Value must be >= 1ms.
* @return This {@link WaveformBuilder} object to enable adding multiple transitions in
* chain.
* @hide
*/
@TestApi
@SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created.
@NonNull
public WaveformBuilder addSustain(@NonNull Duration duration) {
int durationMs = (int) duration.toMillis();
Preconditions.checkArgument(durationMs >= 1, "Sustain duration must be >= 1ms");
mSegments.add(new StepSegment(mLastAmplitude, mLastFrequencyHz, durationMs));
return this;
}
/**
* Build the waveform as 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 list of transitions.
* @hide
*/
@TestApi
@NonNull
public VibrationEffect build() {
if (mSegments.isEmpty()) {
throw new IllegalStateException(
"WaveformBuilder must have at least one transition to build.");
}
VibrationEffect effect = new Composed(mSegments, /* repeatIndex= */ -1);
effect.validate();
return effect;
}
private void checkVibrationParameter(@NonNull VibrationParameter vibrationParameter,
String paramName) {
Preconditions.checkNotNull(vibrationParameter, "%s is null", paramName);
Preconditions.checkArgument(
(vibrationParameter instanceof AmplitudeVibrationParameter)
|| (vibrationParameter instanceof FrequencyVibrationParameter),
"%s is a unknown parameter", paramName);
}
private float extractTargetAmplitude(@Nullable VibrationParameter target1,
@Nullable VibrationParameter target2) {
if (target2 instanceof AmplitudeVibrationParameter) {
return ((AmplitudeVibrationParameter) target2).amplitude;
}
if (target1 instanceof AmplitudeVibrationParameter) {
return ((AmplitudeVibrationParameter) target1).amplitude;
}
return mLastAmplitude;
}
private float extractTargetFrequency(@Nullable VibrationParameter target1,
@Nullable VibrationParameter target2) {
if (target2 instanceof FrequencyVibrationParameter) {
return ((FrequencyVibrationParameter) target2).frequencyHz;
}
if (target1 instanceof FrequencyVibrationParameter) {
return ((FrequencyVibrationParameter) target1).frequencyHz;
}
return mLastFrequencyHz;
}
private void addTransitionSegment(Duration duration, float targetAmplitude,
float targetFrequency) {
Preconditions.checkNotNull(duration, "Duration is null");
Preconditions.checkArgument(!duration.isNegative(),
"Transition duration must be non-negative");
int durationMs = (int) duration.toMillis();
// Ignore transitions with zero duration, but keep values for next additions.
if (durationMs > 0) {
if ((Math.abs(mLastAmplitude - targetAmplitude) < EPSILON)
&& (Math.abs(mLastFrequencyHz - targetFrequency) < EPSILON)) {
// No value is changing, this can be best represented by a step segment.
mSegments.add(new StepSegment(targetAmplitude, targetFrequency, durationMs));
} else {
mSegments.add(new RampSegment(mLastAmplitude, targetAmplitude,
mLastFrequencyHz, targetFrequency, durationMs));
}
}
mLastAmplitude = targetAmplitude;
mLastFrequencyHz = targetFrequency;
}
}
/**
* A representation of a single vibration parameter.
*
* This is to describe a waveform haptic effect, which consists of one or more timed
* transitions to a new set of {@link VibrationParameter}s.
*
* Examples of concrete parameters are the vibration amplitude or frequency.
*
* @see VibrationEffect.WaveformBuilder
* @hide
*/
@TestApi
@SuppressWarnings("UserHandleName") // This is not a regular set of parameters, no *Params.
public static class VibrationParameter {
VibrationParameter() {
}
/**
* The target vibration amplitude.
*
* @param amplitude The amplitude value, between 0 and 1, inclusive, where 0 represents the
* vibrator turned off and 1 represents the maximum amplitude the vibrator
* can reach across all supported frequencies.
* @return The {@link VibrationParameter} instance that represents given amplitude.
* @hide
*/
@TestApi
@NonNull
public static VibrationParameter targetAmplitude(
@FloatRange(from = 0, to = 1) float amplitude) {
return new AmplitudeVibrationParameter(amplitude);
}
/**
* The target vibration frequency.
*
* @param frequencyHz The frequency value, in hertz.
* @return The {@link VibrationParameter} instance that represents given frequency.
* @hide
*/
@TestApi
@NonNull
public static VibrationParameter targetFrequency(@FloatRange(from = 1) float frequencyHz) {
return new FrequencyVibrationParameter(frequencyHz);
}
}
/** The vibration amplitude, represented by a value in [0,1]. */
private static final class AmplitudeVibrationParameter extends VibrationParameter {
public final float amplitude;
AmplitudeVibrationParameter(float amplitude) {
Preconditions.checkArgument((amplitude >= 0) && (amplitude <= 1),
"Amplitude must be within [0,1]");
this.amplitude = amplitude;
}
}
/** The vibration frequency, in hertz, or zero to represent undefined frequency. */
private static final class FrequencyVibrationParameter extends VibrationParameter {
public final float frequencyHz;
FrequencyVibrationParameter(float frequencyHz) {
Preconditions.checkArgument(frequencyHz >= 1, "Frequency must be >= 1");
Preconditions.checkArgument(Float.isFinite(frequencyHz), "Frequency must be finite");
this.frequencyHz = frequencyHz;
}
}
@NonNull
public static final Parcelable.Creator
* {@code VibrationEffect effect = VibrationEffect.startComposition()
* .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE, 0.5f)
* .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, 0.5f)
* .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1.0f, 100)
* .compose();}
*
*
* VibrationEffect popEffect = VibrationEffect.startComposition()
* .addPrimitive(PRIMITIVE_QUICK_RISE)
* .addPrimitive(PRIMITIVE_CLICK, 0.7, 50, DELAY_TYPE_PAUSE)
* .compose()
*
*/
@FlaggedApi(Flags.FLAG_PRIMITIVE_COMPOSITION_ABSOLUTE_DELAY)
public static final int DELAY_TYPE_PAUSE = 0;
/**
* The delay represents an offset before starting this primitive, relative to the start
* time of the previous primitive in the composition.
*
*
* VibrationEffect.startComposition()
* .addPrimitive(PRIMITIVE_CLICK, 1.0)
* .addPrimitive(PRIMITIVE_TICK, 1.0, 20, DELAY_TYPE_RELATIVE_START_OFFSET)
* .addPrimitive(PRIMITIVE_THUD, 1.0, 80, DELAY_TYPE_RELATIVE_START_OFFSET)
* .compose()
*
*
* Will be performed on the device as follows:
*
*
* 0ms 20ms 100ms
* PRIMITIVE_CLICK---PRIMITIVE_TICK-----------PRIMITIVE_THUD
*
*
* {@code
* VibrationEffect effect = new VibrationEffect.WaveformEnvelopeBuilder()
* .addControlPoint(1.0f, 120f, 100)
* .addControlPoint(1.0f, 120f, 200)
* .addControlPoint(0.0f, 120f, 100)
* .build();
* }
*
*
*
*/
@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
public static final class WaveformEnvelopeBuilder {
private ArrayList
*
*
* {@code
* VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
* .setInitialSharpness(0.0f)
* .addControlPoint(1.0f, 1.0f, 500)
* .addControlPoint(0.0f, 1.0f, 100)
* .build();
* }
*/
@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
public static final class BasicEnvelopeBuilder {
private ArrayList
* {@code import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
* import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
*
* VibrationEffect effect = VibrationEffect.startWaveform(targetFrequency(60))
* .addTransition(Duration.ofMillis(100), targetAmplitude(1), targetFrequency(120))
* .addSustain(Duration.ofMillis(200))
* .addTransition(Duration.ofMillis(100), targetAmplitude(0), targetFrequency(60))
* .build();}
*
*
* {@code VibrationEffect patternToRepeat = VibrationEffect.startWaveform(targetAmplitude(0.2f))
* .addSustain(Duration.ofMillis(10))
* .addTransition(Duration.ofMillis(20), targetAmplitude(0.4f))
* .addSustain(Duration.ofMillis(30))
* .addTransition(Duration.ofMillis(40), targetAmplitude(0.8f))
* .addSustain(Duration.ofMillis(50))
* .addTransition(Duration.ofMillis(60), targetAmplitude(0.2f))
* .build();
*
* VibrationEffect effect = VibrationEffect.startComposition()
* .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK)
* .addOffDuration(Duration.ofMillis(20))
* .repeatEffectIndefinitely(patternToRepeat)
* .compose();}
*
*
* {@code // These two effects are the same
* VibrationEffect waveform = VibrationEffect.createWaveform(
* new long[] { 10, 20, 30 }, // timings in milliseconds
* new int[] { 51, 102, 204 }, // amplitudes in [0,255]
* -1); // repeat index
*
* VibrationEffect sameWaveform = VibrationEffect.startWaveform(targetAmplitude(0.2f))
* .addSustain(Duration.ofMillis(10))
* .addTransition(Duration.ZERO, targetAmplitude(0.4f))
* .addSustain(Duration.ofMillis(20))
* .addTransition(Duration.ZERO, targetAmplitude(0.8f))
* .addSustain(Duration.ofMillis(30))
* .build();}
*
* @see VibrationEffect#startWaveform
* @hide
*/
@TestApi
public static final class WaveformBuilder {
// Epsilon used for float comparison of amplitude and frequency values on transitions.
private static final float EPSILON = 1e-5f;
private ArrayList