1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.os;
18 
19 import android.annotation.NonNull;
20 import android.annotation.TestApi;
21 import android.util.SparseArray;
22 
23 import com.android.internal.util.Preconditions;
24 
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.Objects;
28 
29 /**
30  * A CombinedVibration describes a combination of haptic effects to be performed by one or more
31  * {@link Vibrator Vibrators}.
32  *
33  * These effects may be any number of things, from single shot vibrations to complex waveforms.
34  *
35  * @see VibrationEffect
36  */
37 @SuppressWarnings({"ParcelNotFinal", "ParcelCreator"}) // Parcel only extended here.
38 public abstract class CombinedVibration implements Parcelable {
39     private static final int PARCEL_TOKEN_MONO = 1;
40     private static final int PARCEL_TOKEN_STEREO = 2;
41     private static final int PARCEL_TOKEN_SEQUENTIAL = 3;
42 
43     /** Prevent subclassing from outside of the framework. */
CombinedVibration()44     CombinedVibration() {
45     }
46 
47     /**
48      * Create a vibration that plays a single effect in parallel on all vibrators.
49      *
50      * A parallel vibration that takes a single {@link VibrationEffect} to be performed by multiple
51      * vibrators at the same time.
52      *
53      * @param effect The {@link VibrationEffect} to perform.
54      * @return The combined vibration representing the single effect to be played in all vibrators.
55      */
56     @NonNull
createParallel(@onNull VibrationEffect effect)57     public static CombinedVibration createParallel(@NonNull VibrationEffect effect) {
58         CombinedVibration combined = new Mono(effect);
59         combined.validate();
60         return combined;
61     }
62 
63     /**
64      * Start creating a vibration that plays effects in parallel on one or more vibrators.
65      *
66      * A parallel vibration takes one or more {@link VibrationEffect VibrationEffects} associated to
67      * individual vibrators to be performed at the same time.
68      *
69      * @see CombinedVibration.ParallelCombination
70      */
71     @NonNull
startParallel()72     public static ParallelCombination startParallel() {
73         return new ParallelCombination();
74     }
75 
76     /**
77      * Start creating a vibration that plays effects in sequence on one or more vibrators.
78      *
79      * A sequential vibration takes one or more {@link CombinedVibration CombinedVibrations} to be
80      * performed by one or more vibrators in order. Each {@link CombinedVibration} starts only after
81      * the previous one is finished.
82      *
83      * @hide
84      * @see CombinedVibration.SequentialCombination
85      */
86     @TestApi
87     @NonNull
startSequential()88     public static SequentialCombination startSequential() {
89         return new SequentialCombination();
90     }
91 
92     @Override
describeContents()93     public int describeContents() {
94         return 0;
95     }
96 
97     /**
98      * Gets the estimated duration of the combined vibration in milliseconds.
99      *
100      * <p>For parallel combinations this means the maximum duration of any individual {@link
101      * VibrationEffect}. For sequential combinations, this is a sum of each step and delays.
102      *
103      * <p>For combinations of effects without a defined end (e.g. a Waveform with a non-negative
104      * repeat index), this returns Long.MAX_VALUE. For effects with an unknown duration (e.g.
105      * Prebaked effects where the length is device and potentially run-time dependent), this returns
106      * -1.
107      *
108      * @hide
109      */
110     @TestApi
getDuration()111     public abstract long getDuration();
112 
113     /**
114      * Returns true if this effect could represent a touch haptic feedback.
115      *
116      * <p>It is strongly recommended that an instance of {@link VibrationAttributes} is specified
117      * for each vibration, with the correct usage. When a vibration is played with usage UNKNOWN,
118      * then this method will be used to classify the most common use case and make sure they are
119      * covered by the user settings for "Touch feedback".
120      *
121      * @hide
122      */
isHapticFeedbackCandidate()123     public boolean isHapticFeedbackCandidate() {
124         return false;
125     }
126 
127     /** @hide */
validate()128     public abstract void validate();
129 
130     /** @hide */
hasVibrator(int vibratorId)131     public abstract boolean hasVibrator(int vibratorId);
132 
133     /**
134      * A combination of haptic effects that should be played in multiple vibrators in parallel.
135      *
136      * @see CombinedVibration#startParallel()
137      */
138     public static final class ParallelCombination {
139 
140         private final SparseArray<VibrationEffect> mEffects = new SparseArray<>();
141 
ParallelCombination()142         ParallelCombination() {
143         }
144 
145         /**
146          * Add or replace a one shot vibration effect to be performed by the specified vibrator.
147          *
148          * @param vibratorId The id of the vibrator that should perform this effect.
149          * @param effect     The effect this vibrator should play.
150          * @return The {@link ParallelCombination} object to enable adding
151          * multiple effects in one chain.
152          * @see VibrationEffect#createOneShot(long, int)
153          */
154         @NonNull
addVibrator(int vibratorId, @NonNull VibrationEffect effect)155         public ParallelCombination addVibrator(int vibratorId, @NonNull VibrationEffect effect) {
156             mEffects.put(vibratorId, effect);
157             return this;
158         }
159 
160         /**
161          * Combine all of the added effects into a {@link CombinedVibration}.
162          *
163          * The {@link ParallelCombination} object is still valid after this
164          * call, so you can continue adding more effects to it and generating more
165          * {@link CombinedVibration}s by calling this method again.
166          *
167          * @return The {@link CombinedVibration} resulting from combining the added effects to
168          * be played in parallel.
169          */
170         @NonNull
combine()171         public CombinedVibration combine() {
172             if (mEffects.size() == 0) {
173                 throw new IllegalStateException(
174                         "Combination must have at least one element to combine.");
175             }
176             CombinedVibration combined = new Stereo(mEffects);
177             combined.validate();
178             return combined;
179         }
180     }
181 
182     /**
183      * A combination of haptic effects that should be played in multiple vibrators in sequence.
184      *
185      * @hide
186      * @see CombinedVibration#startSequential()
187      */
188     @TestApi
189     public static final class SequentialCombination {
190 
191         private final ArrayList<CombinedVibration> mEffects = new ArrayList<>();
192         private final ArrayList<Integer> mDelays = new ArrayList<>();
193 
SequentialCombination()194         SequentialCombination() {
195         }
196 
197         /**
198          * Add a single vibration effect to be performed next.
199          *
200          * Similar to {@link #addNext(int, VibrationEffect, int)}, but with no delay. The effect
201          * will start playing immediately after the previous vibration is finished.
202          *
203          * @param vibratorId The id of the vibrator that should perform this effect.
204          * @param effect     The effect this vibrator should play.
205          * @return The {@link CombinedVibration.SequentialCombination} object to enable adding
206          * multiple effects in one chain.
207          */
208         @NonNull
addNext(int vibratorId, @NonNull VibrationEffect effect)209         public SequentialCombination addNext(int vibratorId, @NonNull VibrationEffect effect) {
210             return addNext(vibratorId, effect, /* delay= */ 0);
211         }
212 
213         /**
214          * Add a single vibration effect to be performed next.
215          *
216          * The delay is applied immediately after the previous vibration is finished. The effect
217          * will start playing after the delay.
218          *
219          * @param vibratorId The id of the vibrator that should perform this effect.
220          * @param effect     The effect this vibrator should play.
221          * @param delay      The amount of time, in milliseconds, to wait between playing the prior
222          *                   vibration and this one, starting at the time the previous vibration in
223          *                   this sequence is finished.
224          * @return The {@link CombinedVibration.SequentialCombination} object to enable adding
225          * multiple effects in one chain.
226          */
227         @NonNull
addNext(int vibratorId, @NonNull VibrationEffect effect, int delay)228         public SequentialCombination addNext(int vibratorId, @NonNull VibrationEffect effect,
229                 int delay) {
230             return addNext(
231                     CombinedVibration.startParallel().addVibrator(vibratorId, effect).combine(),
232                     delay);
233         }
234 
235         /**
236          * Add a combined vibration effect to be performed next.
237          *
238          * Similar to {@link #addNext(CombinedVibration, int)}, but with no delay. The effect will
239          * start playing immediately after the previous vibration is finished.
240          *
241          * @param effect The combined effect to be performed next.
242          * @return The {@link CombinedVibration.SequentialCombination} object to enable adding
243          * multiple effects in one chain.
244          * @see VibrationEffect#createOneShot(long, int)
245          */
246         @NonNull
addNext(@onNull CombinedVibration effect)247         public SequentialCombination addNext(@NonNull CombinedVibration effect) {
248             return addNext(effect, /* delay= */ 0);
249         }
250 
251         /**
252          * Add a combined vibration effect to be performed next.
253          *
254          * The delay is applied immediately after the previous vibration is finished. The vibration
255          * will start playing after the delay.
256          *
257          * @param effect The combined effect to be performed next.
258          * @param delay  The amount of time, in milliseconds, to wait between playing the prior
259          *               vibration and this one, starting at the time the previous vibration in this
260          *               sequence is finished.
261          * @return The {@link CombinedVibration.SequentialCombination} object to enable adding
262          * multiple effects in one chain.
263          */
264         @NonNull
addNext(@onNull CombinedVibration effect, int delay)265         public SequentialCombination addNext(@NonNull CombinedVibration effect, int delay) {
266             if (effect instanceof Sequential) {
267                 Sequential sequentialEffect = (Sequential) effect;
268                 int firstEffectIndex = mDelays.size();
269                 mEffects.addAll(sequentialEffect.getEffects());
270                 mDelays.addAll(sequentialEffect.getDelays());
271                 mDelays.set(firstEffectIndex, delay + mDelays.get(firstEffectIndex));
272             } else {
273                 mEffects.add(effect);
274                 mDelays.add(delay);
275             }
276             return this;
277         }
278 
279         /**
280          * Combine all of the added effects in sequence.
281          *
282          * The {@link CombinedVibration.SequentialCombination} object is still valid after
283          * this call, so you can continue adding more effects to it and generating more {@link
284          * CombinedVibration}s by calling this method again.
285          *
286          * @return The {@link CombinedVibration} resulting from combining the added effects to
287          * be played in sequence.
288          */
289         @NonNull
combine()290         public CombinedVibration combine() {
291             if (mEffects.size() == 0) {
292                 throw new IllegalStateException(
293                         "Combination must have at least one element to combine.");
294             }
295             CombinedVibration combined = new Sequential(mEffects, mDelays);
296             combined.validate();
297             return combined;
298         }
299     }
300 
301     /**
302      * Represents a single {@link VibrationEffect} that should be played in all vibrators at the
303      * same time.
304      *
305      * @hide
306      */
307     @TestApi
308     public static final class Mono extends CombinedVibration {
309         private final VibrationEffect mEffect;
310 
Mono(Parcel in)311         Mono(Parcel in) {
312             mEffect = VibrationEffect.CREATOR.createFromParcel(in);
313         }
314 
Mono(@onNull VibrationEffect effect)315         Mono(@NonNull VibrationEffect effect) {
316             mEffect = effect;
317         }
318 
319         @NonNull
getEffect()320         public VibrationEffect getEffect() {
321             return mEffect;
322         }
323 
324         @Override
getDuration()325         public long getDuration() {
326             return mEffect.getDuration();
327         }
328 
329         /** @hide */
330         @Override
isHapticFeedbackCandidate()331         public boolean isHapticFeedbackCandidate() {
332             return mEffect.isHapticFeedbackCandidate();
333         }
334 
335         /** @hide */
336         @Override
validate()337         public void validate() {
338             mEffect.validate();
339         }
340 
341         /** @hide */
342         @Override
hasVibrator(int vibratorId)343         public boolean hasVibrator(int vibratorId) {
344             return true;
345         }
346 
347         @Override
equals(Object o)348         public boolean equals(Object o) {
349             if (!(o instanceof Mono)) {
350                 return false;
351             }
352             Mono other = (Mono) o;
353             return mEffect.equals(other.mEffect);
354         }
355 
356         @Override
hashCode()357         public int hashCode() {
358             return Objects.hash(mEffect);
359         }
360 
361         @Override
toString()362         public String toString() {
363             return "Mono{mEffect=" + mEffect + '}';
364         }
365 
366         @Override
describeContents()367         public int describeContents() {
368             return 0;
369         }
370 
371         @Override
writeToParcel(@onNull Parcel out, int flags)372         public void writeToParcel(@NonNull Parcel out, int flags) {
373             out.writeInt(PARCEL_TOKEN_MONO);
374             mEffect.writeToParcel(out, flags);
375         }
376 
377         @NonNull
378         public static final Parcelable.Creator<Mono> CREATOR =
379                 new Parcelable.Creator<Mono>() {
380                     @Override
381                     public Mono createFromParcel(@NonNull Parcel in) {
382                         // Skip the type token
383                         in.readInt();
384                         return new Mono(in);
385                     }
386 
387                     @Override
388                     @NonNull
389                     public Mono[] newArray(int size) {
390                         return new Mono[size];
391                     }
392                 };
393     }
394 
395     /**
396      * Represents a set of {@link VibrationEffect VibrationEffects} associated to individual
397      * vibrators that should be played at the same time.
398      *
399      * @hide
400      */
401     @TestApi
402     public static final class Stereo extends CombinedVibration {
403 
404         /** Mapping vibrator ids to effects. */
405         private final SparseArray<VibrationEffect> mEffects;
406 
Stereo(Parcel in)407         Stereo(Parcel in) {
408             int size = in.readInt();
409             mEffects = new SparseArray<>(size);
410             for (int i = 0; i < size; i++) {
411                 int vibratorId = in.readInt();
412                 mEffects.put(vibratorId, VibrationEffect.CREATOR.createFromParcel(in));
413             }
414         }
415 
Stereo(@onNull SparseArray<VibrationEffect> effects)416         Stereo(@NonNull SparseArray<VibrationEffect> effects) {
417             mEffects = new SparseArray<>(effects.size());
418             for (int i = 0; i < effects.size(); i++) {
419                 mEffects.put(effects.keyAt(i), effects.valueAt(i));
420             }
421         }
422 
423         /** Effects to be performed in parallel, where each key represents the vibrator id. */
424         @NonNull
getEffects()425         public SparseArray<VibrationEffect> getEffects() {
426             return mEffects;
427         }
428 
429         @Override
getDuration()430         public long getDuration() {
431             long maxDuration = Long.MIN_VALUE;
432             boolean hasUnknownStep = false;
433             for (int i = 0; i < mEffects.size(); i++) {
434                 long duration = mEffects.valueAt(i).getDuration();
435                 if (duration == Long.MAX_VALUE) {
436                     // If any duration is repeating, this combination duration is also repeating.
437                     return duration;
438                 }
439                 maxDuration = Math.max(maxDuration, duration);
440                 // If any step is unknown, this combination duration will also be unknown, unless
441                 // any step is repeating. Repeating vibrations take precedence over non-repeating
442                 // ones in the service, so continue looping to check for repeating steps.
443                 hasUnknownStep |= duration < 0;
444             }
445             if (hasUnknownStep) {
446                 // If any step is unknown, this combination duration is also unknown.
447                 return -1;
448             }
449             return maxDuration;
450         }
451 
452         /** @hide */
453         @Override
454         public boolean isHapticFeedbackCandidate() {
455             for (int i = 0; i < mEffects.size(); i++) {
456                 if (!mEffects.valueAt(i).isHapticFeedbackCandidate()) {
457                     return false;
458                 }
459             }
460             return true;
461         }
462 
463         /** @hide */
464         @Override
465         public void validate() {
466             Preconditions.checkArgument(mEffects.size() > 0,
467                     "There should be at least one effect set for a combined effect");
468             for (int i = 0; i < mEffects.size(); i++) {
469                 mEffects.valueAt(i).validate();
470             }
471         }
472 
473         /** @hide */
474         @Override
475         public boolean hasVibrator(int vibratorId) {
476             return mEffects.indexOfKey(vibratorId) >= 0;
477         }
478 
479         @Override
480         public boolean equals(Object o) {
481             if (!(o instanceof Stereo)) {
482                 return false;
483             }
484             Stereo other = (Stereo) o;
485             if (mEffects.size() != other.mEffects.size()) {
486                 return false;
487             }
488             for (int i = 0; i < mEffects.size(); i++) {
489                 if (!mEffects.valueAt(i).equals(other.mEffects.get(mEffects.keyAt(i)))) {
490                     return false;
491                 }
492             }
493             return true;
494         }
495 
496         @Override
497         public int hashCode() {
498             return mEffects.contentHashCode();
499         }
500 
501         @Override
502         public String toString() {
503             return "Stereo{mEffects=" + mEffects + '}';
504         }
505 
506         @Override
507         public int describeContents() {
508             return 0;
509         }
510 
511         @Override
512         public void writeToParcel(@NonNull Parcel out, int flags) {
513             out.writeInt(PARCEL_TOKEN_STEREO);
514             out.writeInt(mEffects.size());
515             for (int i = 0; i < mEffects.size(); i++) {
516                 out.writeInt(mEffects.keyAt(i));
517                 mEffects.valueAt(i).writeToParcel(out, flags);
518             }
519         }
520 
521         @NonNull
522         public static final Parcelable.Creator<Stereo> CREATOR =
523                 new Parcelable.Creator<Stereo>() {
524                     @Override
525                     public Stereo createFromParcel(@NonNull Parcel in) {
526                         // Skip the type token
527                         in.readInt();
528                         return new Stereo(in);
529                     }
530 
531                     @Override
532                     @NonNull
533                     public Stereo[] newArray(int size) {
534                         return new Stereo[size];
535                     }
536                 };
537     }
538 
539     /**
540      * Represents a list of {@link CombinedVibration CombinedVibrations} that should be played in
541      * sequence.
542      *
543      * @hide
544      */
545     @TestApi
546     public static final class Sequential extends CombinedVibration {
547         // If a vibration is playing more than 3 effects, it's probably not haptic feedback
548         private static final long MAX_HAPTIC_FEEDBACK_SEQUENCE_SIZE = 3;
549 
550         private final List<CombinedVibration> mEffects;
551         private final List<Integer> mDelays;
552 
553         Sequential(Parcel in) {
554             int size = in.readInt();
555             mEffects = new ArrayList<>(size);
556             mDelays = new ArrayList<>(size);
557             for (int i = 0; i < size; i++) {
558                 mDelays.add(in.readInt());
559                 mEffects.add(CombinedVibration.CREATOR.createFromParcel(in));
560             }
561         }
562 
563         Sequential(@NonNull List<CombinedVibration> effects,
564                 @NonNull List<Integer> delays) {
565             mEffects = new ArrayList<>(effects);
566             mDelays = new ArrayList<>(delays);
567         }
568 
569         /** Effects to be performed in sequence. */
570         @NonNull
571         public List<CombinedVibration> getEffects() {
572             return mEffects;
573         }
574 
575         /** Delay to be applied before each effect in {@link #getEffects()}. */
576         @NonNull
577         public List<Integer> getDelays() {
578             return mDelays;
579         }
580 
581         @Override
582         public long getDuration() {
583             boolean hasUnknownStep = false;
584             long durations = 0;
585             final int effectCount = mEffects.size();
586             for (int i = 0; i < effectCount; i++) {
587                 CombinedVibration effect = mEffects.get(i);
588                 long duration = effect.getDuration();
589                 if (duration == Long.MAX_VALUE) {
590                     // If any duration is repeating, this combination duration is also repeating.
591                     return duration;
592                 }
593                 durations += duration;
594                 // If any step is unknown, this combination duration will also be unknown, unless
595                 // any step is repeating. Repeating vibrations take precedence over non-repeating
596                 // ones in the service, so continue looping to check for repeating steps.
597                 hasUnknownStep |= duration < 0;
598             }
599             if (hasUnknownStep) {
600                 // If any step is unknown, this combination duration is also unknown.
601                 return -1;
602             }
603             long delays = 0;
604             for (int i = 0; i < effectCount; i++) {
605                 delays += mDelays.get(i);
606             }
607             return durations + delays;
608         }
609 
610         /** @hide */
611         @Override
612         public boolean isHapticFeedbackCandidate() {
613             final int effectCount = mEffects.size();
614             if (effectCount > MAX_HAPTIC_FEEDBACK_SEQUENCE_SIZE) {
615                 return false;
616             }
617             for (int i = 0; i < effectCount; i++) {
618                 if (!mEffects.get(i).isHapticFeedbackCandidate()) {
619                     return false;
620                 }
621             }
622             return true;
623         }
624 
625         /** @hide */
626         @Override
627         public void validate() {
628             Preconditions.checkArgument(mEffects.size() > 0,
629                     "There should be at least one effect set for a combined effect");
630             Preconditions.checkArgument(mEffects.size() == mDelays.size(),
631                     "Effect and delays should have equal length");
632             final int effectCount = mEffects.size();
633             for (int i = 0; i < effectCount; i++) {
634                 if (mDelays.get(i) < 0) {
635                     throw new IllegalArgumentException("Delays must all be >= 0"
636                             + " (delays=" + mDelays + ")");
637                 }
638             }
639             for (int i = 0; i < effectCount; i++) {
640                 CombinedVibration effect = mEffects.get(i);
641                 if (effect instanceof Sequential) {
642                     throw new IllegalArgumentException(
643                             "There should be no nested sequential effects in a combined effect");
644                 }
645                 effect.validate();
646             }
647         }
648 
649         /** @hide */
650         @Override
651         public boolean hasVibrator(int vibratorId) {
652             final int effectCount = mEffects.size();
653             for (int i = 0; i < effectCount; i++) {
654                 if (mEffects.get(i).hasVibrator(vibratorId)) {
655                     return true;
656                 }
657             }
658             return false;
659         }
660 
661         @Override
662         public boolean equals(Object o) {
663             if (!(o instanceof Sequential)) {
664                 return false;
665             }
666             Sequential other = (Sequential) o;
667             return mDelays.equals(other.mDelays) && mEffects.equals(other.mEffects);
668         }
669 
670         @Override
671         public int hashCode() {
672             return Objects.hash(mEffects, mDelays);
673         }
674 
675         @Override
676         public String toString() {
677             return "Sequential{mEffects=" + mEffects + ", mDelays=" + mDelays + '}';
678         }
679 
680         @Override
681         public int describeContents() {
682             return 0;
683         }
684 
685         @Override
686         public void writeToParcel(@NonNull Parcel out, int flags) {
687             out.writeInt(PARCEL_TOKEN_SEQUENTIAL);
688             out.writeInt(mEffects.size());
689             for (int i = 0; i < mEffects.size(); i++) {
690                 out.writeInt(mDelays.get(i));
691                 mEffects.get(i).writeToParcel(out, flags);
692             }
693         }
694 
695         @NonNull
696         public static final Parcelable.Creator<Sequential> CREATOR =
697                 new Parcelable.Creator<Sequential>() {
698                     @Override
699                     public Sequential createFromParcel(@NonNull Parcel in) {
700                         // Skip the type token
701                         in.readInt();
702                         return new Sequential(in);
703                     }
704 
705                     @Override
706                     @NonNull
707                     public Sequential[] newArray(int size) {
708                         return new Sequential[size];
709                     }
710                 };
711     }
712 
713     @NonNull
714     public static final Parcelable.Creator<CombinedVibration> CREATOR =
715             new Parcelable.Creator<CombinedVibration>() {
716                 @Override
717                 public CombinedVibration createFromParcel(Parcel in) {
718                     int token = in.readInt();
719                     if (token == PARCEL_TOKEN_MONO) {
720                         return new Mono(in);
721                     } else if (token == PARCEL_TOKEN_STEREO) {
722                         return new Stereo(in);
723                     } else if (token == PARCEL_TOKEN_SEQUENTIAL) {
724                         return new Sequential(in);
725                     } else {
726                         throw new IllegalStateException(
727                                 "Unexpected combined vibration event type token in parcel.");
728                     }
729                 }
730 
731                 @Override
732                 public CombinedVibration[] newArray(int size) {
733                     return new CombinedVibration[size];
734                 }
735             };
736 }
737