931 lines
33 KiB
Java
931 lines
33 KiB
Java
![]() |
/*
|
||
|
* Copyright (C) 2020 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.NonNull;
|
||
|
import android.annotation.TestApi;
|
||
|
import android.util.SparseArray;
|
||
|
|
||
|
import com.android.internal.util.Preconditions;
|
||
|
|
||
|
import java.util.ArrayList;
|
||
|
import java.util.List;
|
||
|
import java.util.Locale;
|
||
|
import java.util.Objects;
|
||
|
import java.util.StringJoiner;
|
||
|
|
||
|
/**
|
||
|
* A CombinedVibration describes a combination of haptic effects to be performed by one or more
|
||
|
* {@link Vibrator Vibrators}.
|
||
|
*
|
||
|
* These effects may be any number of things, from single shot vibrations to complex waveforms.
|
||
|
*
|
||
|
* @see VibrationEffect
|
||
|
*/
|
||
|
@SuppressWarnings({"ParcelNotFinal", "ParcelCreator"}) // Parcel only extended here.
|
||
|
public abstract class CombinedVibration implements Parcelable {
|
||
|
private static final int PARCEL_TOKEN_MONO = 1;
|
||
|
private static final int PARCEL_TOKEN_STEREO = 2;
|
||
|
private static final int PARCEL_TOKEN_SEQUENTIAL = 3;
|
||
|
|
||
|
/** Prevent subclassing from outside of the framework. */
|
||
|
CombinedVibration() {
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a vibration that plays a single effect in parallel on all vibrators.
|
||
|
*
|
||
|
* A parallel vibration that takes a single {@link VibrationEffect} to be performed by multiple
|
||
|
* vibrators at the same time.
|
||
|
*
|
||
|
* @param effect The {@link VibrationEffect} to perform.
|
||
|
* @return The combined vibration representing the single effect to be played in all vibrators.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public static CombinedVibration createParallel(@NonNull VibrationEffect effect) {
|
||
|
CombinedVibration combined = new Mono(effect);
|
||
|
combined.validate();
|
||
|
return combined;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Start creating a vibration that plays effects in parallel on one or more vibrators.
|
||
|
*
|
||
|
* A parallel vibration takes one or more {@link VibrationEffect VibrationEffects} associated to
|
||
|
* individual vibrators to be performed at the same time.
|
||
|
*
|
||
|
* @see CombinedVibration.ParallelCombination
|
||
|
*/
|
||
|
@NonNull
|
||
|
public static ParallelCombination startParallel() {
|
||
|
return new ParallelCombination();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Start creating a vibration that plays effects in sequence on one or more vibrators.
|
||
|
*
|
||
|
* A sequential vibration takes one or more {@link CombinedVibration CombinedVibrations} to be
|
||
|
* performed by one or more vibrators in order. Each {@link CombinedVibration} starts only after
|
||
|
* the previous one is finished.
|
||
|
*
|
||
|
* @hide
|
||
|
* @see CombinedVibration.SequentialCombination
|
||
|
*/
|
||
|
@TestApi
|
||
|
@NonNull
|
||
|
public static SequentialCombination startSequential() {
|
||
|
return new SequentialCombination();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int describeContents() {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the estimated duration of the combined vibration in milliseconds.
|
||
|
*
|
||
|
* <p>For parallel combinations this means the maximum duration of any individual {@link
|
||
|
* VibrationEffect}. For sequential combinations, this is a sum of each step and delays.
|
||
|
*
|
||
|
* <p>For combinations of 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();
|
||
|
|
||
|
/**
|
||
|
* 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;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public abstract void validate();
|
||
|
|
||
|
/**
|
||
|
* Applies given effect transformation with a fixed parameter to each effect in this vibration.
|
||
|
*
|
||
|
* @param transformation The vibration effect transformation to be applied to all effects
|
||
|
* @param param The fixed parameter to be applied in all effect transformations
|
||
|
* @return the result of running the given transformation on all effects of this vibration
|
||
|
* @hide
|
||
|
*/
|
||
|
public abstract <ParamT> CombinedVibration transform(
|
||
|
VibrationEffect.Transformation<ParamT> transformation, ParamT param);
|
||
|
|
||
|
/**
|
||
|
* Applies given vibrator adapter to each effect in this combined vibration.
|
||
|
*
|
||
|
* @param adapter The vibrator adapter to be used on this vibration
|
||
|
* @return the result of running the given adapter on all effects of this vibration
|
||
|
* @hide
|
||
|
*/
|
||
|
public abstract CombinedVibration adapt(VibratorAdapter adapter);
|
||
|
|
||
|
/** @hide */
|
||
|
public abstract boolean hasVibrator(int vibratorId);
|
||
|
|
||
|
/**
|
||
|
* Returns a compact version of the {@link #toString()} result for debugging purposes.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public abstract String toDebugString();
|
||
|
|
||
|
/**
|
||
|
* Adapts a {@link VibrationEffect} to a specific device vibrator using the ID.
|
||
|
*
|
||
|
* <p>This can be used for adapting effects to the capabilities of the specific device vibrator
|
||
|
* it's been mapped to by the combined vibration.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public interface VibratorAdapter {
|
||
|
|
||
|
/**
|
||
|
* Return the list of vibrator IDs available on the device, to be used by {@link
|
||
|
* CombinedVibration} to fan-out individual effects that aren't assigned to a specific
|
||
|
* vibrator.
|
||
|
*/
|
||
|
int[] getAvailableVibratorIds();
|
||
|
|
||
|
/** Adapts a {@link VibrationEffect} to a given vibrator. */
|
||
|
@NonNull
|
||
|
VibrationEffect adaptToVibrator(int vibratorId, @NonNull VibrationEffect effect);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A combination of haptic effects that should be played in multiple vibrators in parallel.
|
||
|
*
|
||
|
* @see CombinedVibration#startParallel()
|
||
|
*/
|
||
|
public static final class ParallelCombination {
|
||
|
|
||
|
private final SparseArray<VibrationEffect> mEffects = new SparseArray<>();
|
||
|
|
||
|
ParallelCombination() {
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add or replace a one shot vibration effect to be performed by the specified vibrator.
|
||
|
*
|
||
|
* @param vibratorId The id of the vibrator that should perform this effect.
|
||
|
* @param effect The effect this vibrator should play.
|
||
|
* @return The {@link ParallelCombination} object to enable adding
|
||
|
* multiple effects in one chain.
|
||
|
* @see VibrationEffect#createOneShot(long, int)
|
||
|
*/
|
||
|
@NonNull
|
||
|
public ParallelCombination addVibrator(int vibratorId, @NonNull VibrationEffect effect) {
|
||
|
mEffects.put(vibratorId, effect);
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Combine all of the added effects into a {@link CombinedVibration}.
|
||
|
*
|
||
|
* The {@link ParallelCombination} object is still valid after this
|
||
|
* call, so you can continue adding more effects to it and generating more
|
||
|
* {@link CombinedVibration}s by calling this method again.
|
||
|
*
|
||
|
* @return The {@link CombinedVibration} resulting from combining the added effects to
|
||
|
* be played in parallel.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public CombinedVibration combine() {
|
||
|
if (mEffects.size() == 0) {
|
||
|
throw new IllegalStateException(
|
||
|
"Combination must have at least one element to combine.");
|
||
|
}
|
||
|
CombinedVibration combined = new Stereo(mEffects);
|
||
|
combined.validate();
|
||
|
return combined;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A combination of haptic effects that should be played in multiple vibrators in sequence.
|
||
|
*
|
||
|
* @hide
|
||
|
* @see CombinedVibration#startSequential()
|
||
|
*/
|
||
|
@TestApi
|
||
|
public static final class SequentialCombination {
|
||
|
|
||
|
private final ArrayList<CombinedVibration> mEffects = new ArrayList<>();
|
||
|
private final ArrayList<Integer> mDelays = new ArrayList<>();
|
||
|
|
||
|
SequentialCombination() {
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add a single vibration effect to be performed next.
|
||
|
*
|
||
|
* Similar to {@link #addNext(int, VibrationEffect, int)}, but with no delay. The effect
|
||
|
* will start playing immediately after the previous vibration is finished.
|
||
|
*
|
||
|
* @param vibratorId The id of the vibrator that should perform this effect.
|
||
|
* @param effect The effect this vibrator should play.
|
||
|
* @return The {@link CombinedVibration.SequentialCombination} object to enable adding
|
||
|
* multiple effects in one chain.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public SequentialCombination addNext(int vibratorId, @NonNull VibrationEffect effect) {
|
||
|
return addNext(vibratorId, effect, /* delay= */ 0);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add a single vibration effect to be performed next.
|
||
|
*
|
||
|
* The delay is applied immediately after the previous vibration is finished. The effect
|
||
|
* will start playing after the delay.
|
||
|
*
|
||
|
* @param vibratorId The id of the vibrator that should perform this effect.
|
||
|
* @param effect The effect this vibrator should play.
|
||
|
* @param delay The amount of time, in milliseconds, to wait between playing the prior
|
||
|
* vibration and this one, starting at the time the previous vibration in
|
||
|
* this sequence is finished.
|
||
|
* @return The {@link CombinedVibration.SequentialCombination} object to enable adding
|
||
|
* multiple effects in one chain.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public SequentialCombination addNext(int vibratorId, @NonNull VibrationEffect effect,
|
||
|
int delay) {
|
||
|
return addNext(
|
||
|
CombinedVibration.startParallel().addVibrator(vibratorId, effect).combine(),
|
||
|
delay);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add a combined vibration effect to be performed next.
|
||
|
*
|
||
|
* Similar to {@link #addNext(CombinedVibration, int)}, but with no delay. The effect will
|
||
|
* start playing immediately after the previous vibration is finished.
|
||
|
*
|
||
|
* @param effect The combined effect to be performed next.
|
||
|
* @return The {@link CombinedVibration.SequentialCombination} object to enable adding
|
||
|
* multiple effects in one chain.
|
||
|
* @see VibrationEffect#createOneShot(long, int)
|
||
|
*/
|
||
|
@NonNull
|
||
|
public SequentialCombination addNext(@NonNull CombinedVibration effect) {
|
||
|
return addNext(effect, /* delay= */ 0);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add a combined vibration effect to be performed next.
|
||
|
*
|
||
|
* The delay is applied immediately after the previous vibration is finished. The vibration
|
||
|
* will start playing after the delay.
|
||
|
*
|
||
|
* @param effect The combined effect to be performed next.
|
||
|
* @param delay The amount of time, in milliseconds, to wait between playing the prior
|
||
|
* vibration and this one, starting at the time the previous vibration in this
|
||
|
* sequence is finished.
|
||
|
* @return The {@link CombinedVibration.SequentialCombination} object to enable adding
|
||
|
* multiple effects in one chain.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public SequentialCombination addNext(@NonNull CombinedVibration effect, int delay) {
|
||
|
if (effect instanceof Sequential) {
|
||
|
Sequential sequentialEffect = (Sequential) effect;
|
||
|
int firstEffectIndex = mDelays.size();
|
||
|
mEffects.addAll(sequentialEffect.getEffects());
|
||
|
mDelays.addAll(sequentialEffect.getDelays());
|
||
|
mDelays.set(firstEffectIndex, delay + mDelays.get(firstEffectIndex));
|
||
|
} else {
|
||
|
mEffects.add(effect);
|
||
|
mDelays.add(delay);
|
||
|
}
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Combine all of the added effects in sequence.
|
||
|
*
|
||
|
* The {@link CombinedVibration.SequentialCombination} object is still valid after
|
||
|
* this call, so you can continue adding more effects to it and generating more {@link
|
||
|
* CombinedVibration}s by calling this method again.
|
||
|
*
|
||
|
* @return The {@link CombinedVibration} resulting from combining the added effects to
|
||
|
* be played in sequence.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public CombinedVibration combine() {
|
||
|
if (mEffects.size() == 0) {
|
||
|
throw new IllegalStateException(
|
||
|
"Combination must have at least one element to combine.");
|
||
|
}
|
||
|
CombinedVibration combined = new Sequential(mEffects, mDelays);
|
||
|
combined.validate();
|
||
|
return combined;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Represents a single {@link VibrationEffect} that should be played in all vibrators at the
|
||
|
* same time.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@TestApi
|
||
|
public static final class Mono extends CombinedVibration {
|
||
|
private final VibrationEffect mEffect;
|
||
|
|
||
|
Mono(Parcel in) {
|
||
|
mEffect = VibrationEffect.CREATOR.createFromParcel(in);
|
||
|
}
|
||
|
|
||
|
Mono(@NonNull VibrationEffect effect) {
|
||
|
mEffect = effect;
|
||
|
}
|
||
|
|
||
|
@NonNull
|
||
|
public VibrationEffect getEffect() {
|
||
|
return mEffect;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public long getDuration() {
|
||
|
return mEffect.getDuration();
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public boolean isHapticFeedbackCandidate() {
|
||
|
return mEffect.isHapticFeedbackCandidate();
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public void validate() {
|
||
|
mEffect.validate();
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public <ParamT> CombinedVibration transform(
|
||
|
VibrationEffect.Transformation<ParamT> transformation, ParamT param) {
|
||
|
VibrationEffect newEffect = transformation.transform(mEffect, param);
|
||
|
if (mEffect.equals(newEffect)) {
|
||
|
return this;
|
||
|
}
|
||
|
// Make sure the validate methods are triggered
|
||
|
return CombinedVibration.createParallel(newEffect);
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public CombinedVibration adapt(VibratorAdapter adapter) {
|
||
|
ParallelCombination combination = CombinedVibration.startParallel();
|
||
|
boolean hasSameEffects = true;
|
||
|
for (int vibratorId : adapter.getAvailableVibratorIds()) {
|
||
|
VibrationEffect newEffect = adapter.adaptToVibrator(vibratorId, mEffect);
|
||
|
combination.addVibrator(vibratorId, newEffect);
|
||
|
hasSameEffects &= mEffect.equals(newEffect);
|
||
|
}
|
||
|
if (hasSameEffects) {
|
||
|
return this;
|
||
|
}
|
||
|
// Make sure the validate methods are triggered
|
||
|
return combination.combine();
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public boolean hasVibrator(int vibratorId) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean equals(Object o) {
|
||
|
if (this == o) {
|
||
|
return true;
|
||
|
}
|
||
|
if (!(o instanceof Mono)) {
|
||
|
return false;
|
||
|
}
|
||
|
Mono other = (Mono) o;
|
||
|
return mEffect.equals(other.mEffect);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int hashCode() {
|
||
|
return Objects.hash(mEffect);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String toString() {
|
||
|
return "Mono{mEffect=" + mEffect + '}';
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public String toDebugString() {
|
||
|
// Simplify vibration string, use the single effect to represent it.
|
||
|
return mEffect.toDebugString();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int describeContents() {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(@NonNull Parcel out, int flags) {
|
||
|
out.writeInt(PARCEL_TOKEN_MONO);
|
||
|
mEffect.writeToParcel(out, flags);
|
||
|
}
|
||
|
|
||
|
@NonNull
|
||
|
public static final Parcelable.Creator<Mono> CREATOR =
|
||
|
new Parcelable.Creator<Mono>() {
|
||
|
@Override
|
||
|
public Mono createFromParcel(@NonNull Parcel in) {
|
||
|
// Skip the type token
|
||
|
in.readInt();
|
||
|
return new Mono(in);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
@NonNull
|
||
|
public Mono[] newArray(int size) {
|
||
|
return new Mono[size];
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Represents a set of {@link VibrationEffect VibrationEffects} associated to individual
|
||
|
* vibrators that should be played at the same time.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@TestApi
|
||
|
public static final class Stereo extends CombinedVibration {
|
||
|
|
||
|
/** Mapping vibrator ids to effects. */
|
||
|
private final SparseArray<VibrationEffect> mEffects;
|
||
|
|
||
|
Stereo(Parcel in) {
|
||
|
int size = in.readInt();
|
||
|
mEffects = new SparseArray<>(size);
|
||
|
for (int i = 0; i < size; i++) {
|
||
|
int vibratorId = in.readInt();
|
||
|
mEffects.put(vibratorId, VibrationEffect.CREATOR.createFromParcel(in));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Stereo(@NonNull SparseArray<VibrationEffect> effects) {
|
||
|
mEffects = new SparseArray<>(effects.size());
|
||
|
for (int i = 0; i < effects.size(); i++) {
|
||
|
mEffects.put(effects.keyAt(i), effects.valueAt(i));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Effects to be performed in parallel, where each key represents the vibrator id. */
|
||
|
@NonNull
|
||
|
public SparseArray<VibrationEffect> getEffects() {
|
||
|
return mEffects;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public long getDuration() {
|
||
|
long maxDuration = Long.MIN_VALUE;
|
||
|
boolean hasUnknownStep = false;
|
||
|
for (int i = 0; i < mEffects.size(); i++) {
|
||
|
long duration = mEffects.valueAt(i).getDuration();
|
||
|
if (duration == Long.MAX_VALUE) {
|
||
|
// If any duration is repeating, this combination duration is also repeating.
|
||
|
return duration;
|
||
|
}
|
||
|
maxDuration = Math.max(maxDuration, duration);
|
||
|
// If any step is unknown, this combination duration will also be unknown, unless
|
||
|
// any step is repeating. Repeating vibrations take precedence over non-repeating
|
||
|
// ones in the service, so continue looping to check for repeating steps.
|
||
|
hasUnknownStep |= duration < 0;
|
||
|
}
|
||
|
if (hasUnknownStep) {
|
||
|
// If any step is unknown, this combination duration is also unknown.
|
||
|
return -1;
|
||
|
}
|
||
|
return maxDuration;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public boolean isHapticFeedbackCandidate() {
|
||
|
for (int i = 0; i < mEffects.size(); i++) {
|
||
|
if (!mEffects.valueAt(i).isHapticFeedbackCandidate()) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public void validate() {
|
||
|
Preconditions.checkArgument(mEffects.size() > 0,
|
||
|
"There should be at least one effect set for a combined effect");
|
||
|
for (int i = 0; i < mEffects.size(); i++) {
|
||
|
mEffects.valueAt(i).validate();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public <ParamT> CombinedVibration transform(
|
||
|
VibrationEffect.Transformation<ParamT> transformation, ParamT param) {
|
||
|
ParallelCombination combination = CombinedVibration.startParallel();
|
||
|
boolean hasSameEffects = true;
|
||
|
for (int i = 0; i < mEffects.size(); i++) {
|
||
|
int vibratorId = mEffects.keyAt(i);
|
||
|
VibrationEffect effect = mEffects.valueAt(i);
|
||
|
VibrationEffect newEffect = transformation.transform(effect, param);
|
||
|
combination.addVibrator(vibratorId, newEffect);
|
||
|
hasSameEffects &= effect.equals(newEffect);
|
||
|
}
|
||
|
if (hasSameEffects) {
|
||
|
return this;
|
||
|
}
|
||
|
// Make sure the validate methods are triggered
|
||
|
return combination.combine();
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public CombinedVibration adapt(VibratorAdapter adapter) {
|
||
|
ParallelCombination combination = CombinedVibration.startParallel();
|
||
|
boolean hasSameEffects = true;
|
||
|
for (int i = 0; i < mEffects.size(); i++) {
|
||
|
int vibratorId = mEffects.keyAt(i);
|
||
|
VibrationEffect effect = mEffects.valueAt(i);
|
||
|
VibrationEffect newEffect = adapter.adaptToVibrator(vibratorId, effect);
|
||
|
combination.addVibrator(vibratorId, newEffect);
|
||
|
hasSameEffects &= effect.equals(newEffect);
|
||
|
}
|
||
|
if (hasSameEffects) {
|
||
|
return this;
|
||
|
}
|
||
|
// Make sure the validate methods are triggered
|
||
|
return combination.combine();
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public boolean hasVibrator(int vibratorId) {
|
||
|
return mEffects.indexOfKey(vibratorId) >= 0;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean equals(Object o) {
|
||
|
if (this == o) {
|
||
|
return true;
|
||
|
}
|
||
|
if (!(o instanceof Stereo)) {
|
||
|
return false;
|
||
|
}
|
||
|
Stereo other = (Stereo) o;
|
||
|
if (mEffects.size() != other.mEffects.size()) {
|
||
|
return false;
|
||
|
}
|
||
|
for (int i = 0; i < mEffects.size(); i++) {
|
||
|
if (!mEffects.valueAt(i).equals(other.mEffects.get(mEffects.keyAt(i)))) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int hashCode() {
|
||
|
return mEffects.contentHashCode();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String toString() {
|
||
|
return "Stereo{mEffects=" + mEffects + '}';
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public String toDebugString() {
|
||
|
StringJoiner sj = new StringJoiner(",", "Stereo{", "}");
|
||
|
for (int i = 0; i < mEffects.size(); i++) {
|
||
|
sj.add(String.format(Locale.ROOT, "vibrator(id=%d): %s",
|
||
|
mEffects.keyAt(i), mEffects.valueAt(i).toDebugString()));
|
||
|
}
|
||
|
return sj.toString();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int describeContents() {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(@NonNull Parcel out, int flags) {
|
||
|
out.writeInt(PARCEL_TOKEN_STEREO);
|
||
|
out.writeInt(mEffects.size());
|
||
|
for (int i = 0; i < mEffects.size(); i++) {
|
||
|
out.writeInt(mEffects.keyAt(i));
|
||
|
mEffects.valueAt(i).writeToParcel(out, flags);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@NonNull
|
||
|
public static final Parcelable.Creator<Stereo> CREATOR =
|
||
|
new Parcelable.Creator<Stereo>() {
|
||
|
@Override
|
||
|
public Stereo createFromParcel(@NonNull Parcel in) {
|
||
|
// Skip the type token
|
||
|
in.readInt();
|
||
|
return new Stereo(in);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
@NonNull
|
||
|
public Stereo[] newArray(int size) {
|
||
|
return new Stereo[size];
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Represents a list of {@link CombinedVibration CombinedVibrations} that should be played in
|
||
|
* sequence.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@TestApi
|
||
|
public static final class Sequential extends CombinedVibration {
|
||
|
// If a vibration is playing more than 3 effects, it's probably not haptic feedback
|
||
|
private static final long MAX_HAPTIC_FEEDBACK_SEQUENCE_SIZE = 3;
|
||
|
|
||
|
private final List<CombinedVibration> mEffects;
|
||
|
private final List<Integer> mDelays;
|
||
|
|
||
|
Sequential(Parcel in) {
|
||
|
int size = in.readInt();
|
||
|
mEffects = new ArrayList<>(size);
|
||
|
mDelays = new ArrayList<>(size);
|
||
|
for (int i = 0; i < size; i++) {
|
||
|
mDelays.add(in.readInt());
|
||
|
mEffects.add(CombinedVibration.CREATOR.createFromParcel(in));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Sequential(@NonNull List<CombinedVibration> effects,
|
||
|
@NonNull List<Integer> delays) {
|
||
|
mEffects = new ArrayList<>(effects);
|
||
|
mDelays = new ArrayList<>(delays);
|
||
|
}
|
||
|
|
||
|
/** Effects to be performed in sequence. */
|
||
|
@NonNull
|
||
|
public List<CombinedVibration> getEffects() {
|
||
|
return mEffects;
|
||
|
}
|
||
|
|
||
|
/** Delay to be applied before each effect in {@link #getEffects()}. */
|
||
|
@NonNull
|
||
|
public List<Integer> getDelays() {
|
||
|
return mDelays;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public long getDuration() {
|
||
|
boolean hasUnknownStep = false;
|
||
|
long durations = 0;
|
||
|
final int effectCount = mEffects.size();
|
||
|
for (int i = 0; i < effectCount; i++) {
|
||
|
CombinedVibration effect = mEffects.get(i);
|
||
|
long duration = effect.getDuration();
|
||
|
if (duration == Long.MAX_VALUE) {
|
||
|
// If any duration is repeating, this combination duration is also repeating.
|
||
|
return duration;
|
||
|
}
|
||
|
durations += duration;
|
||
|
// If any step is unknown, this combination duration will also be unknown, unless
|
||
|
// any step is repeating. Repeating vibrations take precedence over non-repeating
|
||
|
// ones in the service, so continue looping to check for repeating steps.
|
||
|
hasUnknownStep |= duration < 0;
|
||
|
}
|
||
|
if (hasUnknownStep) {
|
||
|
// If any step is unknown, this combination duration is also unknown.
|
||
|
return -1;
|
||
|
}
|
||
|
long delays = 0;
|
||
|
for (int i = 0; i < effectCount; i++) {
|
||
|
delays += mDelays.get(i);
|
||
|
}
|
||
|
return durations + delays;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public boolean isHapticFeedbackCandidate() {
|
||
|
final int effectCount = mEffects.size();
|
||
|
if (effectCount > MAX_HAPTIC_FEEDBACK_SEQUENCE_SIZE) {
|
||
|
return false;
|
||
|
}
|
||
|
for (int i = 0; i < effectCount; i++) {
|
||
|
if (!mEffects.get(i).isHapticFeedbackCandidate()) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public void validate() {
|
||
|
Preconditions.checkArgument(mEffects.size() > 0,
|
||
|
"There should be at least one effect set for a combined effect");
|
||
|
Preconditions.checkArgument(mEffects.size() == mDelays.size(),
|
||
|
"Effect and delays should have equal length");
|
||
|
final int effectCount = mEffects.size();
|
||
|
for (int i = 0; i < effectCount; i++) {
|
||
|
if (mDelays.get(i) < 0) {
|
||
|
throw new IllegalArgumentException("Delays must all be >= 0"
|
||
|
+ " (delays=" + mDelays + ")");
|
||
|
}
|
||
|
}
|
||
|
for (int i = 0; i < effectCount; i++) {
|
||
|
CombinedVibration effect = mEffects.get(i);
|
||
|
if (effect instanceof Sequential) {
|
||
|
throw new IllegalArgumentException(
|
||
|
"There should be no nested sequential effects in a combined effect");
|
||
|
}
|
||
|
effect.validate();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public <ParamT> CombinedVibration transform(
|
||
|
VibrationEffect.Transformation<ParamT> transformation, ParamT param) {
|
||
|
SequentialCombination combination = CombinedVibration.startSequential();
|
||
|
boolean hasSameEffects = true;
|
||
|
for (int i = 0; i < mEffects.size(); i++) {
|
||
|
CombinedVibration vibration = mEffects.get(i);
|
||
|
CombinedVibration newVibration = vibration.transform(transformation, param);
|
||
|
combination.addNext(newVibration, mDelays.get(i));
|
||
|
hasSameEffects &= vibration.equals(newVibration);
|
||
|
}
|
||
|
if (hasSameEffects) {
|
||
|
return this;
|
||
|
}
|
||
|
// Make sure the validate methods are triggered
|
||
|
return combination.combine();
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public CombinedVibration adapt(VibratorAdapter adapter) {
|
||
|
SequentialCombination combination = CombinedVibration.startSequential();
|
||
|
boolean hasSameEffects = true;
|
||
|
for (int i = 0; i < mEffects.size(); i++) {
|
||
|
CombinedVibration vibration = mEffects.get(i);
|
||
|
CombinedVibration newVibration = vibration.adapt(adapter);
|
||
|
combination.addNext(newVibration, mDelays.get(i));
|
||
|
hasSameEffects &= vibration.equals(newVibration);
|
||
|
}
|
||
|
if (hasSameEffects) {
|
||
|
return this;
|
||
|
}
|
||
|
// Make sure the validate methods are triggered
|
||
|
return combination.combine();
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public boolean hasVibrator(int vibratorId) {
|
||
|
final int effectCount = mEffects.size();
|
||
|
for (int i = 0; i < effectCount; i++) {
|
||
|
if (mEffects.get(i).hasVibrator(vibratorId)) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean equals(Object o) {
|
||
|
if (this == o) {
|
||
|
return true;
|
||
|
}
|
||
|
if (!(o instanceof Sequential)) {
|
||
|
return false;
|
||
|
}
|
||
|
Sequential other = (Sequential) o;
|
||
|
return mDelays.equals(other.mDelays) && mEffects.equals(other.mEffects);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int hashCode() {
|
||
|
return Objects.hash(mEffects, mDelays);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String toString() {
|
||
|
return "Sequential{mEffects=" + mEffects + ", mDelays=" + mDelays + '}';
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public String toDebugString() {
|
||
|
StringJoiner sj = new StringJoiner(",", "Sequential{", "}");
|
||
|
for (int i = 0; i < mEffects.size(); i++) {
|
||
|
sj.add(String.format(Locale.ROOT, "delayMs=%d, effect=%s",
|
||
|
mDelays.get(i), mEffects.get(i).toDebugString()));
|
||
|
}
|
||
|
return sj.toString();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int describeContents() {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(@NonNull Parcel out, int flags) {
|
||
|
out.writeInt(PARCEL_TOKEN_SEQUENTIAL);
|
||
|
out.writeInt(mEffects.size());
|
||
|
for (int i = 0; i < mEffects.size(); i++) {
|
||
|
out.writeInt(mDelays.get(i));
|
||
|
mEffects.get(i).writeToParcel(out, flags);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@NonNull
|
||
|
public static final Parcelable.Creator<Sequential> CREATOR =
|
||
|
new Parcelable.Creator<Sequential>() {
|
||
|
@Override
|
||
|
public Sequential createFromParcel(@NonNull Parcel in) {
|
||
|
// Skip the type token
|
||
|
in.readInt();
|
||
|
return new Sequential(in);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
@NonNull
|
||
|
public Sequential[] newArray(int size) {
|
||
|
return new Sequential[size];
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
@NonNull
|
||
|
public static final Parcelable.Creator<CombinedVibration> CREATOR =
|
||
|
new Parcelable.Creator<CombinedVibration>() {
|
||
|
@Override
|
||
|
public CombinedVibration createFromParcel(Parcel in) {
|
||
|
int token = in.readInt();
|
||
|
if (token == PARCEL_TOKEN_MONO) {
|
||
|
return new Mono(in);
|
||
|
} else if (token == PARCEL_TOKEN_STEREO) {
|
||
|
return new Stereo(in);
|
||
|
} else if (token == PARCEL_TOKEN_SEQUENTIAL) {
|
||
|
return new Sequential(in);
|
||
|
} else {
|
||
|
throw new IllegalStateException(
|
||
|
"Unexpected combined vibration event type token in parcel.");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public CombinedVibration[] newArray(int size) {
|
||
|
return new CombinedVibration[size];
|
||
|
}
|
||
|
};
|
||
|
}
|