/* * 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 segments = new ArrayList<>(); for (int i = 0; i < timings.length; i++) { float parsedAmplitude = amplitudes[i] == DEFAULT_AMPLITUDE ? DEFAULT_AMPLITUDE : (float) amplitudes[i] / MAX_AMPLITUDE; segments.add(new StepSegment(parsedAmplitude, /* frequencyHz= */ 0, (int) timings[i])); } VibrationEffect effect = new Composed(segments, repeat); effect.validate(); return effect; } /** * Create 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. */ @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 The type of parameter to be used on the effect by this transformation * @hide */ public interface Transformation { /** Transforms given effect by applying the given parameter. */ @NonNull VibrationEffect transform(@NonNull VibrationEffect effect, @NonNull ParamT param); } /** * Implementation of {@link VibrationEffect} described by a composition of one or more * {@link VibrationEffectSegment}, with an optional index to represent repeating effects. * * @hide */ @TestApi public static final class Composed extends VibrationEffect { private final ArrayList mSegments; private final int mRepeatIndex; /** @hide */ Composed(@NonNull Parcel in) { this(Objects.requireNonNull(in.readArrayList( VibrationEffectSegment.class.getClassLoader(), VibrationEffectSegment.class)), in.readInt()); } /** @hide */ Composed(@NonNull VibrationEffectSegment segment) { this(Arrays.asList(segment), /* repeatIndex= */ -1); } /** @hide */ public Composed(@NonNull List segments, int repeatIndex) { super(); mSegments = new ArrayList<>(segments); mRepeatIndex = repeatIndex; } @NonNull public List getSegments() { return mSegments; } public int getRepeatIndex() { return mRepeatIndex; } /** @hide */ @Override @Nullable public long[] computeCreateWaveformOffOnTimingsOrNull() { if (getRepeatIndex() >= 0) { // Repeating effects cannot be fully represented as a long[] legacy pattern. return null; } List segments = getSegments(); // The maximum possible size of the final pattern is 1 plus the number of segments in // the original effect. This is because we will add an empty "off" segment at the // start of the pattern if the first segment of the original effect is an "on" segment. // (because the legacy patterns start with an "off" pattern). Other than this one case, // we will add the durations of back-to-back segments of similar amplitudes (amplitudes // that are all "on" or "off") and create a pattern entry for the total duration, which // will not take more number pattern entries than the number of segments processed. long[] patternBuffer = new long[segments.size() + 1]; int patternIndex = 0; for (int i = 0; i < segments.size(); i++) { StepSegment stepSegment = castToValidStepSegmentForOffOnTimingsOrNull(segments.get(i)); if (stepSegment == null) { // This means that there is 1 or more segments of this effect that is/are not a // possible component of a legacy vibration pattern. Thus, the VibrationEffect // does not have any equivalent legacy vibration pattern. return null; } boolean isSegmentOff = stepSegment.getAmplitude() == 0; // Even pattern indices are "off", and odd pattern indices are "on" boolean isCurrentPatternIndexOff = (patternIndex % 2) == 0; if (isSegmentOff != isCurrentPatternIndexOff) { // Move the pattern index one step ahead, so that the current segment's // "off"/"on" property matches that of the index's ++patternIndex; } patternBuffer[patternIndex] += stepSegment.getDuration(); } return Arrays.copyOf(patternBuffer, patternIndex + 1); } /** @hide */ @Override public void validate() { int segmentCount = mSegments.size(); boolean hasNonZeroDuration = false; for (int i = 0; i < segmentCount; i++) { VibrationEffectSegment segment = mSegments.get(i); segment.validate(); // A segment with unknown duration = -1 still counts as a non-zero duration. hasNonZeroDuration |= segment.getDuration() != 0; } if (!hasNonZeroDuration) { throw new IllegalArgumentException("at least one timing must be non-zero" + " (segments=" + mSegments + ")"); } if (mRepeatIndex != -1) { Preconditions.checkArgumentInRange(mRepeatIndex, 0, segmentCount - 1, "repeat index must be within the bounds of the segments (segments.length=" + segmentCount + ", index=" + mRepeatIndex + ")"); } } /** @hide */ @Override @Nullable public VibrationEffect cropToLengthOrNull(int length) { // drop repeating effects if (mRepeatIndex >= 0) { return null; } int segmentCount = mSegments.size(); if (segmentCount <= length) { return this; } ArrayList truncated = new ArrayList(mSegments.subList(0, length)); Composed updated = new Composed(truncated, mRepeatIndex); try { updated.validate(); } catch (IllegalArgumentException e) { return null; } return updated; } @Override public long getDuration() { return getDuration(VibrationEffectSegment::getDuration); } /** @hide */ @Override public long getDuration(@Nullable VibratorInfo vibratorInfo) { return getDuration(segment -> segment.getDuration(vibratorInfo)); } private long getDuration(Function durationFn) { if (mRepeatIndex >= 0) { return Long.MAX_VALUE; } int segmentCount = mSegments.size(); long totalDuration = 0; for (int i = 0; i < segmentCount; i++) { long segmentDuration = durationFn.apply(mSegments.get(i)); if (segmentDuration < 0) { return segmentDuration; } totalDuration += segmentDuration; } return totalDuration; } /** @hide */ @Override public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) { for (VibrationEffectSegment segment : mSegments) { if (!segment.areVibrationFeaturesSupported(vibratorInfo)) { return false; } } return true; } /** @hide */ @Override public boolean isHapticFeedbackCandidate() { long totalDuration = getDuration(); if (totalDuration > MAX_HAPTIC_FEEDBACK_DURATION) { // Vibration duration is known and is longer than the max duration used to classify // haptic feedbacks (or repeating indefinitely with duration == Long.MAX_VALUE). return false; } int segmentCount = mSegments.size(); if (segmentCount > MAX_HAPTIC_FEEDBACK_COMPOSITION_SIZE) { // Vibration has some predefined or primitive constants, it should be limited to the // max composition size used to classify haptic feedbacks. return false; } totalDuration = 0; for (int i = 0; i < segmentCount; i++) { if (!mSegments.get(i).isHapticFeedbackCandidate()) { // There is at least one segment that is not a candidate for a haptic feedback. return false; } long segmentDuration = mSegments.get(i).getDuration(); if (segmentDuration > 0) { totalDuration += segmentDuration; } } // Vibration might still have some ramp or step segments, check the known duration. return totalDuration <= MAX_HAPTIC_FEEDBACK_DURATION; } /** @hide */ @NonNull @Override public Composed resolve(int defaultAmplitude) { return applyToSegments(VibrationEffectSegment::resolve, defaultAmplitude); } /** @hide */ @NonNull @Override public VibrationEffect applyEffectStrength(int effectStrength) { return applyToSegments(VibrationEffectSegment::applyEffectStrength, effectStrength); } /** @hide */ @NonNull @Override public Composed scale(float scaleFactor) { return applyToSegments(VibrationEffectSegment::scale, scaleFactor); } /** @hide */ @NonNull @Override public Composed applyAdaptiveScale(float scaleFactor) { return applyToSegments(VibrationEffectSegment::scaleLinearly, scaleFactor); } /** @hide */ @NonNull @Override public Composed applyRepeatingIndefinitely(boolean wantRepeating, int loopDelayMs) { boolean isRepeating = mRepeatIndex >= 0; if (isRepeating == wantRepeating) { return this; } else if (!wantRepeating) { return new Composed(mSegments, -1); } else if (loopDelayMs <= 0) { // Loop with no delay: repeat at index zero. return new Composed(mSegments, 0); } else { // Append a delay and loop. It doesn't matter that there's a delay on the // end because the looping is always indefinite until cancelled. ArrayList loopingSegments = new ArrayList<>(mSegments.size() + 1); loopingSegments.addAll(mSegments); loopingSegments.add( new StepSegment(/* amplitude= */ 0, /* frequencyHz= */ 0, loopDelayMs)); return new Composed(loopingSegments, 0); } } @Override public boolean equals(@Nullable Object o) { if (this == o) { return true; } if (!(o instanceof Composed other)) { return false; } return mSegments.equals(other.mSegments) && mRepeatIndex == other.mRepeatIndex; } @Override public int hashCode() { return Objects.hash(mSegments, mRepeatIndex); } @Override public String toString() { return "Composed{segments=" + mSegments + ", repeat=" + mRepeatIndex + "}"; } /** @hide */ @Override public String toDebugString() { if (mSegments.size() == 1 && mRepeatIndex < 0) { // Simplify effect string, use the single segment to represent it. return mSegments.get(0).toDebugString(); } StringJoiner sj = new StringJoiner(",", "[", "]"); for (int i = 0; i < mSegments.size(); i++) { sj.add(mSegments.get(i).toDebugString()); } if (mRepeatIndex >= 0) { return String.format(Locale.ROOT, "%s, repeat=%d", sj, mRepeatIndex); } return sj.toString(); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel out, int flags) { out.writeInt(PARCEL_TOKEN_COMPOSED); out.writeList(mSegments); out.writeInt(mRepeatIndex); } @NonNull public static final Creator CREATOR = new Creator() { @Override public Composed createFromParcel(Parcel in) { in.readInt(); // Skip the parcel type token return new Composed(in); } @Override public Composed[] newArray(int size) { return new Composed[size]; } }; /** * Casts a provided {@link VibrationEffectSegment} to a {@link StepSegment} and returns it, * only if it can possibly be a segment for an effect created via * {@link #createWaveform(long[], int)}. Otherwise, returns {@code null}. */ @Nullable private static StepSegment castToValidStepSegmentForOffOnTimingsOrNull( VibrationEffectSegment segment) { if (!(segment instanceof StepSegment)) { return null; } StepSegment stepSegment = (StepSegment) segment; if (stepSegment.getFrequencyHz() != 0) { return null; } float amplitude = stepSegment.getAmplitude(); if (amplitude != 0 && amplitude != DEFAULT_AMPLITUDE) { return null; } return stepSegment; } private Composed applyToSegments( BiFunction function, T param) { int segmentCount = mSegments.size(); ArrayList updatedSegments = new ArrayList<>(segmentCount); for (int i = 0; i < segmentCount; i++) { updatedSegments.add(function.apply(mSegments.get(i), param)); } if (mSegments.equals(updatedSegments)) { return this; } Composed updated = new Composed(updatedSegments, mRepeatIndex); updated.validate(); return updated; } } /** * Implementation of {@link VibrationEffect} described by a generic {@link PersistableBundle} * defined by vendors. * * @hide */ @TestApi @FlaggedApi(FLAG_VENDOR_VIBRATION_EFFECTS) public static final class VendorEffect extends VibrationEffect { /** @hide */ public static final int DEFAULT_STRENGTH = VibrationEffect.EFFECT_STRENGTH_MEDIUM; /** @hide */ public static final float DEFAULT_SCALE = 1.0f; private final PersistableBundle mVendorData; private final int mEffectStrength; private final float mScale; private final float mAdaptiveScale; /** @hide */ VendorEffect(@NonNull Parcel in) { this(Objects.requireNonNull( in.readPersistableBundle(VibrationEffect.class.getClassLoader())), in.readInt(), in.readFloat(), in.readFloat()); } /** @hide */ public VendorEffect(@NonNull PersistableBundle vendorData, int effectStrength, float scale, float adaptiveScale) { mVendorData = vendorData; mEffectStrength = effectStrength; mScale = scale; mAdaptiveScale = adaptiveScale; } @NonNull public PersistableBundle getVendorData() { return mVendorData; } public int getEffectStrength() { return mEffectStrength; } public float getScale() { return mScale; } public float getAdaptiveScale() { return mAdaptiveScale; } /** @hide */ @Override @Nullable public long[] computeCreateWaveformOffOnTimingsOrNull() { return null; } /** @hide */ @Override public void validate() { Preconditions.checkArgument(!mVendorData.isEmpty(), "Vendor effect bundle must be non-empty"); } /** @hide */ @Override @Nullable public VibrationEffect cropToLengthOrNull(int length) { return null; } @Override public long getDuration() { return -1; // UNKNOWN } /** @hide */ @Override public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) { return vibratorInfo.hasCapability(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); } /** @hide */ @Override public boolean isHapticFeedbackCandidate() { return false; } /** @hide */ @NonNull @Override public VendorEffect resolve(int defaultAmplitude) { return this; } /** @hide */ @NonNull @Override public VibrationEffect applyEffectStrength(int effectStrength) { if (mEffectStrength == effectStrength) { return this; } VendorEffect updated = new VendorEffect(mVendorData, effectStrength, mScale, mAdaptiveScale); updated.validate(); return updated; } /** @hide */ @NonNull @Override public VendorEffect scale(float scaleFactor) { if (Float.compare(mScale, scaleFactor) == 0) { return this; } VendorEffect updated = new VendorEffect(mVendorData, mEffectStrength, scaleFactor, mAdaptiveScale); updated.validate(); return updated; } /** @hide */ @NonNull @Override public VibrationEffect applyAdaptiveScale(float scaleFactor) { if (Float.compare(mAdaptiveScale, scaleFactor) == 0) { return this; } VendorEffect updated = new VendorEffect(mVendorData, mEffectStrength, mScale, scaleFactor); updated.validate(); return updated; } /** @hide */ @NonNull @Override public VendorEffect applyRepeatingIndefinitely(boolean wantRepeating, int loopDelayMs) { return this; } @Override public boolean equals(@Nullable Object o) { if (this == o) { return true; } if (!(o instanceof VendorEffect other)) { return false; } return mEffectStrength == other.mEffectStrength && (Float.compare(mScale, other.mScale) == 0) && (Float.compare(mAdaptiveScale, other.mAdaptiveScale) == 0) && isPersistableBundleEquals(mVendorData, other.mVendorData); } @Override public int hashCode() { // PersistableBundle does not implement hashCode, so use its size as a shortcut. return Objects.hash(mVendorData.size(), mEffectStrength, mScale, mAdaptiveScale); } @Override public String toString() { return String.format(Locale.ROOT, "VendorEffect{vendorData=%s, strength=%s, scale=%.2f, adaptiveScale=%.2f}", mVendorData, effectStrengthToString(mEffectStrength), mScale, mAdaptiveScale); } /** @hide */ @Override public String toDebugString() { return String.format(Locale.ROOT, "vendorEffect=%s, strength=%s, scale=%.2f, adaptiveScale=%.2f", mVendorData.toShortString(), effectStrengthToString(mEffectStrength), mScale, mAdaptiveScale); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel out, int flags) { out.writeInt(PARCEL_TOKEN_VENDOR_EFFECT); out.writePersistableBundle(mVendorData); out.writeInt(mEffectStrength); out.writeFloat(mScale); out.writeFloat(mAdaptiveScale); } /** * Compares two {@link PersistableBundle} objects are equals. */ private static boolean isPersistableBundleEquals( PersistableBundle first, PersistableBundle second) { if (first == second) { return true; } if (first == null || second == null || first.size() != second.size()) { return false; } for (String key : first.keySet()) { if (!isPersistableBundleSupportedValueEquals(first.get(key), second.get(key))) { return false; } } return true; } /** * Compares two values which type is supported by {@link PersistableBundle}. * *

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 CREATOR = new Creator() { @Override public VendorEffect createFromParcel(Parcel in) { in.readInt(); // Skip the parcel type token return new VendorEffect(in); } @Override public VendorEffect[] newArray(int size) { return new VendorEffect[size]; } }; } /** * Creates a new {@link VibrationEffect} that repeats the given effect indefinitely. * *

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: * *

     * {@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();}
* *

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: * *

         *     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. * *

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: * *

         *     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
         * 
* *

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 mSegments = new ArrayList<>(); private int mRepeatIndex = -1; Composition() {} /** * Adds a time duration to the current composition, during which the vibrator will be * turned off. * * @param duration The length of time the vibrator should be off. Value must be non-negative * and will be truncated to milliseconds. * @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 addOffDuration(@NonNull Duration duration) { int durationMs = (int) duration.toMillis(); Preconditions.checkArgumentNonnegative(durationMs, "Off period must be non-negative"); if (durationMs > 0) { // Created a segment sustaining the zero amplitude to represent the delay. addSegment(new StepSegment(/* amplitude= */ 0, /* frequencyHz= */ 0, (int) duration.toMillis())); } return this; } /** * Add a haptic effect to the end of the current composition. * *

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: * *

{@code
     * VibrationEffect effect = new VibrationEffect.WaveformEnvelopeBuilder()
     *     .addControlPoint(1.0f, 120f, 100)
     *     .addControlPoint(1.0f, 120f, 200)
     *     .addControlPoint(0.0f, 120f, 100)
     *     .build();
     * }
* *

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: *

*/ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public static final class WaveformEnvelopeBuilder { private ArrayList mSegments = new ArrayList<>(); private float mLastAmplitude = 0f; private float mLastFrequencyHz = Float.NaN; public WaveformEnvelopeBuilder() {} /** * Sets the initial frequency for the waveform in Hertz. * *

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: * *

{@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 mSegments = new ArrayList<>(); private float mLastIntensity = 0f; private float mLastSharpness = Float.NaN; public BasicEnvelopeBuilder() {} /** * Sets the initial sharpness for the basic envelope effect. * *

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: * *

     * {@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();}
* *

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. * *

     * {@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();}
* *

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: * *

     * {@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 mSegments = new ArrayList<>(); private float mLastAmplitude = 0f; private float mLastFrequencyHz = 0f; WaveformBuilder() {} /** * Add a transition to new vibration parameter value to the end of this waveform. * *

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 CREATOR = new Parcelable.Creator() { @Override public VibrationEffect createFromParcel(Parcel in) { switch (in.readInt()) { case PARCEL_TOKEN_COMPOSED: return new Composed(in); case PARCEL_TOKEN_VENDOR_EFFECT: if (Flags.vendorVibrationEffects()) { return new VendorEffect(in); } // else fall through default: throw new IllegalStateException( "Unexpected vibration effect type token in parcel."); } } @Override public VibrationEffect[] newArray(int size) { return new VibrationEffect[size]; } }; }