/* * 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. * *

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. * *

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. * *

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 CombinedVibration transform( VibrationEffect.Transformation 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. * *

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 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 mEffects = new ArrayList<>(); private final ArrayList 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 CombinedVibration transform( VibrationEffect.Transformation 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 CREATOR = new Parcelable.Creator() { @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 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 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 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 CombinedVibration transform( VibrationEffect.Transformation 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 CREATOR = new Parcelable.Creator() { @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 mEffects; private final List 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 effects, @NonNull List delays) { mEffects = new ArrayList<>(effects); mDelays = new ArrayList<>(delays); } /** Effects to be performed in sequence. */ @NonNull public List getEffects() { return mEffects; } /** Delay to be applied before each effect in {@link #getEffects()}. */ @NonNull public List 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 CombinedVibration transform( VibrationEffect.Transformation 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 CREATOR = new Parcelable.Creator() { @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 CREATOR = new Parcelable.Creator() { @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]; } }; }