Loading core/api/current.txt +6 −0 Original line number Diff line number Diff line Loading @@ -34377,6 +34377,7 @@ package android.os { method public static android.os.VibrationEffect createWaveform(long[], int[], int); method public int describeContents(); method @NonNull public static android.os.VibrationEffect.Composition startComposition(); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public static android.os.VibrationEffect.WaveformEnvelopeBuilder startWaveformEnvelope(); field @NonNull public static final android.os.Parcelable.Creator<android.os.VibrationEffect> CREATOR; field public static final int DEFAULT_AMPLITUDE = -1; // 0xffffffff field public static final int EFFECT_CLICK = 0; // 0x0 Loading @@ -34400,6 +34401,11 @@ package android.os { field public static final int PRIMITIVE_TICK = 7; // 0x7 } @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public static final class 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 build(); } public abstract class Vibrator { method public final int areAllEffectsSupported(@NonNull int...); method public final boolean areAllPrimitivesSupported(@NonNull int...); core/api/test-current.txt +11 −0 Original line number Diff line number Diff line Loading @@ -2771,6 +2771,17 @@ package android.os.vibrator { field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.PrimitiveSegment> CREATOR; } @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public final class PwleSegment extends android.os.vibrator.VibrationEffectSegment { method public int describeContents(); method public long getDuration(); method public float getEndAmplitude(); method public float getEndFrequencyHz(); method public float getStartAmplitude(); method public float getStartFrequencyHz(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.PwleSegment> CREATOR; } public final class RampSegment extends android.os.vibrator.VibrationEffectSegment { method public int describeContents(); method public long getDuration(); Loading core/java/android/os/CombinedVibration.java +8 −1 Original line number Diff line number Diff line Loading @@ -194,7 +194,6 @@ public abstract class CombinedVibration implements Parcelable { int[] getAvailableVibratorIds(); /** Adapts a {@link VibrationEffect} to a given vibrator. */ @NonNull VibrationEffect adaptToVibrator(int vibratorId, @NonNull VibrationEffect effect); } Loading Loading @@ -442,6 +441,10 @@ public abstract class CombinedVibration implements Parcelable { boolean hasSameEffects = true; for (int vibratorId : adapter.getAvailableVibratorIds()) { VibrationEffect newEffect = adapter.adaptToVibrator(vibratorId, mEffect); if (newEffect == null) { // The vibration effect contains unsupported segments and cannot be played. return null; } combination.addVibrator(vibratorId, newEffect); hasSameEffects &= mEffect.equals(newEffect); } Loading Loading @@ -649,6 +652,10 @@ public abstract class CombinedVibration implements Parcelable { int vibratorId = mEffects.keyAt(i); VibrationEffect effect = mEffects.valueAt(i); VibrationEffect newEffect = adapter.adaptToVibrator(vibratorId, effect); if (newEffect == null) { // The vibration effect contains unsupported segments and cannot be played. return null; } combination.addVibrator(vibratorId, newEffect); hasSameEffects &= effect.equals(newEffect); } Loading core/java/android/os/VibrationEffect.java +142 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import android.net.Uri; import android.os.vibrator.Flags; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.PwleSegment; import android.os.vibrator.RampSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationEffectSegment; Loading Loading @@ -1691,6 +1692,147 @@ public abstract class VibrationEffect implements Parcelable { } } /** * Start building a waveform vibration. * * <p>The waveform envelope builder offers more flexibility for creating waveform effects, * allowing control over vibration amplitude and frequency via smooth transitions between * values. * * <p>Note: To check whether waveform envelope effects are supported, use * {@link Vibrator#areEnvelopeEffectsSupported()}. * * @see VibrationEffect.WaveformEnvelopeBuilder */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) @NonNull public static VibrationEffect.WaveformEnvelopeBuilder startWaveformEnvelope() { return new WaveformEnvelopeBuilder(); } /** * A builder for waveform effects described by its envelope. * * <p>Waveform effect envelopes are defined by one or more control points describing a target * vibration amplitude and frequency, and a duration to reach those targets. The vibrator * will perform smooth transitions between control points. * * <p>For example, the following code ramps a vibrator from off to full amplitude at 120Hz over * 100ms, holds that state for 200ms, and then ramps back down over 100ms: * * <pre>{@code * VibrationEffect effect = VibrationEffect.startWaveformEnvelope() * .addControlPoint(1.0f, 120f, 100) * .addControlPoint(1.0f, 120f, 200) * .addControlPoint(0.0f, 120f, 100) * .build(); * }</pre> * * <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 * modify these frequencies. * * <p>Therefore, it is strongly recommended that you design your haptic effects with the * device's frequency profile in mind. You can obtain the supported frequency range and other * relevant frequency-related information by getting the * {@link android.os.vibrator.VibratorFrequencyProfile} using the * {@link Vibrator#getFrequencyProfile()} method. * * <p>In addition to these limitations, when designing vibration patterns, it is important to * consider the physical limitations of the vibration actuator. These limitations include * factors such as the maximum number of control points allowed in an envelope effect, the * minimum and maximum durations permitted for each control point, and the maximum overall * duration of the effect. 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>You can use the following APIs to obtain these limits: * <ul> * <li>Maximum envelope control points: {@link Vibrator#getMaxEnvelopeEffectSize()}</li> * <li>Minimum control point duration: * {@link Vibrator#getMinEnvelopeEffectControlPointDurationMillis()}</li> * <li>Maximum control point duration: * {@link Vibrator#getMaxEnvelopeEffectControlPointDurationMillis()}</li> * <li>Maximum total effect duration: {@link Vibrator#getMaxEnvelopeEffectDurationMillis()}</li> * </ul> * * @see VibrationEffect#startWaveformEnvelope() */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public static final class WaveformEnvelopeBuilder { private ArrayList<PwleSegment> mSegments = new ArrayList<>(); private float mLastAmplitude = 0f; private float mLastFrequencyHz = 0f; private WaveformEnvelopeBuilder() {} /** * Adds a new control point to the end of this waveform envelope. * * <p>Amplitude defines the vibrator's strength at this frequency, ranging from 0 (off) to 1 * (maximum achievable strength). This value scales linearly with output strength, not * perceived intensity. It's determined by the actuator response curve. * * <p>Frequency must be greater than zero and within the supported range. To determine * the supported range, use {@link Vibrator#getFrequencyProfile()}. This method returns a * {@link android.os.vibrator.VibratorFrequencyProfile} object, which contains the * minimum and maximum frequencies, among other frequency-related information. Creating * effects using frequencies outside this range will result in the vibration not playing. * * <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 Vibrator#getMinEnvelopeEffectControlPointDurationMillis()}. * * @param amplitude The amplitude value between 0 and 1, inclusive. 0 represents the * 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. */ @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) { if (mSegments.isEmpty()) { mLastFrequencyHz = frequencyHz; } mSegments.add(new PwleSegment(mLastAmplitude, amplitude, mLastFrequencyHz, frequencyHz, timeMillis)); mLastAmplitude = amplitude; mLastFrequencyHz = frequencyHz; return this; } /** * Build the waveform as a single {@link VibrationEffect}. * * <p>The {@link WaveformEnvelopeBuilder} 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. */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) @NonNull public VibrationEffect build() { if (mSegments.isEmpty()) { throw new IllegalStateException( "WaveformEnvelopeBuilder must have at least one control point to build."); } VibrationEffect effect = new Composed(mSegments, /* repeatIndex= */ -1); effect.validate(); return effect; } } /** * A builder for waveform haptic effects. * Loading core/java/android/os/vibrator/PwleSegment.java 0 → 100644 +234 −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 * amplitude and frequency to new values over a specified duration. * * <p>The amplitudes are expressed by float values in the range [0, 1], representing the relative * output acceleration for the vibrator. The frequencies are expressed in hertz by positive finite * float values. * @hide */ @TestApi @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public final class PwleSegment extends VibrationEffectSegment { private final float mStartAmplitude; private final float mStartFrequencyHz; private final float mEndAmplitude; private final float mEndFrequencyHz; private final int mDuration; PwleSegment(@android.annotation.NonNull Parcel in) { this(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in.readInt()); } /** @hide */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public PwleSegment(float startAmplitude, float endAmplitude, float startFrequencyHz, float endFrequencyHz, int duration) { mStartAmplitude = startAmplitude; mEndAmplitude = endAmplitude; mStartFrequencyHz = startFrequencyHz; mEndFrequencyHz = endFrequencyHz; mDuration = duration; } public float getStartAmplitude() { return mStartAmplitude; } public float getEndAmplitude() { return mEndAmplitude; } public float getStartFrequencyHz() { return mStartFrequencyHz; } public float getEndFrequencyHz() { return mEndFrequencyHz; } @Override public long getDuration() { return mDuration; } @Override public boolean equals(Object o) { if (!(o instanceof PwleSegment)) { return false; } PwleSegment other = (PwleSegment) o; return Float.compare(mStartAmplitude, other.mStartAmplitude) == 0 && Float.compare(mEndAmplitude, other.mEndAmplitude) == 0 && Float.compare(mStartFrequencyHz, other.mStartFrequencyHz) == 0 && Float.compare(mEndFrequencyHz, other.mEndFrequencyHz) == 0 && mDuration == other.mDuration; } /** @hide */ @Override public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) { boolean areFeaturesSupported = vibratorInfo.areEnvelopeEffectsSupported(); // Check that the frequency is within the supported range float minFrequency = vibratorInfo.getFrequencyProfile().getMinFrequencyHz(); float maxFrequency = vibratorInfo.getFrequencyProfile().getMaxFrequencyHz(); areFeaturesSupported &= mStartFrequencyHz >= minFrequency && mStartFrequencyHz <= maxFrequency && mEndFrequencyHz >= minFrequency && mEndFrequencyHz <= maxFrequency; return areFeaturesSupported; } /** @hide */ @Override public boolean isHapticFeedbackCandidate() { return true; } /** @hide */ @Override public void validate() { Preconditions.checkArgumentPositive(mStartFrequencyHz, "Start frequency must be greater than zero."); Preconditions.checkArgumentPositive(mEndFrequencyHz, "End frequency must be greater than zero."); Preconditions.checkArgumentPositive(mDuration, "Time must be greater than zero."); Preconditions.checkArgumentInRange(mStartAmplitude, 0f, 1f, "startAmplitude"); Preconditions.checkArgumentInRange(mEndAmplitude, 0f, 1f, "endAmplitude"); } /** @hide */ @NonNull @Override public PwleSegment resolve(int defaultAmplitude) { return this; } /** @hide */ @NonNull @Override public PwleSegment 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 PwleSegment(newStartAmplitude, newEndAmplitude, mStartFrequencyHz, mEndFrequencyHz, mDuration); } /** @hide */ @NonNull @Override public PwleSegment scaleLinearly(float scaleFactor) { float newStartAmplitude = VibrationEffect.scaleLinearly(mStartAmplitude, scaleFactor); float newEndAmplitude = VibrationEffect.scaleLinearly(mEndAmplitude, scaleFactor); if (Float.compare(mStartAmplitude, newStartAmplitude) == 0 && Float.compare(mEndAmplitude, newEndAmplitude) == 0) { return this; } return new PwleSegment(newStartAmplitude, newEndAmplitude, mStartFrequencyHz, mEndFrequencyHz, mDuration); } /** @hide */ @NonNull @Override public PwleSegment applyEffectStrength(int effectStrength) { return this; } @Override public int hashCode() { return Objects.hash(mStartAmplitude, mEndAmplitude, mStartFrequencyHz, mEndFrequencyHz, mDuration); } @Override public String toString() { return "Pwle{startAmplitude=" + mStartAmplitude + ", endAmplitude=" + mEndAmplitude + ", startFrequencyHz=" + mStartFrequencyHz + ", endFrequencyHz=" + mEndFrequencyHz + ", duration=" + mDuration + "}"; } /** @hide */ @Override public String toDebugString() { return String.format(Locale.US, "Pwle=%dms(amplitude=%.2f @ %.2fHz to %.2f @ %.2fHz)", mDuration, mStartAmplitude, mStartFrequencyHz, mEndAmplitude, mEndFrequencyHz); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(PARCEL_TOKEN_PWLE); dest.writeFloat(mStartAmplitude); dest.writeFloat(mEndAmplitude); dest.writeFloat(mStartFrequencyHz); dest.writeFloat(mEndFrequencyHz); dest.writeInt(mDuration); } @android.annotation.NonNull public static final Creator<PwleSegment> CREATOR = new Creator<PwleSegment>() { @Override public PwleSegment createFromParcel(Parcel in) { // Skip the type token in.readInt(); return new PwleSegment(in); } @Override public PwleSegment[] newArray(int size) { return new PwleSegment[size]; } }; } Loading
core/api/current.txt +6 −0 Original line number Diff line number Diff line Loading @@ -34377,6 +34377,7 @@ package android.os { method public static android.os.VibrationEffect createWaveform(long[], int[], int); method public int describeContents(); method @NonNull public static android.os.VibrationEffect.Composition startComposition(); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public static android.os.VibrationEffect.WaveformEnvelopeBuilder startWaveformEnvelope(); field @NonNull public static final android.os.Parcelable.Creator<android.os.VibrationEffect> CREATOR; field public static final int DEFAULT_AMPLITUDE = -1; // 0xffffffff field public static final int EFFECT_CLICK = 0; // 0x0 Loading @@ -34400,6 +34401,11 @@ package android.os { field public static final int PRIMITIVE_TICK = 7; // 0x7 } @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public static final class 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 build(); } public abstract class Vibrator { method public final int areAllEffectsSupported(@NonNull int...); method public final boolean areAllPrimitivesSupported(@NonNull int...);
core/api/test-current.txt +11 −0 Original line number Diff line number Diff line Loading @@ -2771,6 +2771,17 @@ package android.os.vibrator { field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.PrimitiveSegment> CREATOR; } @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public final class PwleSegment extends android.os.vibrator.VibrationEffectSegment { method public int describeContents(); method public long getDuration(); method public float getEndAmplitude(); method public float getEndFrequencyHz(); method public float getStartAmplitude(); method public float getStartFrequencyHz(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.PwleSegment> CREATOR; } public final class RampSegment extends android.os.vibrator.VibrationEffectSegment { method public int describeContents(); method public long getDuration(); Loading
core/java/android/os/CombinedVibration.java +8 −1 Original line number Diff line number Diff line Loading @@ -194,7 +194,6 @@ public abstract class CombinedVibration implements Parcelable { int[] getAvailableVibratorIds(); /** Adapts a {@link VibrationEffect} to a given vibrator. */ @NonNull VibrationEffect adaptToVibrator(int vibratorId, @NonNull VibrationEffect effect); } Loading Loading @@ -442,6 +441,10 @@ public abstract class CombinedVibration implements Parcelable { boolean hasSameEffects = true; for (int vibratorId : adapter.getAvailableVibratorIds()) { VibrationEffect newEffect = adapter.adaptToVibrator(vibratorId, mEffect); if (newEffect == null) { // The vibration effect contains unsupported segments and cannot be played. return null; } combination.addVibrator(vibratorId, newEffect); hasSameEffects &= mEffect.equals(newEffect); } Loading Loading @@ -649,6 +652,10 @@ public abstract class CombinedVibration implements Parcelable { int vibratorId = mEffects.keyAt(i); VibrationEffect effect = mEffects.valueAt(i); VibrationEffect newEffect = adapter.adaptToVibrator(vibratorId, effect); if (newEffect == null) { // The vibration effect contains unsupported segments and cannot be played. return null; } combination.addVibrator(vibratorId, newEffect); hasSameEffects &= effect.equals(newEffect); } Loading
core/java/android/os/VibrationEffect.java +142 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import android.net.Uri; import android.os.vibrator.Flags; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.PwleSegment; import android.os.vibrator.RampSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationEffectSegment; Loading Loading @@ -1691,6 +1692,147 @@ public abstract class VibrationEffect implements Parcelable { } } /** * Start building a waveform vibration. * * <p>The waveform envelope builder offers more flexibility for creating waveform effects, * allowing control over vibration amplitude and frequency via smooth transitions between * values. * * <p>Note: To check whether waveform envelope effects are supported, use * {@link Vibrator#areEnvelopeEffectsSupported()}. * * @see VibrationEffect.WaveformEnvelopeBuilder */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) @NonNull public static VibrationEffect.WaveformEnvelopeBuilder startWaveformEnvelope() { return new WaveformEnvelopeBuilder(); } /** * A builder for waveform effects described by its envelope. * * <p>Waveform effect envelopes are defined by one or more control points describing a target * vibration amplitude and frequency, and a duration to reach those targets. The vibrator * will perform smooth transitions between control points. * * <p>For example, the following code ramps a vibrator from off to full amplitude at 120Hz over * 100ms, holds that state for 200ms, and then ramps back down over 100ms: * * <pre>{@code * VibrationEffect effect = VibrationEffect.startWaveformEnvelope() * .addControlPoint(1.0f, 120f, 100) * .addControlPoint(1.0f, 120f, 200) * .addControlPoint(0.0f, 120f, 100) * .build(); * }</pre> * * <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 * modify these frequencies. * * <p>Therefore, it is strongly recommended that you design your haptic effects with the * device's frequency profile in mind. You can obtain the supported frequency range and other * relevant frequency-related information by getting the * {@link android.os.vibrator.VibratorFrequencyProfile} using the * {@link Vibrator#getFrequencyProfile()} method. * * <p>In addition to these limitations, when designing vibration patterns, it is important to * consider the physical limitations of the vibration actuator. These limitations include * factors such as the maximum number of control points allowed in an envelope effect, the * minimum and maximum durations permitted for each control point, and the maximum overall * duration of the effect. 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>You can use the following APIs to obtain these limits: * <ul> * <li>Maximum envelope control points: {@link Vibrator#getMaxEnvelopeEffectSize()}</li> * <li>Minimum control point duration: * {@link Vibrator#getMinEnvelopeEffectControlPointDurationMillis()}</li> * <li>Maximum control point duration: * {@link Vibrator#getMaxEnvelopeEffectControlPointDurationMillis()}</li> * <li>Maximum total effect duration: {@link Vibrator#getMaxEnvelopeEffectDurationMillis()}</li> * </ul> * * @see VibrationEffect#startWaveformEnvelope() */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public static final class WaveformEnvelopeBuilder { private ArrayList<PwleSegment> mSegments = new ArrayList<>(); private float mLastAmplitude = 0f; private float mLastFrequencyHz = 0f; private WaveformEnvelopeBuilder() {} /** * Adds a new control point to the end of this waveform envelope. * * <p>Amplitude defines the vibrator's strength at this frequency, ranging from 0 (off) to 1 * (maximum achievable strength). This value scales linearly with output strength, not * perceived intensity. It's determined by the actuator response curve. * * <p>Frequency must be greater than zero and within the supported range. To determine * the supported range, use {@link Vibrator#getFrequencyProfile()}. This method returns a * {@link android.os.vibrator.VibratorFrequencyProfile} object, which contains the * minimum and maximum frequencies, among other frequency-related information. Creating * effects using frequencies outside this range will result in the vibration not playing. * * <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 Vibrator#getMinEnvelopeEffectControlPointDurationMillis()}. * * @param amplitude The amplitude value between 0 and 1, inclusive. 0 represents the * 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. */ @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) { if (mSegments.isEmpty()) { mLastFrequencyHz = frequencyHz; } mSegments.add(new PwleSegment(mLastAmplitude, amplitude, mLastFrequencyHz, frequencyHz, timeMillis)); mLastAmplitude = amplitude; mLastFrequencyHz = frequencyHz; return this; } /** * Build the waveform as a single {@link VibrationEffect}. * * <p>The {@link WaveformEnvelopeBuilder} 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. */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) @NonNull public VibrationEffect build() { if (mSegments.isEmpty()) { throw new IllegalStateException( "WaveformEnvelopeBuilder must have at least one control point to build."); } VibrationEffect effect = new Composed(mSegments, /* repeatIndex= */ -1); effect.validate(); return effect; } } /** * A builder for waveform haptic effects. * Loading
core/java/android/os/vibrator/PwleSegment.java 0 → 100644 +234 −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 * amplitude and frequency to new values over a specified duration. * * <p>The amplitudes are expressed by float values in the range [0, 1], representing the relative * output acceleration for the vibrator. The frequencies are expressed in hertz by positive finite * float values. * @hide */ @TestApi @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public final class PwleSegment extends VibrationEffectSegment { private final float mStartAmplitude; private final float mStartFrequencyHz; private final float mEndAmplitude; private final float mEndFrequencyHz; private final int mDuration; PwleSegment(@android.annotation.NonNull Parcel in) { this(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in.readInt()); } /** @hide */ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public PwleSegment(float startAmplitude, float endAmplitude, float startFrequencyHz, float endFrequencyHz, int duration) { mStartAmplitude = startAmplitude; mEndAmplitude = endAmplitude; mStartFrequencyHz = startFrequencyHz; mEndFrequencyHz = endFrequencyHz; mDuration = duration; } public float getStartAmplitude() { return mStartAmplitude; } public float getEndAmplitude() { return mEndAmplitude; } public float getStartFrequencyHz() { return mStartFrequencyHz; } public float getEndFrequencyHz() { return mEndFrequencyHz; } @Override public long getDuration() { return mDuration; } @Override public boolean equals(Object o) { if (!(o instanceof PwleSegment)) { return false; } PwleSegment other = (PwleSegment) o; return Float.compare(mStartAmplitude, other.mStartAmplitude) == 0 && Float.compare(mEndAmplitude, other.mEndAmplitude) == 0 && Float.compare(mStartFrequencyHz, other.mStartFrequencyHz) == 0 && Float.compare(mEndFrequencyHz, other.mEndFrequencyHz) == 0 && mDuration == other.mDuration; } /** @hide */ @Override public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) { boolean areFeaturesSupported = vibratorInfo.areEnvelopeEffectsSupported(); // Check that the frequency is within the supported range float minFrequency = vibratorInfo.getFrequencyProfile().getMinFrequencyHz(); float maxFrequency = vibratorInfo.getFrequencyProfile().getMaxFrequencyHz(); areFeaturesSupported &= mStartFrequencyHz >= minFrequency && mStartFrequencyHz <= maxFrequency && mEndFrequencyHz >= minFrequency && mEndFrequencyHz <= maxFrequency; return areFeaturesSupported; } /** @hide */ @Override public boolean isHapticFeedbackCandidate() { return true; } /** @hide */ @Override public void validate() { Preconditions.checkArgumentPositive(mStartFrequencyHz, "Start frequency must be greater than zero."); Preconditions.checkArgumentPositive(mEndFrequencyHz, "End frequency must be greater than zero."); Preconditions.checkArgumentPositive(mDuration, "Time must be greater than zero."); Preconditions.checkArgumentInRange(mStartAmplitude, 0f, 1f, "startAmplitude"); Preconditions.checkArgumentInRange(mEndAmplitude, 0f, 1f, "endAmplitude"); } /** @hide */ @NonNull @Override public PwleSegment resolve(int defaultAmplitude) { return this; } /** @hide */ @NonNull @Override public PwleSegment 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 PwleSegment(newStartAmplitude, newEndAmplitude, mStartFrequencyHz, mEndFrequencyHz, mDuration); } /** @hide */ @NonNull @Override public PwleSegment scaleLinearly(float scaleFactor) { float newStartAmplitude = VibrationEffect.scaleLinearly(mStartAmplitude, scaleFactor); float newEndAmplitude = VibrationEffect.scaleLinearly(mEndAmplitude, scaleFactor); if (Float.compare(mStartAmplitude, newStartAmplitude) == 0 && Float.compare(mEndAmplitude, newEndAmplitude) == 0) { return this; } return new PwleSegment(newStartAmplitude, newEndAmplitude, mStartFrequencyHz, mEndFrequencyHz, mDuration); } /** @hide */ @NonNull @Override public PwleSegment applyEffectStrength(int effectStrength) { return this; } @Override public int hashCode() { return Objects.hash(mStartAmplitude, mEndAmplitude, mStartFrequencyHz, mEndFrequencyHz, mDuration); } @Override public String toString() { return "Pwle{startAmplitude=" + mStartAmplitude + ", endAmplitude=" + mEndAmplitude + ", startFrequencyHz=" + mStartFrequencyHz + ", endFrequencyHz=" + mEndFrequencyHz + ", duration=" + mDuration + "}"; } /** @hide */ @Override public String toDebugString() { return String.format(Locale.US, "Pwle=%dms(amplitude=%.2f @ %.2fHz to %.2f @ %.2fHz)", mDuration, mStartAmplitude, mStartFrequencyHz, mEndAmplitude, mEndFrequencyHz); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(PARCEL_TOKEN_PWLE); dest.writeFloat(mStartAmplitude); dest.writeFloat(mEndAmplitude); dest.writeFloat(mStartFrequencyHz); dest.writeFloat(mEndFrequencyHz); dest.writeInt(mDuration); } @android.annotation.NonNull public static final Creator<PwleSegment> CREATOR = new Creator<PwleSegment>() { @Override public PwleSegment createFromParcel(Parcel in) { // Skip the type token in.readInt(); return new PwleSegment(in); } @Override public PwleSegment[] newArray(int size) { return new PwleSegment[size]; } }; }