/* * 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(); /** * 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(); /** @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 boolean isHapticFeedbackCandidate() { return mEffect.isHapticFeedbackCandidate(); } /** @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 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 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 { // 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 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]; } }; }