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

Commit de61e621 authored by Ahmad Khalil's avatar Ahmad Khalil Committed by Android (Google) Code Review
Browse files

Merge "Introducing WaveformEnvelopeBuilder to VibrationEffect" into main

parents c153112f 2620ef14
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -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
@@ -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...);
+11 −0
Original line number Diff line number Diff line
@@ -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();
+8 −1
Original line number Diff line number Diff line
@@ -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);
    }

@@ -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);
            }
@@ -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);
            }
+142 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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.
     *
+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