From 601a944e4581f00c35bd491afff1868f56427cbf Mon Sep 17 00:00:00 2001 From: Lais Andrade Date: Thu, 4 Mar 2021 20:00:39 +0000 Subject: [PATCH] Introduce WaveformBuilder to VibrationEffect Create a new parcelable RampSegment and introduce VibrationEffect.startWaveform() method that returns a WaveformBuilder that allows creating step/ramp waveforms that changes amplitude and frequency of the effect. Implementations of VibrationThread and InputManagerService for now just ignore the new segments and keep existing behavior. Bug: 167947076 Test: VibrationEffectTest Change-Id: Ibac895b63196e779457efc297f1f5497a7814ae9 --- core/api/test-current.txt | 32 +++ core/java/android/os/VibrationEffect.java | 235 +++++++++++++++++- .../java/android/os/vibrator/RampSegment.java | 176 +++++++++++++ .../java/android/os/vibrator/StepSegment.java | 25 +- .../os/vibrator/VibrationEffectSegment.java | 6 +- .../vibrator/vibratormanagerservice.proto | 11 + .../src/android/os/VibrationEffectTest.java | 51 +++- .../android/os/vibrator/RampSegmentTest.java | 128 ++++++++++ .../android/os/vibrator/StepSegmentTest.java | 36 +-- .../android/server/vibrator/Vibration.java | 14 ++ .../FakeVibratorControllerProvider.java | 4 +- .../server/vibrator/VibrationThreadTest.java | 2 +- 12 files changed, 688 insertions(+), 32 deletions(-) create mode 100644 core/java/android/os/vibrator/RampSegment.java create mode 100644 core/tests/coretests/src/android/os/vibrator/RampSegmentTest.java diff --git a/core/api/test-current.txt b/core/api/test-current.txt index aba4ce7e05f44..bd45aecb6c7a6 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1665,6 +1665,7 @@ package android.os { method public static android.os.VibrationEffect get(int, boolean); method @Nullable public static android.os.VibrationEffect get(android.net.Uri, android.content.Context); method public abstract long getDuration(); + method @NonNull public static android.os.VibrationEffect.WaveformBuilder startWaveform(); field public static final int EFFECT_POP = 4; // 0x4 field public static final int EFFECT_STRENGTH_LIGHT = 0; // 0x0 field public static final int EFFECT_STRENGTH_MEDIUM = 1; // 0x1 @@ -1686,6 +1687,20 @@ package android.os { field @NonNull public static final android.os.Parcelable.Creator CREATOR; } + public static final class VibrationEffect.Composition { + method @NonNull public android.os.VibrationEffect.Composition addEffect(@NonNull android.os.VibrationEffect); + method @NonNull public android.os.VibrationEffect.Composition addEffect(@NonNull android.os.VibrationEffect, @IntRange(from=0) int); + } + + public static final class VibrationEffect.WaveformBuilder { + method @NonNull public android.os.VibrationEffect.WaveformBuilder addRamp(@FloatRange(from=0.0f, to=1.0f) float, @IntRange(from=0) int); + method @NonNull public android.os.VibrationEffect.WaveformBuilder addRamp(@FloatRange(from=0.0f, to=1.0f) float, @FloatRange(from=-1.0F, to=1.0f) float, @IntRange(from=0) int); + method @NonNull public android.os.VibrationEffect.WaveformBuilder addStep(@FloatRange(from=0.0f, to=1.0f) float, @IntRange(from=0) int); + method @NonNull public android.os.VibrationEffect.WaveformBuilder addStep(@FloatRange(from=0.0f, to=1.0f) float, @FloatRange(from=-1.0F, to=1.0f) float, @IntRange(from=0) int); + method @NonNull public android.os.VibrationEffect build(); + method @NonNull public android.os.VibrationEffect build(int); + } + public class VintfObject { method public static String[] getHalNamesAndVersions(); method public static String getSepolicyVersion(); @@ -1832,11 +1847,28 @@ package android.os.vibrator { field @NonNull public static final android.os.Parcelable.Creator CREATOR; } + public final class RampSegment extends android.os.vibrator.VibrationEffectSegment { + method @NonNull public android.os.vibrator.RampSegment applyEffectStrength(int); + method public int describeContents(); + method public long getDuration(); + method public float getEndAmplitude(); + method public float getEndFrequency(); + method public float getStartAmplitude(); + method public float getStartFrequency(); + method public boolean hasNonZeroAmplitude(); + method @NonNull public android.os.vibrator.RampSegment resolve(int); + method @NonNull public android.os.vibrator.RampSegment scale(float); + method public void validate(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + public final class StepSegment extends android.os.vibrator.VibrationEffectSegment { method @NonNull public android.os.vibrator.StepSegment applyEffectStrength(int); method public int describeContents(); method public float getAmplitude(); method public long getDuration(); + method public float getFrequency(); method public boolean hasNonZeroAmplitude(); method @NonNull public android.os.vibrator.StepSegment resolve(int); method @NonNull public android.os.vibrator.StepSegment scale(float); diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java index 95b5e856afec6..c78bf8c04c91a 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -21,6 +21,7 @@ import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.ContentResolver; @@ -30,6 +31,7 @@ import android.hardware.vibrator.V1_3.Effect; import android.net.Uri; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; +import android.os.vibrator.RampSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationEffectSegment; import android.util.MathUtils; @@ -250,7 +252,7 @@ public abstract class VibrationEffect implements Parcelable { for (int i = 0; i < timings.length; i++) { float parsedAmplitude = amplitudes[i] == DEFAULT_AMPLITUDE ? DEFAULT_AMPLITUDE : (float) amplitudes[i] / MAX_AMPLITUDE; - segments.add(new StepSegment(parsedAmplitude, (int) timings[i])); + segments.add(new StepSegment(parsedAmplitude, /* frequency= */ 0, (int) timings[i])); } VibrationEffect effect = new Composed(segments, repeat); effect.validate(); @@ -389,8 +391,26 @@ public abstract class VibrationEffect implements Parcelable { * @see VibrationEffect.Composition */ @NonNull - public static VibrationEffect.Composition startComposition() { - return new VibrationEffect.Composition(); + public static Composition startComposition() { + return new Composition(); + } + + /** + * Start building a waveform vibration. + * + *

The waveform builder offers more flexibility for creating waveform vibrations, allowing + * control over vibration frequency and ramping up or down the vibration amplitude, frequency or + * both. + * + *

For simpler waveform patterns see {@link #createWaveform} methods. + * + * @hide + * @see VibrationEffect.WaveformBuilder + */ + @TestApi + @NonNull + public static WaveformBuilder startWaveform() { + return new WaveformBuilder(); } @Override @@ -770,6 +790,42 @@ public abstract class VibrationEffect implements Parcelable { Composition() {} + /** + * Add a haptic effect to the end of the current composition. + * + *

Similar to {@link #addEffect(VibrationEffect, int)} , but with no delay applied. + * + * @param effect The effect to add to this composition as a primitive + * @return The {@link Composition} object to enable adding multiple primitives in one chain. + * @hide + */ + @TestApi + @NonNull + public Composition addEffect(@NonNull VibrationEffect effect) { + return addEffect(effect, /* delay= */ 0); + } + + /** + * Add a haptic effect to the end of the current composition. + * + * @param effect The effect to add to this composition as a primitive + * @param delay The amount of time in milliseconds to wait before playing this primitive + * @return The {@link Composition} object to enable adding multiple primitives in one chain. + * @hide + */ + @TestApi + @NonNull + public Composition addEffect(@NonNull VibrationEffect effect, + @IntRange(from = 0) int delay) { + Preconditions.checkArgumentNonnegative(delay); + if (delay > 0) { + // Created a segment sustaining the zero amplitude to represent the delay. + addSegment(new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, + /* duration= */ delay)); + } + return addSegments(effect); + } + /** * Add a haptic primitive to the end of the current composition. * @@ -829,6 +885,21 @@ public abstract class VibrationEffect implements Parcelable { return this; } + private Composition addSegments(VibrationEffect effect) { + if (mRepeatIndex >= 0) { + throw new IllegalStateException( + "Composition already have a repeating effect so any new primitive would be" + + " unreachable."); + } + Composed composed = (Composed) effect; + if (composed.getRepeatIndex() >= 0) { + // Start repeating from the index relative to the composed waveform. + mRepeatIndex = mSegments.size() + composed.getRepeatIndex(); + } + mSegments.addAll(composed.getSegments()); + return this; + } + /** * Compose all of the added primitives together into a single {@link VibrationEffect}. * @@ -881,6 +952,164 @@ public abstract class VibrationEffect implements Parcelable { } } + /** + * A builder for waveform haptic effects. + * + *

Waveform vibrations constitute of one or more timed segments where the vibration + * amplitude, frequency or both can linearly ramp to new values. + * + *

Waveform segments may have zero duration, which represent a jump to new vibration + * amplitude and/or frequency values. + * + *

Waveform segments may have the same start and end vibration amplitude and frequency, + * which represent a step where the amplitude and frequency are maintained for that duration. + * + * @hide + * @see VibrationEffect#startWaveform() + */ + @TestApi + public static final class WaveformBuilder { + private ArrayList mSegments = new ArrayList<>(); + + WaveformBuilder() {} + + /** + * Vibrate with given amplitude for the given duration, in millis, keeping the previous + * frequency the same. + * + *

If the duration is zero the vibrator will jump to new amplitude. + * + * @param amplitude The amplitude for this step + * @param duration The duration of this step in milliseconds + * @return The {@link WaveformBuilder} object to enable adding multiple steps in chain. + */ + @SuppressLint("MissingGetterMatchingBuilder") + @NonNull + public WaveformBuilder addStep(@FloatRange(from = 0f, to = 1f) float amplitude, + @IntRange(from = 0) int duration) { + return addStep(amplitude, getPreviousFrequency(), duration); + } + + /** + * Vibrate with given amplitude for the given duration, in millis, keeping the previous + * vibration frequency the same. + * + *

If the duration is zero the vibrator will jump to new amplitude. + * + * @param amplitude The amplitude for this step + * @param frequency The frequency for this step + * @param duration The duration of this step in milliseconds + * @return The {@link WaveformBuilder} object to enable adding multiple steps in chain. + */ + @SuppressLint("MissingGetterMatchingBuilder") + @NonNull + public WaveformBuilder addStep(@FloatRange(from = 0f, to = 1f) float amplitude, + @FloatRange(from = -1f, to = 1f) float frequency, + @IntRange(from = 0) int duration) { + mSegments.add(new StepSegment(amplitude, frequency, duration)); + return this; + } + + /** + * Ramp vibration linearly for the given duration, in millis, from previous amplitude value + * to the given one, keeping previous frequency. + * + *

If the duration is zero the vibrator will jump to new amplitude. + * + * @param amplitude The final amplitude this ramp should reach + * @param duration The duration of this ramp in milliseconds + * @return The {@link WaveformBuilder} object to enable adding multiple steps in chain. + */ + @SuppressLint("MissingGetterMatchingBuilder") + @NonNull + public WaveformBuilder addRamp(@FloatRange(from = 0f, to = 1f) float amplitude, + @IntRange(from = 0) int duration) { + return addRamp(amplitude, getPreviousFrequency(), duration); + } + + /** + * Ramp vibration linearly for the given duration, in millis, from previous amplitude and + * frequency values to the given ones. + * + *

If the duration is zero the vibrator will jump to new amplitude and frequency. + * + * @param amplitude The final amplitude this ramp should reach + * @param frequency The final frequency this ramp should reach + * @param duration The duration of this ramp in milliseconds + * @return The {@link WaveformBuilder} object to enable adding multiple steps in chain. + */ + @SuppressLint("MissingGetterMatchingBuilder") + @NonNull + public WaveformBuilder addRamp(@FloatRange(from = 0f, to = 1f) float amplitude, + @FloatRange(from = -1f, to = 1f) float frequency, + @IntRange(from = 0) int duration) { + mSegments.add(new RampSegment(getPreviousAmplitude(), amplitude, getPreviousFrequency(), + frequency, duration)); + return this; + } + + /** + * Compose all of the steps together into a single {@link VibrationEffect}. + * + * The {@link WaveformBuilder} object is still valid after this call, so you can + * continue adding more primitives to it and generating more {@link VibrationEffect}s by + * calling this method again. + * + * @return The {@link VibrationEffect} resulting from the composition of the steps. + */ + @NonNull + public VibrationEffect build() { + return build(/* repeat= */ -1); + } + + /** + * Compose all of the steps together into a single {@link VibrationEffect}. + * + *

To cause the pattern to repeat, pass the index at which to start the repetition + * (starting at 0), or -1 to disable repeating. + * + *

The {@link WaveformBuilder} object is still valid after this call, so you can + * continue adding more primitives to it and generating more {@link VibrationEffect}s by + * calling this method again. + * + * @return The {@link VibrationEffect} resulting from the composition of the steps. + */ + @NonNull + public VibrationEffect build(int repeat) { + if (mSegments.isEmpty()) { + throw new IllegalStateException( + "WaveformBuilder must have at least one element to build."); + } + VibrationEffect effect = new Composed(mSegments, repeat); + effect.validate(); + return effect; + } + + private float getPreviousFrequency() { + if (!mSegments.isEmpty()) { + VibrationEffectSegment segment = mSegments.get(mSegments.size() - 1); + if (segment instanceof StepSegment) { + return ((StepSegment) segment).getFrequency(); + } else if (segment instanceof RampSegment) { + return ((RampSegment) segment).getEndFrequency(); + } + } + return 0; + } + + private float getPreviousAmplitude() { + if (!mSegments.isEmpty()) { + VibrationEffectSegment segment = mSegments.get(mSegments.size() - 1); + if (segment instanceof StepSegment) { + return ((StepSegment) segment).getAmplitude(); + } else if (segment instanceof RampSegment) { + return ((RampSegment) segment).getEndAmplitude(); + } + } + return 0; + } + } + @NonNull public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { diff --git a/core/java/android/os/vibrator/RampSegment.java b/core/java/android/os/vibrator/RampSegment.java new file mode 100644 index 0000000000000..aad87c5cd4e73 --- /dev/null +++ b/core/java/android/os/vibrator/RampSegment.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2021 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.vibrator; + +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.os.Parcel; +import android.os.VibrationEffect; + +import com.android.internal.util.Preconditions; + +import java.util.Objects; + +/** + * Representation of {@link VibrationEffectSegment} that ramps vibration amplitude and/or frequency + * for a specified duration. + * + * @hide + */ +@TestApi +public final class RampSegment extends VibrationEffectSegment { + private final float mStartAmplitude; + private final float mStartFrequency; + private final float mEndAmplitude; + private final float mEndFrequency; + private final int mDuration; + + RampSegment(@NonNull Parcel in) { + this(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in.readInt()); + } + + /** @hide */ + public RampSegment(float startAmplitude, float endAmplitude, float startFrequency, + float endFrequency, int duration) { + mStartAmplitude = startAmplitude; + mEndAmplitude = endAmplitude; + mStartFrequency = startFrequency; + mEndFrequency = endFrequency; + mDuration = duration; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof RampSegment)) { + return false; + } + RampSegment other = (RampSegment) o; + return Float.compare(mStartAmplitude, other.mStartAmplitude) == 0 + && Float.compare(mEndAmplitude, other.mEndAmplitude) == 0 + && Float.compare(mStartFrequency, other.mStartFrequency) == 0 + && Float.compare(mEndFrequency, other.mEndFrequency) == 0 + && mDuration == other.mDuration; + } + + public float getStartAmplitude() { + return mStartAmplitude; + } + + public float getEndAmplitude() { + return mEndAmplitude; + } + + public float getStartFrequency() { + return mStartFrequency; + } + + public float getEndFrequency() { + return mEndFrequency; + } + + @Override + public long getDuration() { + return mDuration; + } + + @Override + public boolean hasNonZeroAmplitude() { + return mStartAmplitude > 0 || mEndAmplitude > 0; + } + + @Override + public void validate() { + Preconditions.checkArgumentNonnegative(mDuration, + "Durations must all be >= 0, got " + mDuration); + Preconditions.checkArgumentInRange(mStartAmplitude, 0f, 1f, "startAmplitude"); + Preconditions.checkArgumentInRange(mEndAmplitude, 0f, 1f, "endAmplitude"); + } + + + @NonNull + @Override + public RampSegment resolve(int defaultAmplitude) { + // Default amplitude is not supported for ramping. + return this; + } + + @NonNull + @Override + public RampSegment scale(float scaleFactor) { + float newStartAmplitude = VibrationEffect.scale(mStartAmplitude, scaleFactor); + float newEndAmplitude = VibrationEffect.scale(mEndAmplitude, scaleFactor); + if (Float.compare(mStartAmplitude, newStartAmplitude) == 0 + && Float.compare(mEndAmplitude, newEndAmplitude) == 0) { + return this; + } + return new RampSegment(newStartAmplitude, newEndAmplitude, mStartFrequency, mEndFrequency, + mDuration); + } + + @NonNull + @Override + public RampSegment applyEffectStrength(int effectStrength) { + return this; + } + + @Override + public int hashCode() { + return Objects.hash(mStartAmplitude, mEndAmplitude, mStartFrequency, mEndFrequency, + mDuration); + } + + @Override + public String toString() { + return "Ramp{startAmplitude=" + mStartAmplitude + + ", endAmplitude=" + mEndAmplitude + + ", startFrequency=" + mStartFrequency + + ", endFrequency=" + mEndFrequency + + ", duration=" + mDuration + + "}"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeInt(PARCEL_TOKEN_RAMP); + out.writeFloat(mStartAmplitude); + out.writeFloat(mEndAmplitude); + out.writeFloat(mStartFrequency); + out.writeFloat(mEndFrequency); + out.writeInt(mDuration); + } + + @NonNull + public static final Creator CREATOR = + new Creator() { + @Override + public RampSegment createFromParcel(Parcel in) { + // Skip the type token + in.readInt(); + return new RampSegment(in); + } + + @Override + public RampSegment[] newArray(int size) { + return new RampSegment[size]; + } + }; +} diff --git a/core/java/android/os/vibrator/StepSegment.java b/core/java/android/os/vibrator/StepSegment.java index 61a5d6cec9b13..11209e0ee425a 100644 --- a/core/java/android/os/vibrator/StepSegment.java +++ b/core/java/android/os/vibrator/StepSegment.java @@ -27,23 +27,25 @@ import com.android.internal.util.Preconditions; import java.util.Objects; /** - * Representation of {@link VibrationEffectSegment} that holds a fixed vibration amplitude for a - * specified duration. + * Representation of {@link VibrationEffectSegment} that holds a fixed vibration amplitude and + * frequency for a specified duration. * * @hide */ @TestApi public final class StepSegment extends VibrationEffectSegment { private final float mAmplitude; + private final float mFrequency; private final int mDuration; StepSegment(@NonNull Parcel in) { - this(in.readFloat(), in.readInt()); + this(in.readFloat(), in.readFloat(), in.readInt()); } /** @hide */ - public StepSegment(float amplitude, int duration) { + public StepSegment(float amplitude, float frequency, int duration) { mAmplitude = amplitude; + mFrequency = frequency; mDuration = duration; } @@ -54,6 +56,7 @@ public final class StepSegment extends VibrationEffectSegment { } StepSegment other = (StepSegment) o; return Float.compare(mAmplitude, other.mAmplitude) == 0 + && Float.compare(mFrequency, other.mFrequency) == 0 && mDuration == other.mDuration; } @@ -61,6 +64,10 @@ public final class StepSegment extends VibrationEffectSegment { return mAmplitude; } + public float getFrequency() { + return mFrequency; + } + @Override public long getDuration() { return mDuration; @@ -92,7 +99,8 @@ public final class StepSegment extends VibrationEffectSegment { if (Float.compare(mAmplitude, VibrationEffect.DEFAULT_AMPLITUDE) != 0) { return this; } - return new StepSegment((float) defaultAmplitude / VibrationEffect.MAX_AMPLITUDE, mDuration); + return new StepSegment((float) defaultAmplitude / VibrationEffect.MAX_AMPLITUDE, mFrequency, + mDuration); } @NonNull @@ -101,7 +109,8 @@ public final class StepSegment extends VibrationEffectSegment { if (Float.compare(mAmplitude, VibrationEffect.DEFAULT_AMPLITUDE) == 0) { return this; } - return new StepSegment(VibrationEffect.scale(mAmplitude, scaleFactor), mDuration); + return new StepSegment(VibrationEffect.scale(mAmplitude, scaleFactor), mFrequency, + mDuration); } @NonNull @@ -112,12 +121,13 @@ public final class StepSegment extends VibrationEffectSegment { @Override public int hashCode() { - return Objects.hash(mAmplitude, mDuration); + return Objects.hash(mAmplitude, mFrequency, mDuration); } @Override public String toString() { return "Step{amplitude=" + mAmplitude + + ", frequency=" + mFrequency + ", duration=" + mDuration + "}"; } @@ -131,6 +141,7 @@ public final class StepSegment extends VibrationEffectSegment { public void writeToParcel(@NonNull Parcel out, int flags) { out.writeInt(PARCEL_TOKEN_STEP); out.writeFloat(mAmplitude); + out.writeFloat(mFrequency); out.writeInt(mDuration); } diff --git a/core/java/android/os/vibrator/VibrationEffectSegment.java b/core/java/android/os/vibrator/VibrationEffectSegment.java index 3dc9e1252ecc6..5b42845cfa438 100644 --- a/core/java/android/os/vibrator/VibrationEffectSegment.java +++ b/core/java/android/os/vibrator/VibrationEffectSegment.java @@ -31,7 +31,8 @@ import android.os.VibrationEffect; *

    *
  1. A predefined vibration effect; *
  2. A composable effect primitive; - *
  3. Fixed amplitude value to be held for a specified duration; + *
  4. Fixed amplitude and frequency values to be held for a specified duration; + *
  5. Pairs of amplitude and frequency values to be ramped to for a specified duration; *
* * @hide @@ -42,6 +43,7 @@ public abstract class VibrationEffectSegment implements Parcelable { static final int PARCEL_TOKEN_PREBAKED = 1; static final int PARCEL_TOKEN_PRIMITIVE = 2; static final int PARCEL_TOKEN_STEP = 3; + static final int PARCEL_TOKEN_RAMP = 4; /** Prevent subclassing from outside of this package */ VibrationEffectSegment() { @@ -96,6 +98,8 @@ public abstract class VibrationEffectSegment implements Parcelable { switch (in.readInt()) { case PARCEL_TOKEN_STEP: return new StepSegment(in); + case PARCEL_TOKEN_RAMP: + return new RampSegment(in); case PARCEL_TOKEN_PREBAKED: return new PrebakedSegment(in); case PARCEL_TOKEN_PRIMITIVE: diff --git a/core/proto/android/server/vibrator/vibratormanagerservice.proto b/core/proto/android/server/vibrator/vibratormanagerservice.proto index 16c3ab098832b..7b97524d05105 100644 --- a/core/proto/android/server/vibrator/vibratormanagerservice.proto +++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto @@ -25,6 +25,16 @@ message StepSegmentProto { option (.android.msg_privacy).dest = DEST_AUTOMATIC; optional int32 duration = 1; optional float amplitude = 2; + optional float frequency = 3; +} + +message RampSegmentProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + optional int32 duration = 1; + optional float startAmplitude = 2; + optional float endAmplitude = 3; + optional float startFrequency = 4; + optional float endFrequency = 5; } message PrebakedSegmentProto { @@ -46,6 +56,7 @@ message SegmentProto { optional PrebakedSegmentProto prebaked = 1; optional PrimitiveSegmentProto primitive = 2; optional StepSegmentProto step = 3; + optional RampSegmentProto ramp = 4; } // A com.android.os.VibrationEffect object. diff --git a/core/tests/coretests/src/android/os/VibrationEffectTest.java b/core/tests/coretests/src/android/os/VibrationEffectTest.java index 242adab140a3a..009665fa1f53f 100644 --- a/core/tests/coretests/src/android/os/VibrationEffectTest.java +++ b/core/tests/coretests/src/android/os/VibrationEffectTest.java @@ -118,7 +118,16 @@ public class VibrationEffectTest { public void testValidateWaveform() { VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -1).validate(); VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, 0).validate(); + VibrationEffect.startWaveform() + .addStep(/* amplitude= */ 1, /* duration= */ 10) + .addRamp(/* amplitude= */ 0, /* duration= */ 20) + .addStep(/* amplitude= */ 1, /* frequency*/ 1, /* duration= */ 100) + .addRamp(/* amplitude= */ 0.5f, /* frequency*/ -1, /* duration= */ 50) + .build() + .validate(); + assertThrows(IllegalStateException.class, + () -> VibrationEffect.startWaveform().build().validate()); assertThrows(IllegalArgumentException.class, () -> VibrationEffect.createWaveform(new long[0], new int[0], -1).validate()); assertThrows(IllegalArgumentException.class, @@ -132,17 +141,31 @@ public class VibrationEffectTest { assertThrows(IllegalArgumentException.class, () -> VibrationEffect.createWaveform( TEST_TIMINGS, TEST_AMPLITUDES, TEST_TIMINGS.length).validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.startWaveform() + .addStep(/* amplitude= */ -2, 10).build().validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.startWaveform() + .addStep(1, /* duration= */ -1).build().validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.startWaveform() + .addStep(1, 0, /* duration= */ -1).build().validate()); } @Test public void testValidateComposed() { VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addEffect(TEST_ONE_SHOT) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f) + .addEffect(TEST_WAVEFORM, 100) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10) + .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) .compose() .validate(); + assertThrows(IllegalStateException.class, + () -> VibrationEffect.startComposition().compose().validate()); assertThrows(IllegalArgumentException.class, () -> VibrationEffect.startComposition().addPrimitive(-1).compose().validate()); assertThrows(IllegalArgumentException.class, @@ -150,6 +173,16 @@ public class VibrationEffectTest { .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, -1, 10) .compose() .validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, -10) + .compose() + .validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.startComposition() + .addEffect(TEST_ONE_SHOT, /* delay= */ -10) + .compose() + .validate()); assertThrows(IllegalArgumentException.class, () -> VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, -1) @@ -185,6 +218,12 @@ public class VibrationEffectTest { .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 1) .compose(); assertEquals(effect, effect.resolve(51)); + + VibrationEffect.Composed resolved = VibrationEffect.startComposition() + .addEffect(DEFAULT_ONE_SHOT) + .compose() + .resolve(51); + assertEquals(0.2f, ((StepSegment) resolved.getSegments().get(0)).getAmplitude()); } @Test @@ -215,6 +254,13 @@ public class VibrationEffectTest { .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 1) .compose(); assertEquals(effect, effect.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_LIGHT)); + + VibrationEffect.Composed applied = VibrationEffect.startComposition() + .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) + .compose() + .applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_LIGHT); + assertEquals(VibrationEffect.EFFECT_STRENGTH_LIGHT, + ((PrebakedSegment) applied.getSegments().get(0)).getEffectStrength()); } @Test @@ -251,13 +297,16 @@ public class VibrationEffectTest { VibrationEffect.Composed effect = (VibrationEffect.Composed) VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 1) + .addEffect(TEST_ONE_SHOT) .compose(); VibrationEffect.Composed scaledUp = effect.scale(1.5f); assertTrue(0.5f < ((PrimitiveSegment) scaledUp.getSegments().get(0)).getScale()); + assertTrue(100 / 255f < ((StepSegment) scaledUp.getSegments().get(1)).getAmplitude()); VibrationEffect.Composed scaledDown = effect.scale(0.5f); assertTrue(0.5f > ((PrimitiveSegment) scaledDown.getSegments().get(0)).getScale()); + assertTrue(100 / 255f > ((StepSegment) scaledDown.getSegments().get(1)).getAmplitude()); } private Resources mockRingtoneResources() { @@ -293,4 +342,4 @@ public class VibrationEffectTest { return context; } -} \ No newline at end of file +} diff --git a/core/tests/coretests/src/android/os/vibrator/RampSegmentTest.java b/core/tests/coretests/src/android/os/vibrator/RampSegmentTest.java new file mode 100644 index 0000000000000..174b4a76b3c37 --- /dev/null +++ b/core/tests/coretests/src/android/os/vibrator/RampSegmentTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2021 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.vibrator; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertSame; +import static junit.framework.Assert.assertTrue; + +import static org.testng.Assert.assertThrows; + +import android.os.Parcel; +import android.os.VibrationEffect; +import android.platform.test.annotations.Presubmit; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@Presubmit +@RunWith(MockitoJUnitRunner.class) +public class RampSegmentTest { + private static final float TOLERANCE = 1e-2f; + + @Test + public void testCreation() { + RampSegment ramp = new RampSegment(/* startAmplitude= */ 1, /* endAmplitude= */ 0, + /* StartFrequency= */ -1, /* endFrequency= */ 1, /* duration= */ 100); + + assertEquals(100L, ramp.getDuration()); + assertTrue(ramp.hasNonZeroAmplitude()); + assertEquals(1f, ramp.getStartAmplitude()); + assertEquals(0f, ramp.getEndAmplitude()); + assertEquals(-1f, ramp.getStartFrequency()); + assertEquals(1f, ramp.getEndFrequency()); + } + + @Test + public void testSerialization() { + RampSegment original = new RampSegment(0, 1, 0, 0.5f, 10); + Parcel parcel = Parcel.obtain(); + original.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + assertEquals(original, RampSegment.CREATOR.createFromParcel(parcel)); + } + + @Test + public void testValidate() { + new RampSegment(/* startAmplitude= */ 1, /* endAmplitude= */ 0, + /* StartFrequency= */ -1, /* endFrequency= */ 1, /* duration= */ 100).validate(); + + assertThrows(IllegalArgumentException.class, + () -> new RampSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0, 0, 0, 0).validate()); + assertThrows(IllegalArgumentException.class, + () -> new RampSegment(/* startAmplitude= */ -2, 0, 0, 0, 0).validate()); + assertThrows(IllegalArgumentException.class, + () -> new RampSegment(0, /* endAmplitude= */ 2, 0, 0, 0).validate()); + assertThrows(IllegalArgumentException.class, + () -> new RampSegment(0, 0, 0, 0, /* duration= */ -1).validate()); + } + + @Test + public void testHasNonZeroAmplitude() { + assertTrue(new RampSegment(0, 1, 0, 0, 0).hasNonZeroAmplitude()); + assertTrue(new RampSegment(0.01f, 0, 0, 0, 0).hasNonZeroAmplitude()); + assertFalse(new RampSegment(0, 0, 0, 0, 0).hasNonZeroAmplitude()); + } + + @Test + public void testResolve() { + RampSegment ramp = new RampSegment(0, 1, 0, 0, 0); + assertSame(ramp, ramp.resolve(100)); + } + + @Test + public void testApplyEffectStrength_ignoresAndReturnsSameEffect() { + RampSegment ramp = new RampSegment(1, 0, 1, 0, 0); + assertSame(ramp, ramp.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_STRONG)); + } + + @Test + public void testScale() { + RampSegment initial = new RampSegment(0, 1, 0, 0, 0); + + assertEquals(0f, initial.scale(1).getStartAmplitude(), TOLERANCE); + assertEquals(0f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE); + assertEquals(0f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE); + assertEquals(0f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE); + assertEquals(0f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE); + + assertEquals(1f, initial.scale(1).getEndAmplitude(), TOLERANCE); + assertEquals(0.34f, initial.scale(0.5f).getEndAmplitude(), TOLERANCE); + // The original value was not scaled up, so this only scales it down. + assertEquals(1f, initial.scale(1.5f).getEndAmplitude(), TOLERANCE); + assertEquals(0.53f, initial.scale(1.5f).scale(2 / 3f).getEndAmplitude(), TOLERANCE); + // Does not restore to the exact original value because scale up is a bit offset. + assertEquals(0.71f, initial.scale(0.8f).getEndAmplitude(), TOLERANCE); + assertEquals(0.84f, initial.scale(0.8f).scale(1.25f).getEndAmplitude(), TOLERANCE); + } + + @Test + public void testScale_halfPrimitiveScaleValue() { + RampSegment initial = new RampSegment(0.5f, 1, 0, 0, 0); + + assertEquals(0.5f, initial.scale(1).getStartAmplitude(), TOLERANCE); + assertEquals(0.17f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE); + // Does not restore to the exact original value because scale up is a bit offset. + assertEquals(0.86f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE); + assertEquals(0.47f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE); + // Does not restore to the exact original value because scale up is a bit offset. + assertEquals(0.35f, initial.scale(0.8f).getStartAmplitude(), TOLERANCE); + assertEquals(0.5f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE); + } +} diff --git a/core/tests/coretests/src/android/os/vibrator/StepSegmentTest.java b/core/tests/coretests/src/android/os/vibrator/StepSegmentTest.java index 188b6c166102b..79529b8cb13c3 100644 --- a/core/tests/coretests/src/android/os/vibrator/StepSegmentTest.java +++ b/core/tests/coretests/src/android/os/vibrator/StepSegmentTest.java @@ -38,16 +38,18 @@ public class StepSegmentTest { @Test public void testCreation() { - StepSegment step = new StepSegment(/* amplitude= */ 1f, /* duration= */ 100); + StepSegment step = new StepSegment(/* amplitude= */ 1f, /* frequency= */ -1f, + /* duration= */ 100); assertEquals(100, step.getDuration()); assertTrue(step.hasNonZeroAmplitude()); assertEquals(1f, step.getAmplitude()); + assertEquals(-1f, step.getFrequency()); } @Test public void testSerialization() { - StepSegment original = new StepSegment(0.5f, 10); + StepSegment original = new StepSegment(0.5f, 1f, 10); Parcel parcel = Parcel.obtain(); original.writeToParcel(parcel, 0); parcel.setDataPosition(0); @@ -56,31 +58,31 @@ public class StepSegmentTest { @Test public void testValidate() { - new StepSegment(/* amplitude= */ 0f, /* duration= */ 100).validate(); + new StepSegment(/* amplitude= */ 0f, /* frequency= */ -1f, /* duration= */ 100).validate(); assertThrows(IllegalArgumentException.class, - () -> new StepSegment(/* amplitude= */ -2, 10).validate()); + () -> new StepSegment(/* amplitude= */ -2, 1f, 10).validate()); assertThrows(IllegalArgumentException.class, - () -> new StepSegment(/* amplitude= */ 2, 10).validate()); + () -> new StepSegment(/* amplitude= */ 2, 1f, 10).validate()); assertThrows(IllegalArgumentException.class, - () -> new StepSegment(2, /* duration= */ -1).validate()); + () -> new StepSegment(2, 1f, /* duration= */ -1).validate()); } @Test public void testHasNonZeroAmplitude() { - assertTrue(new StepSegment(1f, 0).hasNonZeroAmplitude()); - assertTrue(new StepSegment(0.01f, 0).hasNonZeroAmplitude()); - assertTrue(new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0).hasNonZeroAmplitude()); - assertFalse(new StepSegment(0, 0).hasNonZeroAmplitude()); + assertTrue(new StepSegment(1f, 0, 0).hasNonZeroAmplitude()); + assertTrue(new StepSegment(0.01f, 0, 0).hasNonZeroAmplitude()); + assertTrue(new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0, 0).hasNonZeroAmplitude()); + assertFalse(new StepSegment(0, 0, 0).hasNonZeroAmplitude()); } @Test public void testResolve() { - StepSegment original = new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0); + StepSegment original = new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0, 0); assertEquals(1f, original.resolve(VibrationEffect.MAX_AMPLITUDE).getAmplitude()); assertEquals(0.2f, original.resolve(51).getAmplitude(), TOLERANCE); - StepSegment resolved = new StepSegment(0, 0); + StepSegment resolved = new StepSegment(0, 0, 0); assertSame(resolved, resolved.resolve(100)); assertThrows(IllegalArgumentException.class, () -> resolved.resolve(1000)); @@ -88,13 +90,13 @@ public class StepSegmentTest { @Test public void testApplyEffectStrength_ignoresAndReturnsSameEffect() { - StepSegment step = new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0); + StepSegment step = new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0, 0); assertSame(step, step.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_STRONG)); } @Test public void testScale_fullAmplitude() { - StepSegment initial = new StepSegment(1f, 0); + StepSegment initial = new StepSegment(1f, 0, 0); assertEquals(1f, initial.scale(1).getAmplitude(), TOLERANCE); assertEquals(0.34f, initial.scale(0.5f).getAmplitude(), TOLERANCE); @@ -108,7 +110,7 @@ public class StepSegmentTest { @Test public void testScale_halfAmplitude() { - StepSegment initial = new StepSegment(0.5f, 0); + StepSegment initial = new StepSegment(0.5f, 0, 0); assertEquals(0.5f, initial.scale(1).getAmplitude(), TOLERANCE); assertEquals(0.17f, initial.scale(0.5f).getAmplitude(), TOLERANCE); @@ -122,7 +124,7 @@ public class StepSegmentTest { @Test public void testScale_zeroAmplitude() { - StepSegment initial = new StepSegment(0, 0); + StepSegment initial = new StepSegment(0, 0, 0); assertEquals(0f, initial.scale(1).getAmplitude(), TOLERANCE); assertEquals(0f, initial.scale(0.5f).getAmplitude(), TOLERANCE); @@ -131,7 +133,7 @@ public class StepSegmentTest { @Test public void testScale_defaultAmplitude() { - StepSegment initial = new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0); + StepSegment initial = new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0, 0); assertEquals(VibrationEffect.DEFAULT_AMPLITUDE, initial.scale(1).getAmplitude(), TOLERANCE); assertEquals(VibrationEffect.DEFAULT_AMPLITUDE, initial.scale(0.5f).getAmplitude(), diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java index 1e897ea207ede..cd840589475a5 100644 --- a/services/core/java/com/android/server/vibrator/Vibration.java +++ b/services/core/java/com/android/server/vibrator/Vibration.java @@ -24,6 +24,7 @@ import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; +import android.os.vibrator.RampSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationEffectSegment; import android.util.SparseArray; @@ -345,6 +346,8 @@ final class Vibration { final long token = proto.start(fieldId); if (segment instanceof StepSegment) { dumpEffect(proto, SegmentProto.STEP, (StepSegment) segment); + } else if (segment instanceof RampSegment) { + dumpEffect(proto, SegmentProto.RAMP, (RampSegment) segment); } else if (segment instanceof PrebakedSegment) { dumpEffect(proto, SegmentProto.PREBAKED, (PrebakedSegment) segment); } else if (segment instanceof PrimitiveSegment) { @@ -357,6 +360,17 @@ final class Vibration { final long token = proto.start(fieldId); proto.write(StepSegmentProto.DURATION, segment.getDuration()); proto.write(StepSegmentProto.AMPLITUDE, segment.getAmplitude()); + proto.write(StepSegmentProto.FREQUENCY, segment.getFrequency()); + proto.end(token); + } + + private void dumpEffect(ProtoOutputStream proto, long fieldId, RampSegment segment) { + final long token = proto.start(fieldId); + proto.write(RampSegmentProto.DURATION, segment.getDuration()); + proto.write(RampSegmentProto.START_AMPLITUDE, segment.getStartAmplitude()); + proto.write(RampSegmentProto.END_AMPLITUDE, segment.getEndAmplitude()); + proto.write(RampSegmentProto.START_FREQUENCY, segment.getStartFrequency()); + proto.write(RampSegmentProto.END_FREQUENCY, segment.getEndFrequency()); proto.end(token); } diff --git a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java index a959a5e7274d9..1e3c344748209 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java @@ -75,8 +75,8 @@ final class FakeVibratorControllerProvider { @Override public void on(long milliseconds, long vibrationId) { - mEffectSegments.add( - new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, (int) milliseconds)); + mEffectSegments.add(new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, + /* frequency= */ 0, (int) milliseconds)); applyLatency(); scheduleListener(milliseconds, vibrationId); } diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java index 739a1a3c92f23..37e0ec2159d92 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java @@ -921,7 +921,7 @@ public class VibrationThreadTest { } private VibrationEffectSegment expectedOneShot(long millis) { - return new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, (int) millis); + return new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, /* frequency= */ 0, (int) millis); } private VibrationEffectSegment expectedPrebaked(int effectId) {