Loading core/api/current.txt +8 −1 Original line number Diff line number Diff line Loading @@ -34849,6 +34849,13 @@ package android.os { field public static final int EFFECT_TICK = 2; // 0x2 } @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public static final class VibrationEffect.BasicEnvelopeBuilder { ctor public VibrationEffect.BasicEnvelopeBuilder(); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.BasicEnvelopeBuilder addControlPoint(@FloatRange(from=0, to=1) float, @FloatRange(from=0, to=1) float, long); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect build(); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.BasicEnvelopeBuilder setInitialSharpness(@FloatRange(from=0, to=1) float); } public static final class VibrationEffect.Composition { method @NonNull public android.os.VibrationEffect.Composition addPrimitive(int); method @NonNull public android.os.VibrationEffect.Composition addPrimitive(int, @FloatRange(from=0.0f, to=1.0f) float); Loading @@ -34869,7 +34876,7 @@ package android.os { @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public static final class VibrationEffect.WaveformEnvelopeBuilder { ctor public VibrationEffect.WaveformEnvelopeBuilder(); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.WaveformEnvelopeBuilder addControlPoint(@FloatRange(from=0, to=1) float, @FloatRange(from=0) float, int); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.WaveformEnvelopeBuilder addControlPoint(@FloatRange(from=0, to=1) float, @FloatRange(from=0) float, long); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect build(); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.WaveformEnvelopeBuilder setInitialFrequencyHz(@FloatRange(from=0) float); } core/api/test-current.txt +11 −0 Original line number Diff line number Diff line Loading @@ -2783,6 +2783,17 @@ package android.os.storage { package android.os.vibrator { @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public final class BasicPwleSegment extends android.os.vibrator.VibrationEffectSegment { method public int describeContents(); method public long getDuration(); method public float getEndIntensity(); method public float getEndSharpness(); method public float getStartIntensity(); method public float getStartSharpness(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.BasicPwleSegment> CREATOR; } public final class PrebakedSegment extends android.os.vibrator.VibrationEffectSegment { method public int describeContents(); method public long getDuration(); Loading core/java/android/os/VibrationEffect.java +175 −8 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.os; import static android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS; import android.annotation.DurationMillisLong; import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; Loading @@ -34,6 +35,7 @@ import android.hardware.vibrator.IVibrator; import android.hardware.vibrator.V1_0.EffectStrength; import android.hardware.vibrator.V1_3.Effect; import android.net.Uri; import android.os.vibrator.BasicPwleSegment; import android.os.vibrator.Flags; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; Loading Loading @@ -1846,6 +1848,8 @@ public abstract class VibrationEffect implements Parcelable { * .build(); * }</pre> * * <p>The builder automatically starts all effects at 0 amplitude. * * <p>It is crucial to ensure that the frequency range used in your effect is compatible with * the device's capabilities. The framework will not play any frequencies that fall partially * or completely outside the device's supported range. It will also not attempt to correct or Loading Loading @@ -1911,7 +1915,7 @@ public abstract class VibrationEffect implements Parcelable { firstSegment.getEndAmplitude(), initialFrequencyHz, // Update start frequency firstSegment.getEndFrequencyHz(), (int) firstSegment.getDuration())); firstSegment.getDuration())); } return this; Loading @@ -1934,24 +1938,25 @@ public abstract class VibrationEffect implements Parcelable { * {@link VibratorEnvelopeEffectInfo#getMinControlPointDurationMillis()}. * * @param amplitude The amplitude value between 0 and 1, inclusive. 0 represents the * vibrator being off, and 1 represents the maximum achievable amplitude * vibrator being off, and 1 represents the maximum achievable * amplitude * at this frequency. * @param frequencyHz The frequency in Hz, must be greater than zero. * @param timeMillis The transition time in milliseconds. * @param durationMillis The transition time in milliseconds. */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) @SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created. @NonNull public WaveformEnvelopeBuilder addControlPoint( @FloatRange(from = 0, to = 1) float amplitude, @FloatRange(from = 0) float frequencyHz, int timeMillis) { @FloatRange(from = 0) float frequencyHz, @DurationMillisLong long durationMillis) { if (Float.isNaN(mLastFrequencyHz)) { mLastFrequencyHz = frequencyHz; } mSegments.add(new PwleSegment(mLastAmplitude, amplitude, mLastFrequencyHz, frequencyHz, timeMillis)); durationMillis)); mLastAmplitude = amplitude; mLastFrequencyHz = frequencyHz; Loading Loading @@ -1982,6 +1987,168 @@ public abstract class VibrationEffect implements Parcelable { } } /** * A builder for waveform effects defined by their envelope, designed to provide a consistent * haptic perception across devices with varying capabilities. * * <p>This builder simplifies the creation of waveform effects by automatically adapting them * to different devices based on their capabilities. Effects are defined by control points * specifying target vibration intensity and sharpness, along with durations to reach those * targets. The vibrator will smoothly transition between these control points. * * <p><b>Intensity:</b> Defines the overall strength of the vibration, ranging from * 0 (off) to 1 (maximum achievable strength). Higher values result in stronger * vibrations. Supported intensity values guarantee sensitivity levels (SL) above * 10 dB SL to ensure human perception. * * <p><b>Sharpness:</b> Defines the crispness of the vibration, ranging from 0 to 1. * Lower values produce smoother vibrations, while higher values create a sharper, * more snappy sensation. Sharpness is mapped to its equivalent frequency within * the device's supported frequency range. * * <p>While this builder handles most of the adaptation logic, it does come with some * limitations: * <ul> * <li>It may not use the full range of frequencies</li> * <li>It's restricted to a frequency range that can generate output of at least 10 db * SL</li> * <li>Effects must end with a zero intensity control point. Failure to end at a zero * intensity control point will result in an {@link IllegalStateException}.</li> * </ul> * * <p>The builder automatically starts all effects at 0 intensity. * * <p>To avoid these limitations and to have more control over the effects output, use * {@link WaveformEnvelopeBuilder}, where direct amplitude and frequency values can be used. * * <p>For optimal cross-device consistency, it's recommended to limit the number of control * points to a maximum of 16. However this is not mandatory, and if a pattern exceeds the * maximum number of allowed control points, the framework will automatically break down the * effect to ensure it plays correctly. * * <p>For example, the following code creates a vibration effect that ramps up the intensity * from a low-pitched to a high-pitched strong vibration over 500ms and then ramps it down to * 0 (off) over 100ms: * * <pre>{@code * VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder() * .setInitialSharpness(0.0f) * .addControlPoint(1.0f, 1.0f, 500) * .addControlPoint(0.0f, 1.0f, 100) * .build(); * }</pre> */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public static final class BasicEnvelopeBuilder { private ArrayList<BasicPwleSegment> mSegments = new ArrayList<>(); private float mLastIntensity = 0f; private float mLastSharpness = Float.NaN; public BasicEnvelopeBuilder() {} /** * Sets the initial sharpness for the basic envelope effect. * * <p>The effect will start vibrating at this sharpness when it transitions to the * intensity and sharpness defined by the first control point. * * <p> The sharpness defines the crispness of the vibration, ranging from 0 to 1. Lower * values translate to smoother vibrations, while higher values create a sharper more snappy * sensation. This value is mapped to the supported frequency range of the device. * * @param initialSharpness The starting sharpness of the vibration in the range of [0, 1]. */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) @SuppressWarnings("MissingGetterMatchingBuilder")// No getter to initial sharpness once set. @NonNull public BasicEnvelopeBuilder setInitialSharpness( @FloatRange(from = 0, to = 1) float initialSharpness) { if (mSegments.isEmpty()) { mLastSharpness = initialSharpness; } else { BasicPwleSegment firstSegment = mSegments.getFirst(); mSegments.set(0, new BasicPwleSegment( firstSegment.getStartIntensity(), firstSegment.getEndIntensity(), initialSharpness, // Update start sharpness firstSegment.getEndSharpness(), firstSegment.getDuration())); } return this; } /** * Adds a new control point to the end of this waveform envelope. * * <p>Intensity defines the overall strength of the vibration, ranging from 0 (off) to 1 * (maximum achievable strength). Higher values translate to stronger vibrations. * * <p>Sharpness defines the crispness of the vibration, ranging from 0 to 1. Lower * values translate to smoother vibrations, while higher values create a sharper more snappy * sensation. This value is mapped to the supported frequency range of the device. * * <p>Time specifies the duration (in milliseconds) for the vibrator to smoothly transition * from the previous control point to this new one. It must be greater than zero. To * transition as quickly as possible, use * {@link VibratorEnvelopeEffectInfo#getMinControlPointDurationMillis()}. * * @param intensity The target vibration intensity, ranging from 0 (off) to 1 (maximum * strength). * @param sharpness The target sharpness, ranging from 0 (smoothest) to 1 (sharpest). * @param durationMillis The transition time in milliseconds. */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) @SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created. @NonNull public BasicEnvelopeBuilder addControlPoint( @FloatRange(from = 0, to = 1) float intensity, @FloatRange(from = 0, to = 1) float sharpness, @DurationMillisLong long durationMillis) { if (Float.isNaN(mLastSharpness)) { mLastSharpness = sharpness; } mSegments.add(new BasicPwleSegment(mLastIntensity, intensity, mLastSharpness, sharpness, durationMillis)); mLastIntensity = intensity; mLastSharpness = sharpness; return this; } /** * Build the waveform as a single {@link VibrationEffect}. * * <p>The {@link BasicEnvelopeBuilder} 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 list of control points. * @throws IllegalStateException if the last control point does not end at zero intensity. */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) @NonNull public VibrationEffect build() { if (mSegments.isEmpty()) { throw new IllegalStateException( "BasicEnvelopeBuilder must have at least one control point to build."); } if (mSegments.getLast().getEndIntensity() != 0) { throw new IllegalStateException( "Basic envelope effects must end at a zero intensity control point."); } VibrationEffect effect = new Composed(mSegments, /* repeatIndex= */ -1); effect.validate(); return effect; } } /** * A builder for waveform haptic effects. * Loading core/java/android/os/vibrator/BasicPwleSegment.java 0 → 100644 +222 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.FlaggedApi; import android.annotation.NonNull; import android.annotation.TestApi; import android.os.Parcel; import android.os.VibrationEffect; import android.os.VibratorInfo; import com.android.internal.util.Preconditions; import java.util.Locale; import java.util.Objects; /** * A {@link VibrationEffectSegment} that represents a smooth transition from the starting * intensity and sharpness to new values over a specified duration. * * <p>The intensity and sharpness are expressed by float values in the range [0, 1], where * intensity represents the user-perceived strength of the vibration, while sharpness represents * the crispness of the vibration. * * @hide */ @TestApi @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public final class BasicPwleSegment extends VibrationEffectSegment { private final float mStartIntensity; private final float mEndIntensity; private final float mStartSharpness; private final float mEndSharpness; private final long mDuration; BasicPwleSegment(@NonNull Parcel in) { this(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in.readLong()); } /** @hide */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public BasicPwleSegment(float startIntensity, float endIntensity, float startSharpness, float endSharpness, long duration) { mStartIntensity = startIntensity; mEndIntensity = endIntensity; mStartSharpness = startSharpness; mEndSharpness = endSharpness; mDuration = duration; } public float getStartIntensity() { return mStartIntensity; } public float getEndIntensity() { return mEndIntensity; } public float getStartSharpness() { return mStartSharpness; } public float getEndSharpness() { return mEndSharpness; } @Override public long getDuration() { return mDuration; } @Override public boolean equals(Object o) { if (!(o instanceof BasicPwleSegment)) { return false; } BasicPwleSegment other = (BasicPwleSegment) o; return Float.compare(mStartIntensity, other.mStartIntensity) == 0 && Float.compare(mEndIntensity, other.mEndIntensity) == 0 && Float.compare(mStartSharpness, other.mStartSharpness) == 0 && Float.compare(mEndSharpness, other.mEndSharpness) == 0 && mDuration == other.mDuration; } /** @hide */ @Override public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) { return vibratorInfo.areEnvelopeEffectsSupported(); } /** @hide */ @Override public boolean isHapticFeedbackCandidate() { return true; } /** @hide */ @Override public void validate() { Preconditions.checkArgumentInRange(mStartSharpness, 0f, 1f, "startSharpness"); Preconditions.checkArgumentInRange(mEndSharpness, 0f, 1f, "endSharpness"); Preconditions.checkArgumentInRange(mStartIntensity, 0f, 1f, "startIntensity"); Preconditions.checkArgumentInRange(mEndIntensity, 0f, 1f, "endIntensity"); Preconditions.checkArgumentPositive(mDuration, "Time must be greater than zero."); } /** @hide */ @NonNull @Override public BasicPwleSegment resolve(int defaultAmplitude) { return this; } /** @hide */ @NonNull @Override public BasicPwleSegment scale(float scaleFactor) { float newStartIntensity = VibrationEffect.scale(mStartIntensity, scaleFactor); float newEndIntensity = VibrationEffect.scale(mEndIntensity, scaleFactor); if (Float.compare(mStartIntensity, newStartIntensity) == 0 && Float.compare(mEndIntensity, newEndIntensity) == 0) { return this; } return new BasicPwleSegment(newStartIntensity, newEndIntensity, mStartSharpness, mEndSharpness, mDuration); } /** @hide */ @NonNull @Override public BasicPwleSegment scaleLinearly(float scaleFactor) { float newStartIntensity = VibrationEffect.scaleLinearly(mStartIntensity, scaleFactor); float newEndIntensity = VibrationEffect.scaleLinearly(mEndIntensity, scaleFactor); if (Float.compare(mStartIntensity, newStartIntensity) == 0 && Float.compare(mEndIntensity, newEndIntensity) == 0) { return this; } return new BasicPwleSegment(newStartIntensity, newEndIntensity, mStartSharpness, mEndSharpness, mDuration); } /** @hide */ @NonNull @Override public BasicPwleSegment applyEffectStrength(int effectStrength) { return this; } @Override public int hashCode() { return Objects.hash(mStartIntensity, mEndIntensity, mStartSharpness, mEndSharpness, mDuration); } @Override public String toString() { return "BasicPwle{startIntensity=" + mStartIntensity + ", endIntensity=" + mEndIntensity + ", startSharpness=" + mStartSharpness + ", endSharpness=" + mEndSharpness + ", duration=" + mDuration + "}"; } /** @hide */ @Override public String toDebugString() { return String.format(Locale.US, "Pwle=%dms(intensity=%.2f @ %.2f to %.2f @ %.2f)", mDuration, mStartIntensity, mStartSharpness, mEndIntensity, mEndSharpness); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(PARCEL_TOKEN_PWLE); dest.writeFloat(mStartIntensity); dest.writeFloat(mEndIntensity); dest.writeFloat(mStartSharpness); dest.writeFloat(mEndSharpness); dest.writeLong(mDuration); } @NonNull public static final Creator<BasicPwleSegment> CREATOR = new Creator<BasicPwleSegment>() { @Override public BasicPwleSegment createFromParcel(Parcel in) { // Skip the type token in.readInt(); return new BasicPwleSegment(in); } @Override public BasicPwleSegment[] newArray(int size) { return new BasicPwleSegment[size]; } }; } core/java/android/os/vibrator/PwlePoint.java +8 −0 Original line number Diff line number Diff line Loading @@ -63,4 +63,12 @@ public final class PwlePoint { public int hashCode() { return Objects.hash(mAmplitude, mFrequencyHz, mTimeMillis); } @Override public String toString() { return "PwlePoint{amplitude=" + mAmplitude + ", frequency=" + mFrequencyHz + ", time=" + mTimeMillis + "}"; } } Loading
core/api/current.txt +8 −1 Original line number Diff line number Diff line Loading @@ -34849,6 +34849,13 @@ package android.os { field public static final int EFFECT_TICK = 2; // 0x2 } @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public static final class VibrationEffect.BasicEnvelopeBuilder { ctor public VibrationEffect.BasicEnvelopeBuilder(); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.BasicEnvelopeBuilder addControlPoint(@FloatRange(from=0, to=1) float, @FloatRange(from=0, to=1) float, long); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect build(); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.BasicEnvelopeBuilder setInitialSharpness(@FloatRange(from=0, to=1) float); } public static final class VibrationEffect.Composition { method @NonNull public android.os.VibrationEffect.Composition addPrimitive(int); method @NonNull public android.os.VibrationEffect.Composition addPrimitive(int, @FloatRange(from=0.0f, to=1.0f) float); Loading @@ -34869,7 +34876,7 @@ package android.os { @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public static final class VibrationEffect.WaveformEnvelopeBuilder { ctor public VibrationEffect.WaveformEnvelopeBuilder(); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.WaveformEnvelopeBuilder addControlPoint(@FloatRange(from=0, to=1) float, @FloatRange(from=0) float, int); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.WaveformEnvelopeBuilder addControlPoint(@FloatRange(from=0, to=1) float, @FloatRange(from=0) float, long); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect build(); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.WaveformEnvelopeBuilder setInitialFrequencyHz(@FloatRange(from=0) float); }
core/api/test-current.txt +11 −0 Original line number Diff line number Diff line Loading @@ -2783,6 +2783,17 @@ package android.os.storage { package android.os.vibrator { @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public final class BasicPwleSegment extends android.os.vibrator.VibrationEffectSegment { method public int describeContents(); method public long getDuration(); method public float getEndIntensity(); method public float getEndSharpness(); method public float getStartIntensity(); method public float getStartSharpness(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.BasicPwleSegment> CREATOR; } public final class PrebakedSegment extends android.os.vibrator.VibrationEffectSegment { method public int describeContents(); method public long getDuration(); Loading
core/java/android/os/VibrationEffect.java +175 −8 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.os; import static android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS; import android.annotation.DurationMillisLong; import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; Loading @@ -34,6 +35,7 @@ import android.hardware.vibrator.IVibrator; import android.hardware.vibrator.V1_0.EffectStrength; import android.hardware.vibrator.V1_3.Effect; import android.net.Uri; import android.os.vibrator.BasicPwleSegment; import android.os.vibrator.Flags; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; Loading Loading @@ -1846,6 +1848,8 @@ public abstract class VibrationEffect implements Parcelable { * .build(); * }</pre> * * <p>The builder automatically starts all effects at 0 amplitude. * * <p>It is crucial to ensure that the frequency range used in your effect is compatible with * the device's capabilities. The framework will not play any frequencies that fall partially * or completely outside the device's supported range. It will also not attempt to correct or Loading Loading @@ -1911,7 +1915,7 @@ public abstract class VibrationEffect implements Parcelable { firstSegment.getEndAmplitude(), initialFrequencyHz, // Update start frequency firstSegment.getEndFrequencyHz(), (int) firstSegment.getDuration())); firstSegment.getDuration())); } return this; Loading @@ -1934,24 +1938,25 @@ public abstract class VibrationEffect implements Parcelable { * {@link VibratorEnvelopeEffectInfo#getMinControlPointDurationMillis()}. * * @param amplitude The amplitude value between 0 and 1, inclusive. 0 represents the * vibrator being off, and 1 represents the maximum achievable amplitude * vibrator being off, and 1 represents the maximum achievable * amplitude * at this frequency. * @param frequencyHz The frequency in Hz, must be greater than zero. * @param timeMillis The transition time in milliseconds. * @param durationMillis The transition time in milliseconds. */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) @SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created. @NonNull public WaveformEnvelopeBuilder addControlPoint( @FloatRange(from = 0, to = 1) float amplitude, @FloatRange(from = 0) float frequencyHz, int timeMillis) { @FloatRange(from = 0) float frequencyHz, @DurationMillisLong long durationMillis) { if (Float.isNaN(mLastFrequencyHz)) { mLastFrequencyHz = frequencyHz; } mSegments.add(new PwleSegment(mLastAmplitude, amplitude, mLastFrequencyHz, frequencyHz, timeMillis)); durationMillis)); mLastAmplitude = amplitude; mLastFrequencyHz = frequencyHz; Loading Loading @@ -1982,6 +1987,168 @@ public abstract class VibrationEffect implements Parcelable { } } /** * A builder for waveform effects defined by their envelope, designed to provide a consistent * haptic perception across devices with varying capabilities. * * <p>This builder simplifies the creation of waveform effects by automatically adapting them * to different devices based on their capabilities. Effects are defined by control points * specifying target vibration intensity and sharpness, along with durations to reach those * targets. The vibrator will smoothly transition between these control points. * * <p><b>Intensity:</b> Defines the overall strength of the vibration, ranging from * 0 (off) to 1 (maximum achievable strength). Higher values result in stronger * vibrations. Supported intensity values guarantee sensitivity levels (SL) above * 10 dB SL to ensure human perception. * * <p><b>Sharpness:</b> Defines the crispness of the vibration, ranging from 0 to 1. * Lower values produce smoother vibrations, while higher values create a sharper, * more snappy sensation. Sharpness is mapped to its equivalent frequency within * the device's supported frequency range. * * <p>While this builder handles most of the adaptation logic, it does come with some * limitations: * <ul> * <li>It may not use the full range of frequencies</li> * <li>It's restricted to a frequency range that can generate output of at least 10 db * SL</li> * <li>Effects must end with a zero intensity control point. Failure to end at a zero * intensity control point will result in an {@link IllegalStateException}.</li> * </ul> * * <p>The builder automatically starts all effects at 0 intensity. * * <p>To avoid these limitations and to have more control over the effects output, use * {@link WaveformEnvelopeBuilder}, where direct amplitude and frequency values can be used. * * <p>For optimal cross-device consistency, it's recommended to limit the number of control * points to a maximum of 16. However this is not mandatory, and if a pattern exceeds the * maximum number of allowed control points, the framework will automatically break down the * effect to ensure it plays correctly. * * <p>For example, the following code creates a vibration effect that ramps up the intensity * from a low-pitched to a high-pitched strong vibration over 500ms and then ramps it down to * 0 (off) over 100ms: * * <pre>{@code * VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder() * .setInitialSharpness(0.0f) * .addControlPoint(1.0f, 1.0f, 500) * .addControlPoint(0.0f, 1.0f, 100) * .build(); * }</pre> */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public static final class BasicEnvelopeBuilder { private ArrayList<BasicPwleSegment> mSegments = new ArrayList<>(); private float mLastIntensity = 0f; private float mLastSharpness = Float.NaN; public BasicEnvelopeBuilder() {} /** * Sets the initial sharpness for the basic envelope effect. * * <p>The effect will start vibrating at this sharpness when it transitions to the * intensity and sharpness defined by the first control point. * * <p> The sharpness defines the crispness of the vibration, ranging from 0 to 1. Lower * values translate to smoother vibrations, while higher values create a sharper more snappy * sensation. This value is mapped to the supported frequency range of the device. * * @param initialSharpness The starting sharpness of the vibration in the range of [0, 1]. */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) @SuppressWarnings("MissingGetterMatchingBuilder")// No getter to initial sharpness once set. @NonNull public BasicEnvelopeBuilder setInitialSharpness( @FloatRange(from = 0, to = 1) float initialSharpness) { if (mSegments.isEmpty()) { mLastSharpness = initialSharpness; } else { BasicPwleSegment firstSegment = mSegments.getFirst(); mSegments.set(0, new BasicPwleSegment( firstSegment.getStartIntensity(), firstSegment.getEndIntensity(), initialSharpness, // Update start sharpness firstSegment.getEndSharpness(), firstSegment.getDuration())); } return this; } /** * Adds a new control point to the end of this waveform envelope. * * <p>Intensity defines the overall strength of the vibration, ranging from 0 (off) to 1 * (maximum achievable strength). Higher values translate to stronger vibrations. * * <p>Sharpness defines the crispness of the vibration, ranging from 0 to 1. Lower * values translate to smoother vibrations, while higher values create a sharper more snappy * sensation. This value is mapped to the supported frequency range of the device. * * <p>Time specifies the duration (in milliseconds) for the vibrator to smoothly transition * from the previous control point to this new one. It must be greater than zero. To * transition as quickly as possible, use * {@link VibratorEnvelopeEffectInfo#getMinControlPointDurationMillis()}. * * @param intensity The target vibration intensity, ranging from 0 (off) to 1 (maximum * strength). * @param sharpness The target sharpness, ranging from 0 (smoothest) to 1 (sharpest). * @param durationMillis The transition time in milliseconds. */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) @SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created. @NonNull public BasicEnvelopeBuilder addControlPoint( @FloatRange(from = 0, to = 1) float intensity, @FloatRange(from = 0, to = 1) float sharpness, @DurationMillisLong long durationMillis) { if (Float.isNaN(mLastSharpness)) { mLastSharpness = sharpness; } mSegments.add(new BasicPwleSegment(mLastIntensity, intensity, mLastSharpness, sharpness, durationMillis)); mLastIntensity = intensity; mLastSharpness = sharpness; return this; } /** * Build the waveform as a single {@link VibrationEffect}. * * <p>The {@link BasicEnvelopeBuilder} 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 list of control points. * @throws IllegalStateException if the last control point does not end at zero intensity. */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) @NonNull public VibrationEffect build() { if (mSegments.isEmpty()) { throw new IllegalStateException( "BasicEnvelopeBuilder must have at least one control point to build."); } if (mSegments.getLast().getEndIntensity() != 0) { throw new IllegalStateException( "Basic envelope effects must end at a zero intensity control point."); } VibrationEffect effect = new Composed(mSegments, /* repeatIndex= */ -1); effect.validate(); return effect; } } /** * A builder for waveform haptic effects. * Loading
core/java/android/os/vibrator/BasicPwleSegment.java 0 → 100644 +222 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.FlaggedApi; import android.annotation.NonNull; import android.annotation.TestApi; import android.os.Parcel; import android.os.VibrationEffect; import android.os.VibratorInfo; import com.android.internal.util.Preconditions; import java.util.Locale; import java.util.Objects; /** * A {@link VibrationEffectSegment} that represents a smooth transition from the starting * intensity and sharpness to new values over a specified duration. * * <p>The intensity and sharpness are expressed by float values in the range [0, 1], where * intensity represents the user-perceived strength of the vibration, while sharpness represents * the crispness of the vibration. * * @hide */ @TestApi @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public final class BasicPwleSegment extends VibrationEffectSegment { private final float mStartIntensity; private final float mEndIntensity; private final float mStartSharpness; private final float mEndSharpness; private final long mDuration; BasicPwleSegment(@NonNull Parcel in) { this(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in.readLong()); } /** @hide */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public BasicPwleSegment(float startIntensity, float endIntensity, float startSharpness, float endSharpness, long duration) { mStartIntensity = startIntensity; mEndIntensity = endIntensity; mStartSharpness = startSharpness; mEndSharpness = endSharpness; mDuration = duration; } public float getStartIntensity() { return mStartIntensity; } public float getEndIntensity() { return mEndIntensity; } public float getStartSharpness() { return mStartSharpness; } public float getEndSharpness() { return mEndSharpness; } @Override public long getDuration() { return mDuration; } @Override public boolean equals(Object o) { if (!(o instanceof BasicPwleSegment)) { return false; } BasicPwleSegment other = (BasicPwleSegment) o; return Float.compare(mStartIntensity, other.mStartIntensity) == 0 && Float.compare(mEndIntensity, other.mEndIntensity) == 0 && Float.compare(mStartSharpness, other.mStartSharpness) == 0 && Float.compare(mEndSharpness, other.mEndSharpness) == 0 && mDuration == other.mDuration; } /** @hide */ @Override public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) { return vibratorInfo.areEnvelopeEffectsSupported(); } /** @hide */ @Override public boolean isHapticFeedbackCandidate() { return true; } /** @hide */ @Override public void validate() { Preconditions.checkArgumentInRange(mStartSharpness, 0f, 1f, "startSharpness"); Preconditions.checkArgumentInRange(mEndSharpness, 0f, 1f, "endSharpness"); Preconditions.checkArgumentInRange(mStartIntensity, 0f, 1f, "startIntensity"); Preconditions.checkArgumentInRange(mEndIntensity, 0f, 1f, "endIntensity"); Preconditions.checkArgumentPositive(mDuration, "Time must be greater than zero."); } /** @hide */ @NonNull @Override public BasicPwleSegment resolve(int defaultAmplitude) { return this; } /** @hide */ @NonNull @Override public BasicPwleSegment scale(float scaleFactor) { float newStartIntensity = VibrationEffect.scale(mStartIntensity, scaleFactor); float newEndIntensity = VibrationEffect.scale(mEndIntensity, scaleFactor); if (Float.compare(mStartIntensity, newStartIntensity) == 0 && Float.compare(mEndIntensity, newEndIntensity) == 0) { return this; } return new BasicPwleSegment(newStartIntensity, newEndIntensity, mStartSharpness, mEndSharpness, mDuration); } /** @hide */ @NonNull @Override public BasicPwleSegment scaleLinearly(float scaleFactor) { float newStartIntensity = VibrationEffect.scaleLinearly(mStartIntensity, scaleFactor); float newEndIntensity = VibrationEffect.scaleLinearly(mEndIntensity, scaleFactor); if (Float.compare(mStartIntensity, newStartIntensity) == 0 && Float.compare(mEndIntensity, newEndIntensity) == 0) { return this; } return new BasicPwleSegment(newStartIntensity, newEndIntensity, mStartSharpness, mEndSharpness, mDuration); } /** @hide */ @NonNull @Override public BasicPwleSegment applyEffectStrength(int effectStrength) { return this; } @Override public int hashCode() { return Objects.hash(mStartIntensity, mEndIntensity, mStartSharpness, mEndSharpness, mDuration); } @Override public String toString() { return "BasicPwle{startIntensity=" + mStartIntensity + ", endIntensity=" + mEndIntensity + ", startSharpness=" + mStartSharpness + ", endSharpness=" + mEndSharpness + ", duration=" + mDuration + "}"; } /** @hide */ @Override public String toDebugString() { return String.format(Locale.US, "Pwle=%dms(intensity=%.2f @ %.2f to %.2f @ %.2f)", mDuration, mStartIntensity, mStartSharpness, mEndIntensity, mEndSharpness); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(PARCEL_TOKEN_PWLE); dest.writeFloat(mStartIntensity); dest.writeFloat(mEndIntensity); dest.writeFloat(mStartSharpness); dest.writeFloat(mEndSharpness); dest.writeLong(mDuration); } @NonNull public static final Creator<BasicPwleSegment> CREATOR = new Creator<BasicPwleSegment>() { @Override public BasicPwleSegment createFromParcel(Parcel in) { // Skip the type token in.readInt(); return new BasicPwleSegment(in); } @Override public BasicPwleSegment[] newArray(int size) { return new BasicPwleSegment[size]; } }; }
core/java/android/os/vibrator/PwlePoint.java +8 −0 Original line number Diff line number Diff line Loading @@ -63,4 +63,12 @@ public final class PwlePoint { public int hashCode() { return Objects.hash(mAmplitude, mFrequencyHz, mTimeMillis); } @Override public String toString() { return "PwlePoint{amplitude=" + mAmplitude + ", frequency=" + mFrequencyHz + ", time=" + mTimeMillis + "}"; } }