1651 lines
68 KiB
Java
1651 lines
68 KiB
Java
/*
|
|
* 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];
|
|
}
|
|
};
|
|
}
|