Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d01d77c9 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Introducing BasicEnvelopeBuilder to VibrationEffect" into main

parents a166c54a fc6547ac
Loading
Loading
Loading
Loading
+8 −1
Original line number Diff line number Diff line
@@ -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);
@@ -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);
  }
+11 −0
Original line number Diff line number Diff line
@@ -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();
+175 −8
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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
@@ -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;
@@ -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;
@@ -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.
     *
+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];
                }
            };
}
+8 −0
Original line number Diff line number Diff line
@@ -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