diff options
Diffstat (limited to 'core/java/android/os/CombinedVibration.java')
| -rw-r--r-- | core/java/android/os/CombinedVibration.java | 687 |
1 files changed, 687 insertions, 0 deletions
diff --git a/core/java/android/os/CombinedVibration.java b/core/java/android/os/CombinedVibration.java new file mode 100644 index 000000000000..aff55aff39f7 --- /dev/null +++ b/core/java/android/os/CombinedVibration.java @@ -0,0 +1,687 @@ +/* + * 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.Objects; + +/** + * 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(); + + /** @hide */ + public abstract void validate(); + + /** @hide */ + public abstract boolean hasVibrator(int vibratorId); + + /** + * 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 void validate() { + mEffect.validate(); + } + + /** @hide */ + @Override + public boolean hasVibrator(int vibratorId) { + return true; + } + + @Override + public boolean equals(Object o) { + 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 + '}'; + } + + @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 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 boolean hasVibrator(int vibratorId) { + return mEffects.indexOfKey(vibratorId) >= 0; + } + + @Override + public boolean equals(Object o) { + 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 + '}'; + } + + @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 { + 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 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 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 (!(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 + '}'; + } + + @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]; + } + }; +} |
