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

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

Merge "Add initial frequency to envelope effects" into main

parents ce7e0227 b563aba2
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -34531,6 +34531,7 @@ package android.os {
    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();
    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public static android.os.VibrationEffect.WaveformEnvelopeBuilder startWaveformEnvelope(@FloatRange(from=0) float);
    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
+34 −2
Original line number Diff line number Diff line
@@ -1740,7 +1740,9 @@ public abstract class VibrationEffect implements Parcelable {
     *
     * <p>The waveform envelope builder offers more flexibility for creating waveform effects,
     * allowing control over vibration amplitude and frequency via smooth transitions between
     * values.
     * values. The waveform will start the first transition from the vibrator off state, using
     * the same frequency of the first control point. To provide a different initial vibration
     * frequency, use {@link #startWaveformEnvelope(float)}.
     *
     * <p>Note: To check whether waveform envelope effects are supported, use
     * {@link Vibrator#areEnvelopeEffectsSupported()}.
@@ -1753,6 +1755,32 @@ public abstract class VibrationEffect implements Parcelable {
        return new WaveformEnvelopeBuilder();
    }

    /**
     * Start building a waveform vibration with an initial frequency.
     *
     * <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>This is the same as {@link #startWaveformEnvelope()}, but the waveform will start
     * vibrating at given frequency, in hertz, while it transitions to the new amplitude and
     * frequency of the first control point.
     *
     * <p>Note: To check whether waveform envelope effects are supported, use
     * {@link Vibrator#areEnvelopeEffectsSupported()}.
     *
     * @param initialFrequencyHz The starting frequency of the vibration, in hertz. Must be greater
     *                           than zero.
     *
     * @see VibrationEffect.WaveformEnvelopeBuilder
     */
    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
    @NonNull
    public static VibrationEffect.WaveformEnvelopeBuilder startWaveformEnvelope(
            @FloatRange(from = 0) float initialFrequencyHz) {
        return new WaveformEnvelopeBuilder(initialFrequencyHz);
    }

    /**
     * A builder for waveform effects described by its envelope.
     *
@@ -1810,6 +1838,10 @@ public abstract class VibrationEffect implements Parcelable {

        private WaveformEnvelopeBuilder() {}

        private WaveformEnvelopeBuilder(float initialFrequency) {
            mLastFrequencyHz = initialFrequency;
        }

        /**
         * Adds a new control point to the end of this waveform envelope.
         *
@@ -1841,7 +1873,7 @@ public abstract class VibrationEffect implements Parcelable {
                @FloatRange(from = 0, to = 1) float amplitude,
                @FloatRange(from = 0) float frequencyHz, int timeMillis) {

            if (mSegments.isEmpty()) {
            if (mLastFrequencyHz == 0) {
                mLastFrequencyHz = frequencyHz;
            }

+66 −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 java.util.Objects;

/**
 * A {@link PwlePoint} represents a single point in an envelope vibration effect. Defined by its
 * amplitude, frequency and time to transition to this point from the previous one in the envelope.
 *
 * @hide
 */
public final class PwlePoint {
    private final float mAmplitude;
    private final float mFrequencyHz;
    private final int mTimeMillis;

    /** @hide */
    public PwlePoint(float amplitude, float frequencyHz, int timeMillis) {
        mAmplitude = amplitude;
        mFrequencyHz = frequencyHz;
        mTimeMillis = timeMillis;
    }

    public float getAmplitude() {
        return mAmplitude;
    }

    public float getFrequencyHz() {
        return mFrequencyHz;
    }

    public int getTimeMillis() {
        return mTimeMillis;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof PwlePoint)) {
            return false;
        }
        PwlePoint other = (PwlePoint) obj;
        return Float.compare(mAmplitude, other.mAmplitude) == 0
                && Float.compare(mFrequencyHz, other.mFrequencyHz) == 0
                && mTimeMillis == other.mTimeMillis;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mAmplitude, mFrequencyHz, mTimeMillis);
    }
}
+68 −0
Original line number Diff line number Diff line
@@ -425,6 +425,15 @@ public class VibrationEffectTest {
                .build();

        assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());

        effect = VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/ 60)
                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20)
                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 50)
                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 80)
                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 40)
                .build();

        assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
    }

    @Test
@@ -643,6 +652,14 @@ public class VibrationEffectTest {
                .build()
                .validate();

        VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/ 30)
                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20)
                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 50)
                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 80)
                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 40)
                .build()
                .validate();

        VibrationEffect.createRepeatingEffect(
                /*preamble=*/ VibrationEffect.startWaveformEnvelope()
                        .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f,
@@ -693,6 +710,34 @@ public class VibrationEffectTest {
                                /*timeMillis=*/ 0)
                        .build()
                        .validate());

        assertThrows(IllegalStateException.class,
                () -> VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/30)
                        .build().validate());
        assertThrows(IllegalArgumentException.class,
                () -> VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/30)
                        .addControlPoint(/*amplitude=*/ -1.0f, /*frequencyHz=*/ 60f,
                                /*timeMillis=*/ 20)
                        .build()
                        .validate());
        assertThrows(IllegalArgumentException.class,
                () -> VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/30)
                        .addControlPoint(/*amplitude=*/ 1.1f, /*frequencyHz=*/ 60f,
                                /*timeMillis=*/ 20)
                        .build()
                        .validate());
        assertThrows(IllegalArgumentException.class,
                () -> VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/30)
                        .addControlPoint(/*amplitude=*/ 0.8f, /*frequencyHz=*/ 0f,
                                /*timeMillis=*/ 20)
                        .build()
                        .validate());
        assertThrows(IllegalArgumentException.class,
                () -> VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/30)
                        .addControlPoint(/*amplitude=*/ 0.8f, /*frequencyHz=*/ 100f,
                                /*timeMillis=*/ 0)
                        .build()
                        .validate());
    }

    @Test
@@ -1331,6 +1376,11 @@ public class VibrationEffectTest {
                .addTransition(Duration.ofMillis(500), targetAmplitude(0))
                .build()
                .isHapticFeedbackCandidate());
    }

    @Test
    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
    public void testIsHapticFeedbackCandidate_longEnvelopeEffects_notCandidates() {
        assertFalse(VibrationEffect.startWaveformEnvelope()
                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 200)
                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500)
@@ -1338,6 +1388,13 @@ public class VibrationEffectTest {
                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400)
                .build()
                .isHapticFeedbackCandidate());
        assertFalse(VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/ 40)
                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 200)
                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500)
                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 800)
                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400)
                .build()
                .isHapticFeedbackCandidate());
    }

    @Test
@@ -1351,12 +1408,23 @@ public class VibrationEffectTest {
                .addTransition(Duration.ofMillis(300), targetAmplitude(0))
                .build()
                .isHapticFeedbackCandidate());
    }

    @Test
    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
    public void testIsHapticFeedbackCandidate_shortEnvelopeEffects_areCandidates() {
        assertTrue(VibrationEffect.startWaveformEnvelope()
                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500)
                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400)
                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 100)
                .build()
                .isHapticFeedbackCandidate());
        assertTrue(VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/ 30)
                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500)
                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400)
                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 100)
                .build()
                .isHapticFeedbackCandidate());
    }

    @Test
+23 −14
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.os.Trace;
import android.os.VibrationEffect;
import android.os.vibrator.Flags;
import android.os.vibrator.PwlePoint;
import android.os.vibrator.PwleSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.util.Slog;
@@ -57,7 +58,7 @@ final class ComposePwleV2VibratorStep extends AbstractComposedVibratorStep {
            // Load the next PwleSegments to create a single composePwleV2 call to the vibrator,
            // limited to the vibrator's maximum envelope effect size.
            int limit = controller.getVibratorInfo().getMaxEnvelopeEffectSize();
            List<PwleSegment> pwles = unrollPwleSegments(effect, segmentIndex, limit);
            List<PwlePoint> pwles = unrollPwleSegments(effect, segmentIndex, limit);

            if (pwles.isEmpty()) {
                Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposeEnvelopeStep: "
@@ -70,7 +71,7 @@ final class ComposePwleV2VibratorStep extends AbstractComposedVibratorStep {
                Slog.d(VibrationThread.TAG, "Compose " + pwles + " PWLEs on vibrator "
                        + controller.getVibratorInfo().getId());
            }
            PwleSegment[] pwlesArray = pwles.toArray(new PwleSegment[pwles.size()]);
            PwlePoint[] pwlesArray = pwles.toArray(new PwlePoint[pwles.size()]);
            long vibratorOnResult = controller.on(pwlesArray, getVibration().id);
            handleVibratorOnResult(vibratorOnResult);
            getVibration().stats.reportComposePwle(vibratorOnResult, pwlesArray);
@@ -82,9 +83,9 @@ final class ComposePwleV2VibratorStep extends AbstractComposedVibratorStep {
        }
    }

    private List<PwleSegment> unrollPwleSegments(VibrationEffect.Composed effect, int startIndex,
    private List<PwlePoint> unrollPwleSegments(VibrationEffect.Composed effect, int startIndex,
            int limit) {
        List<PwleSegment> segments = new ArrayList<>(limit);
        List<PwlePoint> pwlePoints = new ArrayList<>(limit);
        float bestBreakAmplitude = 1;
        int bestBreakPosition = limit; // Exclusive index.

@@ -93,7 +94,7 @@ final class ComposePwleV2VibratorStep extends AbstractComposedVibratorStep {

        // Loop once after reaching the limit to see if breaking it will really be necessary, then
        // apply the best break position found, otherwise return the full list as it fits the limit.
        for (int i = startIndex; segments.size() <= limit; i++) {
        for (int i = startIndex; pwlePoints.size() < limit; i++) {
            if (i == segmentCount) {
                if (repeatIndex >= 0) {
                    i = repeatIndex;
@@ -104,12 +105,20 @@ final class ComposePwleV2VibratorStep extends AbstractComposedVibratorStep {
            }
            VibrationEffectSegment segment = effect.getSegments().get(i);
            if (segment instanceof PwleSegment pwleSegment) {
                segments.add(pwleSegment);
                if (pwlePoints.isEmpty()) {
                    // The initial state is defined by the starting amplitude and frequency of the
                    // first PwleSegment. The time parameter is set to zero to indicate this is
                    // the initial condition without any ramp up time.
                    pwlePoints.add(new PwlePoint(pwleSegment.getStartAmplitude(),
                            pwleSegment.getStartFrequencyHz(), /*timeMillis=*/ 0));
                }
                pwlePoints.add(new PwlePoint(pwleSegment.getEndAmplitude(),
                        pwleSegment.getEndFrequencyHz(), (int) pwleSegment.getDuration()));

                if (isBetterBreakPosition(segments, bestBreakAmplitude, limit)) {
                if (isBetterBreakPosition(pwlePoints, bestBreakAmplitude, limit)) {
                    // Mark this position as the best one so far to break a long waveform.
                    bestBreakAmplitude = pwleSegment.getEndAmplitude();
                    bestBreakPosition = segments.size(); // Break after this pwle ends.
                    bestBreakPosition = pwlePoints.size(); // Break after this pwle ends.
                }
            } else {
                // First non-pwle segment, stop collecting pwles.
@@ -117,21 +126,21 @@ final class ComposePwleV2VibratorStep extends AbstractComposedVibratorStep {
            }
        }

        return segments.size() > limit
        return pwlePoints.size() > limit
                // Remove excessive segments, using the best breaking position recorded.
                ? segments.subList(0, bestBreakPosition)
                ? pwlePoints.subList(0, bestBreakPosition)
                // Return all collected pwle segments.
                : segments;
                : pwlePoints;
    }

    /**
     * Returns true if the current segment list represents a better break position for a PWLE,
     * given the current amplitude being used for breaking it at a smaller size and the size limit.
     */
    private boolean isBetterBreakPosition(List<PwleSegment> segments,
    private boolean isBetterBreakPosition(List<PwlePoint> segments,
            float currentBestBreakAmplitude, int limit) {
        PwleSegment lastSegment = segments.get(segments.size() - 1);
        float breakAmplitudeCandidate = lastSegment.getEndAmplitude();
        PwlePoint lastSegment = segments.get(segments.size() - 1);
        float breakAmplitudeCandidate = lastSegment.getAmplitude();
        int breakPositionCandidate = segments.size();

        if (breakPositionCandidate > limit) {
Loading