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