content: /*
   * Copyright (C) 2017 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.IntDef;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.TestApi;
  import android.content.ContentResolver;
  import android.content.Context;
  import android.hardware.vibrator.V1_0.EffectStrength;
  import android.hardware.vibrator.V1_3.Effect;
  import android.net.Uri;
  import android.util.MathUtils;
  
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.Arrays;
  
  /**
   * A VibrationEffect describes a haptic effect to be performed by a {@link Vibrator}.
   *
   * These effects may be any number of things, from single shot vibrations to complex waveforms.
   */
  public abstract class VibrationEffect implements Parcelable {
      private static final int PARCEL_TOKEN_ONE_SHOT = 1;
      private static final int PARCEL_TOKEN_WAVEFORM = 2;
      private static final int PARCEL_TOKEN_EFFECT = 3;
  
      /**
       * The default vibration strength of the device.
       */
      public static final int DEFAULT_AMPLITUDE = -1;
  
      /**
       * The maximum amplitude value
       * @hide
       */
      public static final int MAX_AMPLITUDE = 255;
  
      /**
       * A click effect. Use this effect as a baseline, as it's the most common type of click effect.
       *
       * @see #get(int)
       */
      public static final int EFFECT_CLICK = Effect.CLICK;
  
      /**
       * A double click effect.
       *
       * @see #get(int)
       */
      public static final int EFFECT_DOUBLE_CLICK = Effect.DOUBLE_CLICK;
  
      /**
       * A tick effect. This effect is less strong compared to {@link #EFFECT_CLICK}.
       * @see #get(int)
       */
      public static final int EFFECT_TICK = Effect.TICK;
  
      /**
       * A thud effect.
       * @see #get(int)
       * @hide
       */
      @TestApi
      public static final int EFFECT_THUD = Effect.THUD;
  
      /**
       * A pop effect.
       * @see #get(int)
       * @hide
       */
      @TestApi
      public static final int EFFECT_POP = Effect.POP;
  
      /**
       * A heavy click effect. This effect is stronger than {@link #EFFECT_CLICK}.
       * @see #get(int)
       */
      public static final int EFFECT_HEAVY_CLICK = Effect.HEAVY_CLICK;
  
      /**
       * A texture effect meant to replicate soft ticks.
       *
       * Unlike normal effects, texture effects are meant to be called repeatedly, generally in
       * response to some motion, in order to replicate the feeling of some texture underneath the
       * user's fingers.
       *
       * @see #get(int)
       * @hide
       */
      @TestApi
      public static final int EFFECT_TEXTURE_TICK = Effect.TEXTURE_TICK;
  
      /** {@hide} */
      @TestApi
      public static final int EFFECT_STRENGTH_LIGHT = EffectStrength.LIGHT;
  
      /** {@hide} */
      @TestApi
      public static final int EFFECT_STRENGTH_MEDIUM = EffectStrength.MEDIUM;
  
      /** {@hide} */
      @TestApi
      public static final int EFFECT_STRENGTH_STRONG = EffectStrength.STRONG;
  
      /**
       * Ringtone patterns. They may correspond with the device's ringtone audio, or may just be a
       * pattern that can be played as a ringtone with any audio, depending on the device.
       *
       * @see #get(Uri, Context)
       * @hide
       */
      @TestApi
      public static final int[] RINGTONES = {
          Effect.RINGTONE_1,
          Effect.RINGTONE_2,
          Effect.RINGTONE_3,
          Effect.RINGTONE_4,
          Effect.RINGTONE_5,
          Effect.RINGTONE_6,
          Effect.RINGTONE_7,
          Effect.RINGTONE_8,
          Effect.RINGTONE_9,
          Effect.RINGTONE_10,
          Effect.RINGTONE_11,
          Effect.RINGTONE_12,
          Effect.RINGTONE_13,
          Effect.RINGTONE_14,
          Effect.RINGTONE_15
      };
  
      /** @hide */
      @IntDef(prefix = { "EFFECT_" }, value = {
              EFFECT_TICK,
              EFFECT_CLICK,
              EFFECT_HEAVY_CLICK,
              EFFECT_DOUBLE_CLICK,
      })
      @Retention(RetentionPolicy.SOURCE)
      public @interface EffectType {}
  
      /** @hide to prevent subclassing from outside of the framework */
      public VibrationEffect() { }
  
      /**
       * Create a one shot vibration.
       *
       * One shot vibrations will vibrate constantly for the specified period of time at the
       * specified amplitude, and then stop.
       *
       * @param milliseconds The number of milliseconds to vibrate. This must be a positive number.
       * @param amplitude The strength of the vibration. This must be a value between 1 and 255, or
       * {@link #DEFAULT_AMPLITUDE}.
       *
       * @return The desired effect.
       */
      public static VibrationEffect createOneShot(long milliseconds, int amplitude) {
          VibrationEffect effect = new OneShot(milliseconds, amplitude);
          effect.validate();
          return effect;
      }
  
      /**
       * Create a waveform vibration.
       *
       * Waveform vibrations are a potentially repeating series of timing and amplitude pairs. For
       * each pair, the value in the amplitude array determines the strength of the vibration and the
       * value in the timing array determines how long it vibrates for. An amplitude of 0 implies no
       * vibration (i.e. off), and any pairs with a timing value of 0 will be ignored.
       * <p>
       * The amplitude array of the generated waveform will be the same size as the given
       * timing array with alternating values of 0 (i.e. off) and {@link #DEFAULT_AMPLITUDE},
       * starting with 0. Therefore the first timing value will be the period to wait before turning
       * the vibrator on, the second value will be how long to vibrate at {@link #DEFAULT_AMPLITUDE}
       * strength, etc.
       * </p><p>
       * To cause the pattern to repeat, pass the index into the timings array at which to start the
       * repetition, or -1 to disable repeating.
       * </p>
       *
       * @param timings The pattern of alternating on-off timings, starting with off. Timing values
       *                of 0 will cause the timing / amplitude pair to be ignored.
       * @param repeat The index into the timings array at which to repeat, or -1 if you you don't
       *               want to repeat.
       *
       * @return The desired effect.
       */
      public static VibrationEffect createWaveform(long[] timings, int repeat) {
          int[] amplitudes = new int[timings.length];
          for (int i = 0; i < (timings.length / 2); i++) {
              amplitudes[i*2 + 1] = VibrationEffect.DEFAULT_AMPLITUDE;
          }
          return createWaveform(timings, amplitudes, repeat);
      }
  
      /**
       * Create a waveform vibration.
       *
       * Waveform vibrations are a potentially repeating series of timing and amplitude pairs. For
       * each pair, the value in the amplitude array determines the strength of the vibration and the
       * value in the timing array determines how long it vibrates for. An amplitude of 0 implies no
       * vibration (i.e. off), and any pairs with a timing value of 0 will be ignored.
       * </p><p>
       * To cause the pattern to repeat, pass the index into the timings array at which to start the
       * repetition, or -1 to disable repeating.
       * </p>
       *
       * @param timings The timing values of the timing / amplitude pairs. Timing values of 0
       *                will cause the pair to be ignored.
       * @param amplitudes The amplitude values of the timing / amplitude pairs. Amplitude values
       *                   must be between 0 and 255, or equal to {@link #DEFAULT_AMPLITUDE}. An
       *                   amplitude value of 0 implies the motor is off.
       * @param repeat The index into the timings array at which to repeat, or -1 if you you don't
       *               want to repeat.
       *
       * @return The desired effect.
       */
      public static VibrationEffect createWaveform(long[] timings, int[] amplitudes, int repeat) {
          VibrationEffect effect = new Waveform(timings, amplitudes, repeat);
          effect.validate();
          return effect;
      }
  
      /**
       * Create a predefined vibration effect.
       *
       * Predefined effects are a set of common vibration effects that should be identical, regardless
       * of the app they come from, in order to provide a cohesive experience for users across
       * the entire device. They also may be custom tailored to the device hardware in order to
       * provide a better experience than you could otherwise build using the generic building
       * blocks.
       *
       * This will fallback to a generic pattern if one exists and there does not exist a
       * hardware-specific implementation of the effect.
       *
       * @param effectId The ID of the effect to perform:
       *                 {@link #EFFECT_CLICK}, {@link #EFFECT_DOUBLE_CLICK}, {@link #EFFECT_TICK}
       *
       * @return The desired effect.
       */
      @NonNull
      public static VibrationEffect createPredefined(@EffectType int effectId) {
          return get(effectId, true);
      }
  
      /**
       * Get a predefined vibration effect.
       *
       * Predefined effects are a set of common vibration effects that should be identical, regardless
       * of the app they come from, in order to provide a cohesive experience for users across
       * the entire device. They also may be custom tailored to the device hardware in order to
       * provide a better experience than you could otherwise build using the generic building
       * blocks.
       *
       * This will fallback to a generic pattern if one exists and there does not exist a
       * hardware-specific implementation of the effect.
       *
       * @param effectId The ID of the effect to perform:
       *                 {@link #EFFECT_CLICK}, {@link #EFFECT_DOUBLE_CLICK}, {@link #EFFECT_TICK}
       *
       * @return The desired effect.
       * @hide
       */
      @TestApi
      public static VibrationEffect get(int effectId) {
          return get(effectId, true);
      }
  
      /**
       * Get a predefined vibration effect.
       *
       * Predefined effects are a set of common vibration effects that should be identical, regardless
       * of the app they come from, in order to provide a cohesive experience for users across
       * the entire device. They also may be custom tailored to the device hardware in order to
       * provide a better experience than you could otherwise build using the generic building
       * blocks.
       *
       * Some effects you may only want to play if there's a hardware specific implementation because
       * they may, for example, be too disruptive to the user without tuning. The {@code fallback}
       * parameter allows you to decide whether you want to fallback to the generic implementation or
       * only play if there's a tuned, hardware specific one available.
       *
       * @param effectId The ID of the effect to perform:
       *                 {@link #EFFECT_CLICK}, {@link #EFFECT_DOUBLE_CLICK}, {@link #EFFECT_TICK}
       * @param fallback Whether to fallback to a generic pattern if a hardware specific
       *                 implementation doesn't exist.
       *
       * @return The desired effect.
       * @hide
       */
      @TestApi
      public static VibrationEffect get(int effectId, boolean fallback) {
          VibrationEffect effect = new Prebaked(effectId, fallback);
          effect.validate();
          return effect;
      }
  
      /**
       * Get a predefined vibration effect associated with a given URI.
       *
       * Predefined effects are a set of common vibration effects that should be identical, regardless
       * of the app they come from, in order to provide a cohesive experience for users across
       * the entire device. They also may be custom tailored to the device hardware in order to
       * provide a better experience than you could otherwise build using the generic building
       * blocks.
       *
       * @param uri The URI associated with the haptic effect.
       * @param context The context used to get the URI to haptic effect association.
       *
       * @return The desired effect, or {@code null} if there's no associated effect.
       *
       * @hide
       */
      @TestApi
      @Nullable
      public static VibrationEffect get(Uri uri, Context context) {
          final ContentResolver cr = context.getContentResolver();
          Uri uncanonicalUri = cr.uncanonicalize(uri);
          if (uncanonicalUri == null) {
              // If we already had an uncanonical URI, it's possible we'll get null back here. In
              // this case, just use the URI as passed in since it wasn't canonicalized in the first
              // place.
              uncanonicalUri = uri;
          }
          String[] uris = context.getResources().getStringArray(
                  com.android.internal.R.array.config_ringtoneEffectUris);
          for (int i = 0; i < uris.length && i < RINGTONES.length; i++) {
              if (uris[i] == null) {
                  continue;
              }
              Uri mappedUri = cr.uncanonicalize(Uri.parse(uris[i]));
              if (mappedUri == null) {
                  continue;
              }
              if (mappedUri.equals(uncanonicalUri)) {
                  return get(RINGTONES[i]);
              }
          }
          return null;
      }
  
      @Override
      public int describeContents() {
          return 0;
      }
  
      /** @hide */
      public abstract void validate();
  
      /**
       * Gets the estimated duration of the vibration in milliseconds.
       *
       * For 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();
  
      /**
       * Scale the amplitude with the given constraints.
       *
       * This assumes that the previous value was in the range [0, MAX_AMPLITUDE]
       * @hide
       */
      @TestApi
      protected static int scale(int amplitude, float gamma, int maxAmplitude) {
          float val = MathUtils.pow(amplitude / (float) MAX_AMPLITUDE, gamma);
          return (int) (val * maxAmplitude);
      }
  
      /** @hide */
      @TestApi
      public static class OneShot extends VibrationEffect implements Parcelable {
          private final long mDuration;
          private final int mAmplitude;
  
          public OneShot(Parcel in) {
              mDuration = in.readLong();
              mAmplitude = in.readInt();
          }
  
          public OneShot(long milliseconds, int amplitude) {
              mDuration = milliseconds;
              mAmplitude = amplitude;
          }
  
          @Override
          public long getDuration() {
              return mDuration;
          }
  
          public int getAmplitude() {
              return mAmplitude;
          }
  
          /**
           * Scale the amplitude of this effect.
           *
           * @param gamma the gamma adjustment to apply
           * @param maxAmplitude the new maximum amplitude of the effect, must be between 0 and
           *         MAX_AMPLITUDE
           * @throws IllegalArgumentException if maxAmplitude less than 0 or more than MAX_AMPLITUDE
           *
           * @return A {@link OneShot} effect with the same timing but scaled amplitude.
           */
          public OneShot scale(float gamma, int maxAmplitude) {
              if (maxAmplitude > MAX_AMPLITUDE || maxAmplitude < 0) {
                  throw new IllegalArgumentException(
                          "Amplitude is negative or greater than MAX_AMPLITUDE");
              }
              int newAmplitude = scale(mAmplitude, gamma, maxAmplitude);
              return new OneShot(mDuration, newAmplitude);
          }
  
          /**
           * Resolve default values into integer amplitude numbers.
           *
           * @param defaultAmplitude the default amplitude to apply, must be between 0 and
           *         MAX_AMPLITUDE
           * @return A {@link OneShot} effect with same physical meaning but explicitly set amplitude
           *
           * @hide
           */
          public OneShot resolve(int defaultAmplitude) {
              if (defaultAmplitude > MAX_AMPLITUDE || defaultAmplitude < 0) {
                  throw new IllegalArgumentException(
                          "Amplitude is negative or greater than MAX_AMPLITUDE");
              }
              if (mAmplitude == DEFAULT_AMPLITUDE) {
                  return new OneShot(mDuration, defaultAmplitude);
              }
              return this;
          }
  
          @Override
          public void validate() {
              if (mAmplitude < -1 || mAmplitude == 0 || mAmplitude > 255) {
                  throw new IllegalArgumentException(
                          "amplitude must either be DEFAULT_AMPLITUDE, "
                          + "or between 1 and 255 inclusive (amplitude=" + mAmplitude + ")");
              }
              if (mDuration <= 0) {
                  throw new IllegalArgumentException(
                          "duration must be positive (duration=" + mDuration + ")");
              }
          }
  
          @Override
          public boolean equals(Object o) {
              if (!(o instanceof VibrationEffect.OneShot)) {
                  return false;
              }
              VibrationEffect.OneShot other = (VibrationEffect.OneShot) o;
              return other.mDuration == mDuration && other.mAmplitude == mAmplitude;
          }
  
          @Override
          public int hashCode() {
              int result = 17;
              result += 37 * (int) mDuration;
              result += 37 * mAmplitude;
              return result;
          }
  
          @Override
          public String toString() {
              return "OneShot{mDuration=" + mDuration + ", mAmplitude=" + mAmplitude + "}";
          }
  
          @Override
          public void writeToParcel(Parcel out, int flags) {
              out.writeInt(PARCEL_TOKEN_ONE_SHOT);
              out.writeLong(mDuration);
              out.writeInt(mAmplitude);
          }
  
          public static final @android.annotation.NonNull Parcelable.Creator<OneShot> CREATOR =
              new Parcelable.Creator<OneShot>() {
                  @Override
                  public OneShot createFromParcel(Parcel in) {
                      // Skip the type token
                      in.readInt();
                      return new OneShot(in);
                  }
                  @Override
                  public OneShot[] newArray(int size) {
                      return new OneShot[size];
                  }
              };
      }
  
      /** @hide */
      @TestApi
      public static class Waveform extends VibrationEffect implements Parcelable {
          private final long[] mTimings;
          private final int[] mAmplitudes;
          private final int mRepeat;
  
          public Waveform(Parcel in) {
              this(in.createLongArray(), in.createIntArray(), in.readInt());
          }
  
          public Waveform(long[] timings, int[] amplitudes, int repeat) {
              mTimings = new long[timings.length];
              System.arraycopy(timings, 0, mTimings, 0, timings.length);
              mAmplitudes = new int[amplitudes.length];
              System.arraycopy(amplitudes, 0, mAmplitudes, 0, amplitudes.length);
              mRepeat = repeat;
          }
  
          public long[] getTimings() {
              return mTimings;
          }
  
          public int[] getAmplitudes() {
              return mAmplitudes;
          }
  
          public int getRepeatIndex() {
              return mRepeat;
          }
  
          @Override
          public long getDuration() {
              if (mRepeat >= 0) {
                  return Long.MAX_VALUE;
              }
              long duration = 0;
              for (long d : mTimings) {
                  duration += d;
              }
              return duration;
          }
  
          /**
           * Scale the Waveform with the given gamma and new max amplitude.
           *
           * @param gamma the gamma adjustment to apply
           * @param maxAmplitude the new maximum amplitude of the effect, must be between 0 and
           *         MAX_AMPLITUDE
           * @throws IllegalArgumentException if maxAmplitude less than 0 or more than MAX_AMPLITUDE
           *
           * @return A {@link Waveform} effect with the same timings and repeat index
           *         but scaled amplitude.
           */
          public Waveform scale(float gamma, int maxAmplitude) {
              if (maxAmplitude > MAX_AMPLITUDE || maxAmplitude < 0) {
                  throw new IllegalArgumentException(
                          "Amplitude is negative or greater than MAX_AMPLITUDE");
              }
              if (gamma == 1.0f && maxAmplitude == MAX_AMPLITUDE) {
                  // Just return a copy of the original if there's no scaling to be done.
                  return new Waveform(mTimings, mAmplitudes, mRepeat);
              }
  
              int[] scaledAmplitudes = Arrays.copyOf(mAmplitudes, mAmplitudes.length);
              for (int i = 0; i < scaledAmplitudes.length; i++) {
                  scaledAmplitudes[i] = scale(scaledAmplitudes[i], gamma, maxAmplitude);
              }
              return new Waveform(mTimings, scaledAmplitudes, mRepeat);
          }
  
          /**
           * Resolve default values into integer amplitude numbers.
           *
           * @param defaultAmplitude the default amplitude to apply, must be between 0 and
           *         MAX_AMPLITUDE
           * @return A {@link Waveform} effect with same physical meaning but explicitly set
           *         amplitude
           *
           * @hide
           */
          public Waveform resolve(int defaultAmplitude) {
              if (defaultAmplitude > MAX_AMPLITUDE || defaultAmplitude < 0) {
                  throw new IllegalArgumentException(
                          "Amplitude is negative or greater than MAX_AMPLITUDE");
              }
              int[] resolvedAmplitudes = Arrays.copyOf(mAmplitudes, mAmplitudes.length);
              for (int i = 0; i < resolvedAmplitudes.length; i++) {
                  if (resolvedAmplitudes[i] == DEFAULT_AMPLITUDE) {
                      resolvedAmplitudes[i] = defaultAmplitude;
                  }
              }
              return new Waveform(mTimings, resolvedAmplitudes, mRepeat);
          }
  
          @Override
          public void validate() {
              if (mTimings.length != mAmplitudes.length) {
                  throw new IllegalArgumentException(
                          "timing and amplitude arrays must be of equal length"
                          + " (timings.length=" + mTimings.length
                          + ", amplitudes.length=" + mAmplitudes.length + ")");
              }
              if (!hasNonZeroEntry(mTimings)) {
                  throw new IllegalArgumentException("at least one timing must be non-zero"
                          + " (timings=" + Arrays.toString(mTimings) + ")");
              }
              for (long timing : mTimings) {
                  if (timing < 0) {
                      throw new IllegalArgumentException("timings must all be >= 0"
                              + " (timings=" + Arrays.toString(mTimings) + ")");
                  }
              }
              for (int amplitude : mAmplitudes) {
                  if (amplitude < -1 || amplitude > 255) {
                      throw new IllegalArgumentException(
                              "amplitudes must all be DEFAULT_AMPLITUDE or between 0 and 255"
                              + " (amplitudes=" + Arrays.toString(mAmplitudes) + ")");
                  }
              }
              if (mRepeat < -1 || mRepeat >= mTimings.length) {
                  throw new IllegalArgumentException(
                          "repeat index must be within the bounds of the timings array"
                          + " (timings.length=" + mTimings.length + ", index=" + mRepeat + ")");
              }
          }
  
          @Override
          public boolean equals(Object o) {
              if (!(o instanceof VibrationEffect.Waveform)) {
                  return false;
              }
              VibrationEffect.Waveform other = (VibrationEffect.Waveform) o;
              return Arrays.equals(mTimings, other.mTimings)
                  && Arrays.equals(mAmplitudes, other.mAmplitudes)
                  && mRepeat == other.mRepeat;
          }
  
          @Override
          public int hashCode() {
              int result = 17;
              result += 37 * Arrays.hashCode(mTimings);
              result += 37 * Arrays.hashCode(mAmplitudes);
              result += 37 * mRepeat;
              return result;
          }
  
          @Override
          public String toString() {
              return "Waveform{mTimings=" + Arrays.toString(mTimings)
                  + ", mAmplitudes=" + Arrays.toString(mAmplitudes)
                  + ", mRepeat=" + mRepeat
                  + "}";
          }
  
          @Override
          public void writeToParcel(Parcel out, int flags) {
              out.writeInt(PARCEL_TOKEN_WAVEFORM);
              out.writeLongArray(mTimings);
              out.writeIntArray(mAmplitudes);
              out.writeInt(mRepeat);
          }
  
          private static boolean hasNonZeroEntry(long[] vals) {
              for (long val : vals) {
                  if (val != 0) {
                      return true;
                  }
              }
              return false;
          }
  
  
          public static final @android.annotation.NonNull Parcelable.Creator<Waveform> CREATOR =
              new Parcelable.Creator<Waveform>() {
                  @Override
                  public Waveform createFromParcel(Parcel in) {
                      // Skip the type token
                      in.readInt();
                      return new Waveform(in);
                  }
                  @Override
                  public Waveform[] newArray(int size) {
                      return new Waveform[size];
                  }
              };
      }
  
      /** @hide */
      @TestApi
      public static class Prebaked extends VibrationEffect implements Parcelable {
          private final int mEffectId;
          private final boolean mFallback;
  
          private int mEffectStrength;
  
          public Prebaked(Parcel in) {
              this(in.readInt(), in.readByte() != 0);
              mEffectStrength = in.readInt();
          }
  
          public Prebaked(int effectId, boolean fallback) {
              mEffectId = effectId;
              mFallback = fallback;
              mEffectStrength = EffectStrength.MEDIUM;
          }
  
          public int getId() {
              return mEffectId;
          }
  
          /**
           * Whether the effect should fall back to a generic pattern if there's no hardware specific
           * implementation of it.
           */
          public boolean shouldFallback() {
              return mFallback;
          }
  
          @Override
          public long getDuration() {
              return -1;
          }
  
          /**
           * Set the effect strength of the prebaked effect.
           */
          public void setEffectStrength(int strength) {
              if (!isValidEffectStrength(strength)) {
                  throw new IllegalArgumentException("Invalid effect strength: " + strength);
              }
              mEffectStrength = strength;
          }
  
          /**
           * Set the effect strength.
           */
          public int getEffectStrength() {
              return mEffectStrength;
          }
  
          private static boolean isValidEffectStrength(int strength) {
              switch (strength) {
                  case EffectStrength.LIGHT:
                  case EffectStrength.MEDIUM:
                  case EffectStrength.STRONG:
                      return true;
                  default:
                      return false;
              }
          }
  
          @Override
          public void validate() {
              switch (mEffectId) {
                  case EFFECT_CLICK:
                  case EFFECT_DOUBLE_CLICK:
                  case EFFECT_TICK:
                  case EFFECT_TEXTURE_TICK:
                  case EFFECT_THUD:
                  case EFFECT_POP:
                  case EFFECT_HEAVY_CLICK:
                      break;
                  default:
                      if (mEffectId < RINGTONES[0] || mEffectId > RINGTONES[RINGTONES.length - 1]) {
                          throw new IllegalArgumentException(
                                  "Unknown prebaked effect type (value=" + mEffectId + ")");
                      }
              }
              if (!isValidEffectStrength(mEffectStrength)) {
                  throw new IllegalArgumentException(
                          "Unknown prebaked effect strength (value=" + mEffectStrength + ")");
              }
          }
  
          @Override
          public boolean equals(Object o) {
              if (!(o instanceof VibrationEffect.Prebaked)) {
                  return false;
              }
              VibrationEffect.Prebaked other = (VibrationEffect.Prebaked) o;
              return mEffectId == other.mEffectId
                  && mFallback == other.mFallback
                  && mEffectStrength == other.mEffectStrength;
          }
  
          @Override
          public int hashCode() {
              int result = 17;
              result += 37 * mEffectId;
              result += 37 * mEffectStrength;
              return result;
          }
  
          @Override
          public String toString() {
              return "Prebaked{mEffectId=" + mEffectId
                  + ", mEffectStrength=" + mEffectStrength
                  + ", mFallback=" + mFallback
                  + "}";
          }
  
  
          @Override
          public void writeToParcel(Parcel out, int flags) {
              out.writeInt(PARCEL_TOKEN_EFFECT);
              out.writeInt(mEffectId);
              out.writeByte((byte) (mFallback ? 1 : 0));
              out.writeInt(mEffectStrength);
          }
  
          public static final @NonNull Parcelable.Creator<Prebaked> CREATOR =
              new Parcelable.Creator<Prebaked>() {
                  @Override
                  public Prebaked createFromParcel(Parcel in) {
                      // Skip the type token
                      in.readInt();
                      return new Prebaked(in);
                  }
                  @Override
                  public Prebaked[] newArray(int size) {
                      return new Prebaked[size];
                  }
              };
      }
  
      public static final @NonNull Parcelable.Creator<VibrationEffect> CREATOR =
              new Parcelable.Creator<VibrationEffect>() {
                  @Override
                  public VibrationEffect createFromParcel(Parcel in) {
                      int token = in.readInt();
                      if (token == PARCEL_TOKEN_ONE_SHOT) {
                          return new OneShot(in);
                      } else if (token == PARCEL_TOKEN_WAVEFORM) {
                          return new Waveform(in);
                      } else if (token == PARCEL_TOKEN_EFFECT) {
                          return new Prebaked(in);
                      } else {
                          throw new IllegalStateException(
                                  "Unexpected vibration event type token in parcel.");
                      }
                  }
                  @Override
                  public VibrationEffect[] newArray(int size) {
                      return new VibrationEffect[size];
                  }
              };
  }
  