| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.os; |
| |
| import android.annotation.FloatRange; |
| import android.annotation.IntDef; |
| import android.annotation.IntRange; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.TestApi; |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.hardware.vibrator.V1_0.EffectStrength; |
| import android.hardware.vibrator.V1_3.Effect; |
| import android.net.Uri; |
| import android.os.vibrator.PrebakedSegment; |
| import android.os.vibrator.PrimitiveSegment; |
| import android.os.vibrator.RampSegment; |
| import android.os.vibrator.StepSegment; |
| import android.os.vibrator.VibrationEffectSegment; |
| import android.util.MathUtils; |
| |
| import com.android.internal.util.Preconditions; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.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; |
| |
| /** |
| * A VibrationEffect describes a haptic effect to be performed by a {@link Vibrator}. |
| * |
| * <p>These effects may be any number of things, from single shot vibrations to complex waveforms. |
| */ |
| public abstract class VibrationEffect implements Parcelable { |
| // Stevens' coefficient to scale the perceived vibration intensity. |
| private static final float SCALE_GAMMA = 0.65f; |
| // 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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>In effect, the timings array represents the number of milliseconds <em>before</em> 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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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}. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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<StepSegment> 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. |
| * |
| * <p>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. |
| * |
| * <p>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); |
| } |
| |
| /** |
| * Get a predefined vibration effect. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>The waveform builder offers more flexibility for creating waveform vibrations, allowing |
| * control over vibration amplitude and frequency via smooth transitions between values. |
| * |
| * <p>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}. |
| * |
| * <p>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}. |
| * |
| * <p>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(); |
| |
| /** |
| * Gets the estimated duration of the vibration in milliseconds. |
| * |
| * <p>For effects without a defined end (e.g. a Waveform with a non-negative repeat index), this |
| * returns Long.MAX_VALUE. For effects with an unknown duration (e.g. Prebaked effects where |
| * the length is device and potentially run-time dependent), this returns -1. |
| * |
| * @hide |
| */ |
| @TestApi |
| public abstract long getDuration(); |
| |
| /** |
| * Checks if a vibrator with a given {@link VibratorInfo} can play this effect as intended. |
| * |
| * <p>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. |
| * |
| * <p>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 |
| */ |
| public abstract <T extends VibrationEffect> T resolve(int defaultAmplitude); |
| |
| /** |
| * 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 |
| */ |
| public abstract <T extends VibrationEffect> T scale(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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p> 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) { |
| // 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) { |
| switch (effectId) { |
| case EFFECT_CLICK: |
| return "CLICK"; |
| case EFFECT_TICK: |
| return "TICK"; |
| case EFFECT_HEAVY_CLICK: |
| return "HEAVY_CLICK"; |
| case EFFECT_DOUBLE_CLICK: |
| return "DOUBLE_CLICK"; |
| case EFFECT_POP: |
| return "POP"; |
| case EFFECT_THUD: |
| return "THUD"; |
| case EFFECT_TEXTURE_TICK: |
| return "TEXTURE_TICK"; |
| default: |
| return Integer.toString(effectId); |
| } |
| } |
| |
| /** @hide */ |
| public static String effectStrengthToString(int effectStrength) { |
| switch (effectStrength) { |
| case EFFECT_STRENGTH_LIGHT: |
| return "LIGHT"; |
| case EFFECT_STRENGTH_MEDIUM: |
| return "MEDIUM"; |
| case EFFECT_STRENGTH_STRONG: |
| return "STRONG"; |
| default: |
| return Integer.toString(effectStrength); |
| } |
| } |
| |
| /** |
| * Transforms a {@link VibrationEffect} using a generic parameter. |
| * |
| * <p>This can be used for scaling effects based on user settings or adapting them to the |
| * capabilities of a specific device vibrator. |
| * |
| * @param <ParamT> The type of parameter to be used on the effect by this transformation |
| * @hide |
| */ |
| public interface Transformation<ParamT> { |
| |
| /** 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<VibrationEffectSegment> mSegments; |
| private final int mRepeatIndex; |
| |
| Composed(@NonNull Parcel in) { |
| this(in.readArrayList( |
| VibrationEffectSegment.class.getClassLoader(), VibrationEffectSegment.class), |
| in.readInt()); |
| } |
| |
| Composed(@NonNull VibrationEffectSegment segment) { |
| this(Arrays.asList(segment), /* repeatIndex= */ -1); |
| } |
| |
| /** @hide */ |
| public Composed(@NonNull List<? extends VibrationEffectSegment> segments, int repeatIndex) { |
| super(); |
| mSegments = new ArrayList<>(segments); |
| mRepeatIndex = repeatIndex; |
| } |
| |
| @NonNull |
| public List<VibrationEffectSegment> 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<VibrationEffectSegment> 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 + ")"); |
| } |
| } |
| |
| @Override |
| public long getDuration() { |
| if (mRepeatIndex >= 0) { |
| return Long.MAX_VALUE; |
| } |
| int segmentCount = mSegments.size(); |
| long totalDuration = 0; |
| for (int i = 0; i < segmentCount; i++) { |
| long segmentDuration = mSegments.get(i).getDuration(); |
| 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 prebaked 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) { |
| int segmentCount = mSegments.size(); |
| ArrayList<VibrationEffectSegment> resolvedSegments = new ArrayList<>(segmentCount); |
| for (int i = 0; i < segmentCount; i++) { |
| resolvedSegments.add(mSegments.get(i).resolve(defaultAmplitude)); |
| } |
| if (resolvedSegments.equals(mSegments)) { |
| return this; |
| } |
| Composed resolved = new Composed(resolvedSegments, mRepeatIndex); |
| resolved.validate(); |
| return resolved; |
| } |
| |
| /** @hide */ |
| @NonNull |
| @Override |
| public Composed scale(float scaleFactor) { |
| int segmentCount = mSegments.size(); |
| ArrayList<VibrationEffectSegment> scaledSegments = new ArrayList<>(segmentCount); |
| for (int i = 0; i < segmentCount; i++) { |
| scaledSegments.add(mSegments.get(i).scale(scaleFactor)); |
| } |
| if (scaledSegments.equals(mSegments)) { |
| return this; |
| } |
| Composed scaled = new Composed(scaledSegments, mRepeatIndex); |
| scaled.validate(); |
| return scaled; |
| } |
| |
| /** @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<VibrationEffectSegment> 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)) { |
| return false; |
| } |
| Composed other = (Composed) o; |
| 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.writeList(mSegments); |
| out.writeInt(mRepeatIndex); |
| } |
| |
| @NonNull |
| public static final Creator<Composed> CREATOR = |
| new Creator<Composed>() { |
| @Override |
| public Composed createFromParcel(Parcel in) { |
| 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; |
| } |
| } |
| |
| /** |
| * A composition of haptic elements that are combined to be playable as a single |
| * {@link VibrationEffect}. |
| * |
| * <p>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: |
| * |
| * <pre> |
| * {@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();}</pre> |
| * |
| * <p>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 { |
| } |
| |
| /** |
| * 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; |
| |
| |
| private final ArrayList<VibrationEffectSegment> 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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * @param primitiveId The primitive to add |
| * @param scale The scale to apply to the intensity of the primitive. |
| * @param delay The amount of time in milliseconds to wait before playing this primitive, |
| * starting at the time the previous element in this composition is finished. |
| * @return 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) { |
| PrimitiveSegment primitive = new PrimitiveSegment(primitiveId, scale, delay); |
| 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(); |
| } |
| Composed composed = (Composed) effect; |
| if (composed.getRepeatIndex() >= 0) { |
| // Start repeating from the index relative to the composed waveform. |
| mRepeatIndex = mSegments.size() + composed.getRepeatIndex(); |
| } |
| mSegments.addAll(composed.getSegments()); |
| return this; |
| } |
| |
| /** |
| * Compose all of the added primitives together into a single {@link VibrationEffect}. |
| * |
| * <p>The {@link Composition} object is still valid after this call, so you can continue |
| * adding more primitives to it and generating more {@link VibrationEffect}s by calling this |
| * method again. |
| * |
| * @return The {@link VibrationEffect} resulting from the composition of the primitives. |
| */ |
| @NonNull |
| public VibrationEffect compose() { |
| if (mSegments.isEmpty()) { |
| throw new IllegalStateException( |
| "Composition must have at least one element to compose."); |
| } |
| VibrationEffect effect = new Composed(mSegments, mRepeatIndex); |
| effect.validate(); |
| return effect; |
| } |
| |
| /** |
| * Convert the primitive ID to a human readable string for debugging. |
| * @param id The ID to convert |
| * @return The ID in a human readable format. |
| * @hide |
| */ |
| public static String primitiveToString(@PrimitiveType int id) { |
| switch (id) { |
| case PRIMITIVE_NOOP: |
| return "NOOP"; |
| case PRIMITIVE_CLICK: |
| return "CLICK"; |
| case PRIMITIVE_THUD: |
| return "THUD"; |
| case PRIMITIVE_SPIN: |
| return "SPIN"; |
| case PRIMITIVE_QUICK_RISE: |
| return "QUICK_RISE"; |
| case PRIMITIVE_SLOW_RISE: |
| return "SLOW_RISE"; |
| case PRIMITIVE_QUICK_FALL: |
| return "QUICK_FALL"; |
| case PRIMITIVE_TICK: |
| return "TICK"; |
| case PRIMITIVE_LOW_TICK: |
| return "LOW_TICK"; |
| default: |
| return Integer.toString(id); |
| } |
| } |
| } |
| |
| /** |
| * A builder for waveform haptic effects. |
| * |
| * <p>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. |
| * |
| * <p>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: |
| * |
| * <pre> |
| * {@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();}</pre> |
| * |
| * <p>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. |
| * |
| * <p>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)}: |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <pre> |
| * {@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();}</pre> |
| * |
| * <p>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: |
| * |
| * <pre> |
| * {@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();}</pre> |
| * |
| * @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<VibrationEffectSegment> 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. |
| * |
| * <p>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. |
| * |
| * <p>Vibration parameter values will be truncated to conform to the device capabilities |
| * according to the {@link android.os.vibrator.VibratorFrequencyProfile}. |
| * |
| * @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. |
| * |
| * <p>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. |
| * |
| * <p>Vibration parameters values will be truncated to conform to the device capabilities |
| * according to the {@link android.os.vibrator.VibratorFrequencyProfile}. |
| * |
| * @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. |
| * |
| * <p>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}. |
| * |
| * <p>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. |
| * |
| * <p>This is to describe a waveform haptic effect, which consists of one or more timed |
| * transitions to a new set of {@link VibrationParameter}s. |
| * |
| * <p>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<VibrationEffect> CREATOR = |
| new Parcelable.Creator<VibrationEffect>() { |
| @Override |
| public VibrationEffect createFromParcel(Parcel in) { |
| return new Composed(in); |
| } |
| @Override |
| public VibrationEffect[] newArray(int size) { |
| return new VibrationEffect[size]; |
| } |
| }; |
| } |