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