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.Locale; 28 import java.util.Objects; 29 import java.util.StringJoiner; 30 31 /** 32 * A CombinedVibration describes a combination of haptic effects to be performed by one or more 33 * {@link Vibrator Vibrators}. 34 * 35 * These effects may be any number of things, from single shot vibrations to complex waveforms. 36 * 37 * @see VibrationEffect 38 */ 39 @SuppressWarnings({"ParcelNotFinal", "ParcelCreator"}) // Parcel only extended here. 40 public abstract class CombinedVibration implements Parcelable { 41 private static final int PARCEL_TOKEN_MONO = 1; 42 private static final int PARCEL_TOKEN_STEREO = 2; 43 private static final int PARCEL_TOKEN_SEQUENTIAL = 3; 44 45 /** Prevent subclassing from outside of the framework. */ CombinedVibration()46 CombinedVibration() { 47 } 48 49 /** 50 * Create a vibration that plays a single effect in parallel on all vibrators. 51 * 52 * A parallel vibration that takes a single {@link VibrationEffect} to be performed by multiple 53 * vibrators at the same time. 54 * 55 * @param effect The {@link VibrationEffect} to perform. 56 * @return The combined vibration representing the single effect to be played in all vibrators. 57 */ 58 @NonNull createParallel(@onNull VibrationEffect effect)59 public static CombinedVibration createParallel(@NonNull VibrationEffect effect) { 60 CombinedVibration combined = new Mono(effect); 61 combined.validate(); 62 return combined; 63 } 64 65 /** 66 * Start creating a vibration that plays effects in parallel on one or more vibrators. 67 * 68 * A parallel vibration takes one or more {@link VibrationEffect VibrationEffects} associated to 69 * individual vibrators to be performed at the same time. 70 * 71 * @see CombinedVibration.ParallelCombination 72 */ 73 @NonNull startParallel()74 public static ParallelCombination startParallel() { 75 return new ParallelCombination(); 76 } 77 78 /** 79 * Start creating a vibration that plays effects in sequence on one or more vibrators. 80 * 81 * A sequential vibration takes one or more {@link CombinedVibration CombinedVibrations} to be 82 * performed by one or more vibrators in order. Each {@link CombinedVibration} starts only after 83 * the previous one is finished. 84 * 85 * @hide 86 * @see CombinedVibration.SequentialCombination 87 */ 88 @TestApi 89 @NonNull startSequential()90 public static SequentialCombination startSequential() { 91 return new SequentialCombination(); 92 } 93 94 @Override describeContents()95 public int describeContents() { 96 return 0; 97 } 98 99 /** 100 * Gets the estimated duration of the combined vibration in milliseconds. 101 * 102 * <p>For parallel combinations this means the maximum duration of any individual {@link 103 * VibrationEffect}. For sequential combinations, this is a sum of each step and delays. 104 * 105 * <p>For combinations of effects without a defined end (e.g. a Waveform with a non-negative 106 * repeat index), this returns Long.MAX_VALUE. For effects with an unknown duration (e.g. 107 * Prebaked effects where the length is device and potentially run-time dependent), this returns 108 * -1. 109 * 110 * @hide 111 */ 112 @TestApi getDuration()113 public abstract long getDuration(); 114 115 /** 116 * Returns true if this effect could represent a touch haptic feedback. 117 * 118 * <p>It is strongly recommended that an instance of {@link VibrationAttributes} is specified 119 * for each vibration, with the correct usage. When a vibration is played with usage UNKNOWN, 120 * then this method will be used to classify the most common use case and make sure they are 121 * covered by the user settings for "Touch feedback". 122 * 123 * @hide 124 */ isHapticFeedbackCandidate()125 public boolean isHapticFeedbackCandidate() { 126 return false; 127 } 128 129 /** @hide */ validate()130 public abstract void validate(); 131 132 /** 133 * Applies given effect transformation with a fixed parameter to each effect in this vibration. 134 * 135 * @param transformation The vibration effect transformation to be applied to all effects 136 * @param param The fixed parameter to be applied in all effect transformations 137 * @return the result of running the given transformation on all effects of this vibration 138 * @hide 139 */ transform( VibrationEffect.Transformation<ParamT> transformation, ParamT param)140 public abstract <ParamT> CombinedVibration transform( 141 VibrationEffect.Transformation<ParamT> transformation, ParamT param); 142 143 /** 144 * Applies given vibrator adapter to each effect in this combined vibration. 145 * 146 * @param adapter The vibrator adapter to be used on this vibration 147 * @return the result of running the given adapter on all effects of this vibration 148 * @hide 149 */ adapt(VibratorAdapter adapter)150 public abstract CombinedVibration adapt(VibratorAdapter adapter); 151 152 /** @hide */ hasVibrator(int vibratorId)153 public abstract boolean hasVibrator(int vibratorId); 154 155 /** 156 * Returns a compact version of the {@link #toString()} result for debugging purposes. 157 * 158 * @hide 159 */ toDebugString()160 public abstract String toDebugString(); 161 162 /** 163 * Adapts a {@link VibrationEffect} to a specific device vibrator using the ID. 164 * 165 * <p>This can be used for adapting effects to the capabilities of the specific device vibrator 166 * it's been mapped to by the combined vibration. 167 * 168 * @hide 169 */ 170 public interface VibratorAdapter { 171 172 /** 173 * Return the list of vibrator IDs available on the device, to be used by {@link 174 * CombinedVibration} to fan-out individual effects that aren't assigned to a specific 175 * vibrator. 176 */ getAvailableVibratorIds()177 int[] getAvailableVibratorIds(); 178 179 /** Adapts a {@link VibrationEffect} to a given vibrator. */ 180 @NonNull adaptToVibrator(int vibratorId, @NonNull VibrationEffect effect)181 VibrationEffect adaptToVibrator(int vibratorId, @NonNull VibrationEffect effect); 182 } 183 184 /** 185 * A combination of haptic effects that should be played in multiple vibrators in parallel. 186 * 187 * @see CombinedVibration#startParallel() 188 */ 189 public static final class ParallelCombination { 190 191 private final SparseArray<VibrationEffect> mEffects = new SparseArray<>(); 192 ParallelCombination()193 ParallelCombination() { 194 } 195 196 /** 197 * Add or replace a one shot vibration effect to be performed by the specified vibrator. 198 * 199 * @param vibratorId The id of the vibrator that should perform this effect. 200 * @param effect The effect this vibrator should play. 201 * @return The {@link ParallelCombination} object to enable adding 202 * multiple effects in one chain. 203 * @see VibrationEffect#createOneShot(long, int) 204 */ 205 @NonNull addVibrator(int vibratorId, @NonNull VibrationEffect effect)206 public ParallelCombination addVibrator(int vibratorId, @NonNull VibrationEffect effect) { 207 mEffects.put(vibratorId, effect); 208 return this; 209 } 210 211 /** 212 * Combine all of the added effects into a {@link CombinedVibration}. 213 * 214 * The {@link ParallelCombination} object is still valid after this 215 * call, so you can continue adding more effects to it and generating more 216 * {@link CombinedVibration}s by calling this method again. 217 * 218 * @return The {@link CombinedVibration} resulting from combining the added effects to 219 * be played in parallel. 220 */ 221 @NonNull combine()222 public CombinedVibration combine() { 223 if (mEffects.size() == 0) { 224 throw new IllegalStateException( 225 "Combination must have at least one element to combine."); 226 } 227 CombinedVibration combined = new Stereo(mEffects); 228 combined.validate(); 229 return combined; 230 } 231 } 232 233 /** 234 * A combination of haptic effects that should be played in multiple vibrators in sequence. 235 * 236 * @hide 237 * @see CombinedVibration#startSequential() 238 */ 239 @TestApi 240 public static final class SequentialCombination { 241 242 private final ArrayList<CombinedVibration> mEffects = new ArrayList<>(); 243 private final ArrayList<Integer> mDelays = new ArrayList<>(); 244 SequentialCombination()245 SequentialCombination() { 246 } 247 248 /** 249 * Add a single vibration effect to be performed next. 250 * 251 * Similar to {@link #addNext(int, VibrationEffect, int)}, but with no delay. The effect 252 * will start playing immediately after the previous vibration is finished. 253 * 254 * @param vibratorId The id of the vibrator that should perform this effect. 255 * @param effect The effect this vibrator should play. 256 * @return The {@link CombinedVibration.SequentialCombination} object to enable adding 257 * multiple effects in one chain. 258 */ 259 @NonNull addNext(int vibratorId, @NonNull VibrationEffect effect)260 public SequentialCombination addNext(int vibratorId, @NonNull VibrationEffect effect) { 261 return addNext(vibratorId, effect, /* delay= */ 0); 262 } 263 264 /** 265 * Add a single vibration effect to be performed next. 266 * 267 * The delay is applied immediately after the previous vibration is finished. The effect 268 * will start playing after the delay. 269 * 270 * @param vibratorId The id of the vibrator that should perform this effect. 271 * @param effect The effect this vibrator should play. 272 * @param delay The amount of time, in milliseconds, to wait between playing the prior 273 * vibration and this one, starting at the time the previous vibration in 274 * this sequence is finished. 275 * @return The {@link CombinedVibration.SequentialCombination} object to enable adding 276 * multiple effects in one chain. 277 */ 278 @NonNull addNext(int vibratorId, @NonNull VibrationEffect effect, int delay)279 public SequentialCombination addNext(int vibratorId, @NonNull VibrationEffect effect, 280 int delay) { 281 return addNext( 282 CombinedVibration.startParallel().addVibrator(vibratorId, effect).combine(), 283 delay); 284 } 285 286 /** 287 * Add a combined vibration effect to be performed next. 288 * 289 * Similar to {@link #addNext(CombinedVibration, int)}, but with no delay. The effect will 290 * start playing immediately after the previous vibration is finished. 291 * 292 * @param effect The combined effect to be performed next. 293 * @return The {@link CombinedVibration.SequentialCombination} object to enable adding 294 * multiple effects in one chain. 295 * @see VibrationEffect#createOneShot(long, int) 296 */ 297 @NonNull addNext(@onNull CombinedVibration effect)298 public SequentialCombination addNext(@NonNull CombinedVibration effect) { 299 return addNext(effect, /* delay= */ 0); 300 } 301 302 /** 303 * Add a combined vibration effect to be performed next. 304 * 305 * The delay is applied immediately after the previous vibration is finished. The vibration 306 * will start playing after the delay. 307 * 308 * @param effect The combined effect to be performed next. 309 * @param delay The amount of time, in milliseconds, to wait between playing the prior 310 * vibration and this one, starting at the time the previous vibration in this 311 * sequence is finished. 312 * @return The {@link CombinedVibration.SequentialCombination} object to enable adding 313 * multiple effects in one chain. 314 */ 315 @NonNull addNext(@onNull CombinedVibration effect, int delay)316 public SequentialCombination addNext(@NonNull CombinedVibration effect, int delay) { 317 if (effect instanceof Sequential) { 318 Sequential sequentialEffect = (Sequential) effect; 319 int firstEffectIndex = mDelays.size(); 320 mEffects.addAll(sequentialEffect.getEffects()); 321 mDelays.addAll(sequentialEffect.getDelays()); 322 mDelays.set(firstEffectIndex, delay + mDelays.get(firstEffectIndex)); 323 } else { 324 mEffects.add(effect); 325 mDelays.add(delay); 326 } 327 return this; 328 } 329 330 /** 331 * Combine all of the added effects in sequence. 332 * 333 * The {@link CombinedVibration.SequentialCombination} object is still valid after 334 * this call, so you can continue adding more effects to it and generating more {@link 335 * CombinedVibration}s by calling this method again. 336 * 337 * @return The {@link CombinedVibration} resulting from combining the added effects to 338 * be played in sequence. 339 */ 340 @NonNull combine()341 public CombinedVibration combine() { 342 if (mEffects.size() == 0) { 343 throw new IllegalStateException( 344 "Combination must have at least one element to combine."); 345 } 346 CombinedVibration combined = new Sequential(mEffects, mDelays); 347 combined.validate(); 348 return combined; 349 } 350 } 351 352 /** 353 * Represents a single {@link VibrationEffect} that should be played in all vibrators at the 354 * same time. 355 * 356 * @hide 357 */ 358 @TestApi 359 public static final class Mono extends CombinedVibration { 360 private final VibrationEffect mEffect; 361 Mono(Parcel in)362 Mono(Parcel in) { 363 mEffect = VibrationEffect.CREATOR.createFromParcel(in); 364 } 365 Mono(@onNull VibrationEffect effect)366 Mono(@NonNull VibrationEffect effect) { 367 mEffect = effect; 368 } 369 370 @NonNull getEffect()371 public VibrationEffect getEffect() { 372 return mEffect; 373 } 374 375 @Override getDuration()376 public long getDuration() { 377 return mEffect.getDuration(); 378 } 379 380 /** @hide */ 381 @Override isHapticFeedbackCandidate()382 public boolean isHapticFeedbackCandidate() { 383 return mEffect.isHapticFeedbackCandidate(); 384 } 385 386 /** @hide */ 387 @Override validate()388 public void validate() { 389 mEffect.validate(); 390 } 391 392 /** @hide */ 393 @Override transform( VibrationEffect.Transformation<ParamT> transformation, ParamT param)394 public <ParamT> CombinedVibration transform( 395 VibrationEffect.Transformation<ParamT> transformation, ParamT param) { 396 VibrationEffect newEffect = transformation.transform(mEffect, param); 397 if (mEffect.equals(newEffect)) { 398 return this; 399 } 400 // Make sure the validate methods are triggered 401 return CombinedVibration.createParallel(newEffect); 402 } 403 404 /** @hide */ 405 @Override adapt(VibratorAdapter adapter)406 public CombinedVibration adapt(VibratorAdapter adapter) { 407 ParallelCombination combination = CombinedVibration.startParallel(); 408 boolean hasSameEffects = true; 409 for (int vibratorId : adapter.getAvailableVibratorIds()) { 410 VibrationEffect newEffect = adapter.adaptToVibrator(vibratorId, mEffect); 411 combination.addVibrator(vibratorId, newEffect); 412 hasSameEffects &= mEffect.equals(newEffect); 413 } 414 if (hasSameEffects) { 415 return this; 416 } 417 // Make sure the validate methods are triggered 418 return combination.combine(); 419 } 420 421 /** @hide */ 422 @Override hasVibrator(int vibratorId)423 public boolean hasVibrator(int vibratorId) { 424 return true; 425 } 426 427 @Override equals(Object o)428 public boolean equals(Object o) { 429 if (this == o) { 430 return true; 431 } 432 if (!(o instanceof Mono)) { 433 return false; 434 } 435 Mono other = (Mono) o; 436 return mEffect.equals(other.mEffect); 437 } 438 439 @Override hashCode()440 public int hashCode() { 441 return Objects.hash(mEffect); 442 } 443 444 @Override toString()445 public String toString() { 446 return "Mono{mEffect=" + mEffect + '}'; 447 } 448 449 /** @hide */ 450 @Override toDebugString()451 public String toDebugString() { 452 // Simplify vibration string, use the single effect to represent it. 453 return mEffect.toDebugString(); 454 } 455 456 @Override describeContents()457 public int describeContents() { 458 return 0; 459 } 460 461 @Override writeToParcel(@onNull Parcel out, int flags)462 public void writeToParcel(@NonNull Parcel out, int flags) { 463 out.writeInt(PARCEL_TOKEN_MONO); 464 mEffect.writeToParcel(out, flags); 465 } 466 467 @NonNull 468 public static final Parcelable.Creator<Mono> CREATOR = 469 new Parcelable.Creator<Mono>() { 470 @Override 471 public Mono createFromParcel(@NonNull Parcel in) { 472 // Skip the type token 473 in.readInt(); 474 return new Mono(in); 475 } 476 477 @Override 478 @NonNull 479 public Mono[] newArray(int size) { 480 return new Mono[size]; 481 } 482 }; 483 } 484 485 /** 486 * Represents a set of {@link VibrationEffect VibrationEffects} associated to individual 487 * vibrators that should be played at the same time. 488 * 489 * @hide 490 */ 491 @TestApi 492 public static final class Stereo extends CombinedVibration { 493 494 /** Mapping vibrator ids to effects. */ 495 private final SparseArray<VibrationEffect> mEffects; 496 Stereo(Parcel in)497 Stereo(Parcel in) { 498 int size = in.readInt(); 499 mEffects = new SparseArray<>(size); 500 for (int i = 0; i < size; i++) { 501 int vibratorId = in.readInt(); 502 mEffects.put(vibratorId, VibrationEffect.CREATOR.createFromParcel(in)); 503 } 504 } 505 Stereo(@onNull SparseArray<VibrationEffect> effects)506 Stereo(@NonNull SparseArray<VibrationEffect> effects) { 507 mEffects = new SparseArray<>(effects.size()); 508 for (int i = 0; i < effects.size(); i++) { 509 mEffects.put(effects.keyAt(i), effects.valueAt(i)); 510 } 511 } 512 513 /** Effects to be performed in parallel, where each key represents the vibrator id. */ 514 @NonNull getEffects()515 public SparseArray<VibrationEffect> getEffects() { 516 return mEffects; 517 } 518 519 @Override getDuration()520 public long getDuration() { 521 long maxDuration = Long.MIN_VALUE; 522 boolean hasUnknownStep = false; 523 for (int i = 0; i < mEffects.size(); i++) { 524 long duration = mEffects.valueAt(i).getDuration(); 525 if (duration == Long.MAX_VALUE) { 526 // If any duration is repeating, this combination duration is also repeating. 527 return duration; 528 } 529 maxDuration = Math.max(maxDuration, duration); 530 // If any step is unknown, this combination duration will also be unknown, unless 531 // any step is repeating. Repeating vibrations take precedence over non-repeating 532 // ones in the service, so continue looping to check for repeating steps. 533 hasUnknownStep |= duration < 0; 534 } 535 if (hasUnknownStep) { 536 // If any step is unknown, this combination duration is also unknown. 537 return -1; 538 } 539 return maxDuration; 540 } 541 542 /** @hide */ 543 @Override 544 public boolean isHapticFeedbackCandidate() { 545 for (int i = 0; i < mEffects.size(); i++) { 546 if (!mEffects.valueAt(i).isHapticFeedbackCandidate()) { 547 return false; 548 } 549 } 550 return true; 551 } 552 553 /** @hide */ 554 @Override 555 public void validate() { 556 Preconditions.checkArgument(mEffects.size() > 0, 557 "There should be at least one effect set for a combined effect"); 558 for (int i = 0; i < mEffects.size(); i++) { 559 mEffects.valueAt(i).validate(); 560 } 561 } 562 563 /** @hide */ 564 @Override 565 public <ParamT> CombinedVibration transform( 566 VibrationEffect.Transformation<ParamT> transformation, ParamT param) { 567 ParallelCombination combination = CombinedVibration.startParallel(); 568 boolean hasSameEffects = true; 569 for (int i = 0; i < mEffects.size(); i++) { 570 int vibratorId = mEffects.keyAt(i); 571 VibrationEffect effect = mEffects.valueAt(i); 572 VibrationEffect newEffect = transformation.transform(effect, param); 573 combination.addVibrator(vibratorId, newEffect); 574 hasSameEffects &= effect.equals(newEffect); 575 } 576 if (hasSameEffects) { 577 return this; 578 } 579 // Make sure the validate methods are triggered 580 return combination.combine(); 581 } 582 583 /** @hide */ 584 @Override 585 public CombinedVibration adapt(VibratorAdapter adapter) { 586 ParallelCombination combination = CombinedVibration.startParallel(); 587 boolean hasSameEffects = true; 588 for (int i = 0; i < mEffects.size(); i++) { 589 int vibratorId = mEffects.keyAt(i); 590 VibrationEffect effect = mEffects.valueAt(i); 591 VibrationEffect newEffect = adapter.adaptToVibrator(vibratorId, effect); 592 combination.addVibrator(vibratorId, newEffect); 593 hasSameEffects &= effect.equals(newEffect); 594 } 595 if (hasSameEffects) { 596 return this; 597 } 598 // Make sure the validate methods are triggered 599 return combination.combine(); 600 } 601 602 /** @hide */ 603 @Override 604 public boolean hasVibrator(int vibratorId) { 605 return mEffects.indexOfKey(vibratorId) >= 0; 606 } 607 608 @Override 609 public boolean equals(Object o) { 610 if (this == o) { 611 return true; 612 } 613 if (!(o instanceof Stereo)) { 614 return false; 615 } 616 Stereo other = (Stereo) o; 617 if (mEffects.size() != other.mEffects.size()) { 618 return false; 619 } 620 for (int i = 0; i < mEffects.size(); i++) { 621 if (!mEffects.valueAt(i).equals(other.mEffects.get(mEffects.keyAt(i)))) { 622 return false; 623 } 624 } 625 return true; 626 } 627 628 @Override 629 public int hashCode() { 630 return mEffects.contentHashCode(); 631 } 632 633 @Override 634 public String toString() { 635 return "Stereo{mEffects=" + mEffects + '}'; 636 } 637 638 /** @hide */ 639 @Override 640 public String toDebugString() { 641 StringJoiner sj = new StringJoiner(",", "Stereo{", "}"); 642 for (int i = 0; i < mEffects.size(); i++) { 643 sj.add(String.format(Locale.ROOT, "vibrator(id=%d): %s", 644 mEffects.keyAt(i), mEffects.valueAt(i).toDebugString())); 645 } 646 return sj.toString(); 647 } 648 649 @Override 650 public int describeContents() { 651 return 0; 652 } 653 654 @Override 655 public void writeToParcel(@NonNull Parcel out, int flags) { 656 out.writeInt(PARCEL_TOKEN_STEREO); 657 out.writeInt(mEffects.size()); 658 for (int i = 0; i < mEffects.size(); i++) { 659 out.writeInt(mEffects.keyAt(i)); 660 mEffects.valueAt(i).writeToParcel(out, flags); 661 } 662 } 663 664 @NonNull 665 public static final Parcelable.Creator<Stereo> CREATOR = 666 new Parcelable.Creator<Stereo>() { 667 @Override 668 public Stereo createFromParcel(@NonNull Parcel in) { 669 // Skip the type token 670 in.readInt(); 671 return new Stereo(in); 672 } 673 674 @Override 675 @NonNull 676 public Stereo[] newArray(int size) { 677 return new Stereo[size]; 678 } 679 }; 680 } 681 682 /** 683 * Represents a list of {@link CombinedVibration CombinedVibrations} that should be played in 684 * sequence. 685 * 686 * @hide 687 */ 688 @TestApi 689 public static final class Sequential extends CombinedVibration { 690 // If a vibration is playing more than 3 effects, it's probably not haptic feedback 691 private static final long MAX_HAPTIC_FEEDBACK_SEQUENCE_SIZE = 3; 692 693 private final List<CombinedVibration> mEffects; 694 private final List<Integer> mDelays; 695 696 Sequential(Parcel in) { 697 int size = in.readInt(); 698 mEffects = new ArrayList<>(size); 699 mDelays = new ArrayList<>(size); 700 for (int i = 0; i < size; i++) { 701 mDelays.add(in.readInt()); 702 mEffects.add(CombinedVibration.CREATOR.createFromParcel(in)); 703 } 704 } 705 706 Sequential(@NonNull List<CombinedVibration> effects, 707 @NonNull List<Integer> delays) { 708 mEffects = new ArrayList<>(effects); 709 mDelays = new ArrayList<>(delays); 710 } 711 712 /** Effects to be performed in sequence. */ 713 @NonNull 714 public List<CombinedVibration> getEffects() { 715 return mEffects; 716 } 717 718 /** Delay to be applied before each effect in {@link #getEffects()}. */ 719 @NonNull 720 public List<Integer> getDelays() { 721 return mDelays; 722 } 723 724 @Override 725 public long getDuration() { 726 boolean hasUnknownStep = false; 727 long durations = 0; 728 final int effectCount = mEffects.size(); 729 for (int i = 0; i < effectCount; i++) { 730 CombinedVibration effect = mEffects.get(i); 731 long duration = effect.getDuration(); 732 if (duration == Long.MAX_VALUE) { 733 // If any duration is repeating, this combination duration is also repeating. 734 return duration; 735 } 736 durations += duration; 737 // If any step is unknown, this combination duration will also be unknown, unless 738 // any step is repeating. Repeating vibrations take precedence over non-repeating 739 // ones in the service, so continue looping to check for repeating steps. 740 hasUnknownStep |= duration < 0; 741 } 742 if (hasUnknownStep) { 743 // If any step is unknown, this combination duration is also unknown. 744 return -1; 745 } 746 long delays = 0; 747 for (int i = 0; i < effectCount; i++) { 748 delays += mDelays.get(i); 749 } 750 return durations + delays; 751 } 752 753 /** @hide */ 754 @Override 755 public boolean isHapticFeedbackCandidate() { 756 final int effectCount = mEffects.size(); 757 if (effectCount > MAX_HAPTIC_FEEDBACK_SEQUENCE_SIZE) { 758 return false; 759 } 760 for (int i = 0; i < effectCount; i++) { 761 if (!mEffects.get(i).isHapticFeedbackCandidate()) { 762 return false; 763 } 764 } 765 return true; 766 } 767 768 /** @hide */ 769 @Override 770 public void validate() { 771 Preconditions.checkArgument(mEffects.size() > 0, 772 "There should be at least one effect set for a combined effect"); 773 Preconditions.checkArgument(mEffects.size() == mDelays.size(), 774 "Effect and delays should have equal length"); 775 final int effectCount = mEffects.size(); 776 for (int i = 0; i < effectCount; i++) { 777 if (mDelays.get(i) < 0) { 778 throw new IllegalArgumentException("Delays must all be >= 0" 779 + " (delays=" + mDelays + ")"); 780 } 781 } 782 for (int i = 0; i < effectCount; i++) { 783 CombinedVibration effect = mEffects.get(i); 784 if (effect instanceof Sequential) { 785 throw new IllegalArgumentException( 786 "There should be no nested sequential effects in a combined effect"); 787 } 788 effect.validate(); 789 } 790 } 791 792 /** @hide */ 793 @Override 794 public <ParamT> CombinedVibration transform( 795 VibrationEffect.Transformation<ParamT> transformation, ParamT param) { 796 SequentialCombination combination = CombinedVibration.startSequential(); 797 boolean hasSameEffects = true; 798 for (int i = 0; i < mEffects.size(); i++) { 799 CombinedVibration vibration = mEffects.get(i); 800 CombinedVibration newVibration = vibration.transform(transformation, param); 801 combination.addNext(newVibration, mDelays.get(i)); 802 hasSameEffects &= vibration.equals(newVibration); 803 } 804 if (hasSameEffects) { 805 return this; 806 } 807 // Make sure the validate methods are triggered 808 return combination.combine(); 809 } 810 811 /** @hide */ 812 @Override 813 public CombinedVibration adapt(VibratorAdapter adapter) { 814 SequentialCombination combination = CombinedVibration.startSequential(); 815 boolean hasSameEffects = true; 816 for (int i = 0; i < mEffects.size(); i++) { 817 CombinedVibration vibration = mEffects.get(i); 818 CombinedVibration newVibration = vibration.adapt(adapter); 819 combination.addNext(newVibration, mDelays.get(i)); 820 hasSameEffects &= vibration.equals(newVibration); 821 } 822 if (hasSameEffects) { 823 return this; 824 } 825 // Make sure the validate methods are triggered 826 return combination.combine(); 827 } 828 829 /** @hide */ 830 @Override 831 public boolean hasVibrator(int vibratorId) { 832 final int effectCount = mEffects.size(); 833 for (int i = 0; i < effectCount; i++) { 834 if (mEffects.get(i).hasVibrator(vibratorId)) { 835 return true; 836 } 837 } 838 return false; 839 } 840 841 @Override 842 public boolean equals(Object o) { 843 if (this == o) { 844 return true; 845 } 846 if (!(o instanceof Sequential)) { 847 return false; 848 } 849 Sequential other = (Sequential) o; 850 return mDelays.equals(other.mDelays) && mEffects.equals(other.mEffects); 851 } 852 853 @Override 854 public int hashCode() { 855 return Objects.hash(mEffects, mDelays); 856 } 857 858 @Override 859 public String toString() { 860 return "Sequential{mEffects=" + mEffects + ", mDelays=" + mDelays + '}'; 861 } 862 863 /** @hide */ 864 @Override 865 public String toDebugString() { 866 StringJoiner sj = new StringJoiner(",", "Sequential{", "}"); 867 for (int i = 0; i < mEffects.size(); i++) { 868 sj.add(String.format(Locale.ROOT, "delayMs=%d, effect=%s", 869 mDelays.get(i), mEffects.get(i).toDebugString())); 870 } 871 return sj.toString(); 872 } 873 874 @Override 875 public int describeContents() { 876 return 0; 877 } 878 879 @Override 880 public void writeToParcel(@NonNull Parcel out, int flags) { 881 out.writeInt(PARCEL_TOKEN_SEQUENTIAL); 882 out.writeInt(mEffects.size()); 883 for (int i = 0; i < mEffects.size(); i++) { 884 out.writeInt(mDelays.get(i)); 885 mEffects.get(i).writeToParcel(out, flags); 886 } 887 } 888 889 @NonNull 890 public static final Parcelable.Creator<Sequential> CREATOR = 891 new Parcelable.Creator<Sequential>() { 892 @Override 893 public Sequential createFromParcel(@NonNull Parcel in) { 894 // Skip the type token 895 in.readInt(); 896 return new Sequential(in); 897 } 898 899 @Override 900 @NonNull 901 public Sequential[] newArray(int size) { 902 return new Sequential[size]; 903 } 904 }; 905 } 906 907 @NonNull 908 public static final Parcelable.Creator<CombinedVibration> CREATOR = 909 new Parcelable.Creator<CombinedVibration>() { 910 @Override 911 public CombinedVibration createFromParcel(Parcel in) { 912 int token = in.readInt(); 913 if (token == PARCEL_TOKEN_MONO) { 914 return new Mono(in); 915 } else if (token == PARCEL_TOKEN_STEREO) { 916 return new Stereo(in); 917 } else if (token == PARCEL_TOKEN_SEQUENTIAL) { 918 return new Sequential(in); 919 } else { 920 throw new IllegalStateException( 921 "Unexpected combined vibration event type token in parcel."); 922 } 923 } 924 925 @Override 926 public CombinedVibration[] newArray(int size) { 927 return new CombinedVibration[size]; 928 } 929 }; 930 } 931