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

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

Merge "Introduce VibrationEffect.Composition delay type" into main

parents 47d4331e ba8e6b36
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -34668,7 +34668,10 @@ package android.os {
    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);
    method @NonNull public android.os.VibrationEffect.Composition addPrimitive(int, @FloatRange(from=0.0f, to=1.0f) float, @IntRange(from=0) int);
    method @FlaggedApi("android.os.vibrator.primitive_composition_absolute_delay") @NonNull public android.os.VibrationEffect.Composition addPrimitive(int, @FloatRange(from=0.0f, to=1.0f) float, @IntRange(from=0) int, int);
    method @NonNull public android.os.VibrationEffect compose();
    field @FlaggedApi("android.os.vibrator.primitive_composition_absolute_delay") public static final int DELAY_TYPE_PAUSE = 0; // 0x0
    field @FlaggedApi("android.os.vibrator.primitive_composition_absolute_delay") public static final int DELAY_TYPE_RELATIVE_START_OFFSET = 1; // 0x1
    field public static final int PRIMITIVE_CLICK = 1; // 0x1
    field public static final int PRIMITIVE_LOW_TICK = 8; // 0x8
    field public static final int PRIMITIVE_QUICK_FALL = 6; // 0x6
+90 −1
Original line number Diff line number Diff line
@@ -1483,6 +1483,15 @@ public abstract class VibrationEffect implements Parcelable {
        public @interface PrimitiveType {
        }

        /** @hide */
        @IntDef(prefix = { "DELAY_TYPE_" }, value = {
                DELAY_TYPE_PAUSE,
                DELAY_TYPE_RELATIVE_START_OFFSET,
        })
        @Retention(RetentionPolicy.SOURCE)
        public @interface DelayType {
        }

        /**
         * Exception thrown when adding an element to a {@link Composition} that already ends in an
         * indefinitely repeating effect.
@@ -1541,6 +1550,53 @@ public abstract class VibrationEffect implements Parcelable {
        // Internally this maps to the HAL constant CompositePrimitive::LOW_TICK
        public static final int PRIMITIVE_LOW_TICK = 8;

        /**
         * The delay represents a pause in the composition between the end of the previous primitive
         * and the beginning of the next one.
         *
         * <p>The primitive will start after the requested pause after the last primitive ended.
         * The actual time the primitive will be played depends on the previous primitive's actual
         * duration on the device hardware. This enables the combination of primitives to create
         * more complex effects based on how close to each other they'll play. Here is an example:
         *
         * <pre>
         *     VibrationEffect popEffect = VibrationEffect.startComposition()
         *         .addPrimitive(PRIMITIVE_QUICK_RISE)
         *         .addPrimitive(PRIMITIVE_CLICK, 0.7, 50, DELAY_TYPE_PAUSE)
         *         .compose()
         * </pre>
         */
        @FlaggedApi(Flags.FLAG_PRIMITIVE_COMPOSITION_ABSOLUTE_DELAY)
        public static final int DELAY_TYPE_PAUSE = 0;

        /**
         * The delay represents an offset before starting this primitive, relative to the start
         * time of the previous primitive in the composition.
         *
         * <p>The primitive will start at the requested fixed time after the last primitive started,
         * independently of that primitive's actual duration on the device hardware. This enables
         * precise timings of primitives within a composition, ensuring they'll be played at the
         * desired intervals. Here is an example:
         *
         * <pre>
         *     VibrationEffect.startComposition()
         *         .addPrimitive(PRIMITIVE_CLICK, 1.0)
         *         .addPrimitive(PRIMITIVE_TICK, 1.0, 20, DELAY_TYPE_RELATIVE_START_OFFSET)
         *         .addPrimitive(PRIMITIVE_THUD, 1.0, 80, DELAY_TYPE_RELATIVE_START_OFFSET)
         *         .compose()
         * </pre>
         *
         * Will be performed on the device as follows:
         *
         * <pre>
         *  0ms               20ms                     100ms
         *  PRIMITIVE_CLICK---PRIMITIVE_TICK-----------PRIMITIVE_THUD
         * </pre>
         *
         * <p>A primitive will be dropped from the composition if it overlaps with previous ones.
         */
        @FlaggedApi(Flags.FLAG_PRIMITIVE_COMPOSITION_ABSOLUTE_DELAY)
        public static final int DELAY_TYPE_RELATIVE_START_OFFSET = 1;

        private final ArrayList<VibrationEffectSegment> mSegments = new ArrayList<>();
        private int mRepeatIndex = -1;
@@ -1665,7 +1721,26 @@ public abstract class VibrationEffect implements Parcelable {
        @NonNull
        public Composition addPrimitive(@PrimitiveType int primitiveId,
                @FloatRange(from = 0f, to = 1f) float scale, @IntRange(from = 0) int delay) {
            PrimitiveSegment primitive = new PrimitiveSegment(primitiveId, scale, delay);
            return addPrimitive(primitiveId, scale, delay, PrimitiveSegment.DEFAULT_DELAY_TYPE);
        }

        /**
         * Add a haptic primitive to the end of the current composition.
         *
         * @param primitiveId The primitive to add
         * @param scale The scale to apply to the intensity of the primitive.
         * @param delay The amount of time in milliseconds to wait before playing this primitive,
         *              as defined by the given {@code delayType}.
         * @param delayType The type of delay to be applied, e.g. a pause between last primitive and
         *                  this one or a start offset.
         * @return This {@link Composition} object to enable adding multiple elements in one chain.
         */
        @FlaggedApi(Flags.FLAG_PRIMITIVE_COMPOSITION_ABSOLUTE_DELAY)
        @NonNull
        public Composition addPrimitive(@PrimitiveType int primitiveId,
                @FloatRange(from = 0f, to = 1f) float scale, @IntRange(from = 0) int delay,
                @DelayType int delayType) {
            PrimitiveSegment primitive = new PrimitiveSegment(primitiveId, scale, delay, delayType);
            primitive.validate();
            return addSegment(primitive);
        }
@@ -1733,6 +1808,20 @@ public abstract class VibrationEffect implements Parcelable {
                default -> Integer.toString(id);
            };
        }

        /**
         * Convert the delay type to a human readable string for debugging.
         * @param type The delay type to convert
         * @return The delay type in a human readable format.
         * @hide
         */
        public static String delayTypeToString(@DelayType int type) {
            return switch (type) {
                case DELAY_TYPE_PAUSE -> "PAUSE";
                case DELAY_TYPE_RELATIVE_START_OFFSET -> "START_OFFSET";
                default -> Integer.toString(type);
            };
        }
    }

    /**
+43 −9
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import android.os.VibratorInfo;

import com.android.internal.util.Preconditions;

import java.util.Locale;
import java.util.Objects;

/**
@@ -43,19 +44,29 @@ public final class PrimitiveSegment extends VibrationEffectSegment {
    /** @hide */
    public static final int DEFAULT_DELAY_MILLIS = 0;

    /** @hide */
    public static final int DEFAULT_DELAY_TYPE = VibrationEffect.Composition.DELAY_TYPE_PAUSE;

    private final int mPrimitiveId;
    private final float mScale;
    private final int mDelay;
    private final int mDelayType;

    PrimitiveSegment(@NonNull Parcel in) {
        this(in.readInt(), in.readFloat(), in.readInt());
        this(in.readInt(), in.readFloat(), in.readInt(), in.readInt());
    }

    /** @hide */
    public PrimitiveSegment(int id, float scale, int delay) {
        this(id, scale, delay, DEFAULT_DELAY_TYPE);
    }

    /** @hide */
    public PrimitiveSegment(int id, float scale, int delay, int delayType) {
        mPrimitiveId = id;
        mScale = scale;
        mDelay = delay;
        mDelayType = delayType;
    }

    public int getPrimitiveId() {
@@ -70,6 +81,11 @@ public final class PrimitiveSegment extends VibrationEffectSegment {
        return mDelay;
    }

    /** @hide */
    public int getDelayType() {
        return mDelayType;
    }

    @Override
    public long getDuration() {
        return -1;
@@ -112,8 +128,7 @@ public final class PrimitiveSegment extends VibrationEffectSegment {
        if (Float.compare(mScale, newScale) == 0) {
            return this;
        }

        return new PrimitiveSegment(mPrimitiveId, newScale, mDelay);
        return new PrimitiveSegment(mPrimitiveId, newScale, mDelay, mDelayType);
    }

    /** @hide */
@@ -124,8 +139,7 @@ public final class PrimitiveSegment extends VibrationEffectSegment {
        if (Float.compare(mScale, newScale) == 0) {
            return this;
        }

        return new PrimitiveSegment(mPrimitiveId, newScale, mDelay);
        return new PrimitiveSegment(mPrimitiveId, newScale, mDelay, mDelayType);
    }

    /** @hide */
@@ -142,6 +156,7 @@ public final class PrimitiveSegment extends VibrationEffectSegment {
                VibrationEffect.Composition.PRIMITIVE_LOW_TICK, "primitiveId");
        Preconditions.checkArgumentInRange(mScale, 0f, 1f, "scale");
        VibrationEffectSegment.checkDurationArgument(mDelay, "delay");
        Preconditions.checkArgument(isValidDelayType(mDelayType), "delayType");
    }

    @Override
@@ -150,6 +165,7 @@ public final class PrimitiveSegment extends VibrationEffectSegment {
        dest.writeInt(mPrimitiveId);
        dest.writeFloat(mScale);
        dest.writeInt(mDelay);
        dest.writeInt(mDelayType);
    }

    @Override
@@ -163,14 +179,16 @@ public final class PrimitiveSegment extends VibrationEffectSegment {
                + "primitive=" + VibrationEffect.Composition.primitiveToString(mPrimitiveId)
                + ", scale=" + mScale
                + ", delay=" + mDelay
                + ", delayType=" + VibrationEffect.Composition.delayTypeToString(mDelayType)
                + '}';
    }

    /** @hide */
    @Override
    public String toDebugString() {
        return String.format("Primitive=%s(scale=%.2f, delay=%dms)",
                VibrationEffect.Composition.primitiveToString(mPrimitiveId), mScale, mDelay);
        return String.format(Locale.ROOT, "Primitive=%s(scale=%.2f, %s=%dms)",
                VibrationEffect.Composition.primitiveToString(mPrimitiveId), mScale,
                toDelayTypeDebugString(mDelayType), mDelay);
    }

    @Override
@@ -180,12 +198,28 @@ public final class PrimitiveSegment extends VibrationEffectSegment {
        PrimitiveSegment that = (PrimitiveSegment) o;
        return mPrimitiveId == that.mPrimitiveId
                && Float.compare(that.mScale, mScale) == 0
                && mDelay == that.mDelay;
                && mDelay == that.mDelay
                && mDelayType == that.mDelayType;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mPrimitiveId, mScale, mDelay);
        return Objects.hash(mPrimitiveId, mScale, mDelay, mDelayType);
    }

    private static boolean isValidDelayType(int delayType) {
        return switch (delayType) {
            case VibrationEffect.Composition.DELAY_TYPE_PAUSE,
                 VibrationEffect.Composition.DELAY_TYPE_RELATIVE_START_OFFSET -> true;
            default -> false;
        };
    }

    private static String toDelayTypeDebugString(int delayType) {
        return switch (delayType) {
            case VibrationEffect.Composition.DELAY_TYPE_RELATIVE_START_OFFSET -> "startOffset";
            default -> "pause";
        };
    }

    @NonNull
+5 −2
Original line number Diff line number Diff line
@@ -55,8 +55,9 @@ final class DeviceAdapter implements CombinedVibration.VibratorAdapter {

    DeviceAdapter(VibrationSettings settings, SparseArray<VibratorController> vibrators) {
        mSegmentAdapters = Arrays.asList(
                // TODO(b/167947076): add filter that removes unsupported primitives
                // TODO(b/167947076): add filter that replaces unsupported prebaked with fallback
                // Updates primitive delays to hardware supported pauses
                new PrimitiveDelayAdapter(),
                // Convert segments based on device capabilities
                new RampToStepAdapter(settings.getRampStepDuration()),
                new StepToRampAdapter(),
@@ -71,7 +72,9 @@ final class DeviceAdapter implements CombinedVibration.VibratorAdapter {
        );
        mSegmentsValidators = List.of(
                // Validate Pwle segments base on the vibrators frequency range
                new PwleSegmentsValidator()
                new PwleSegmentsValidator(),
                // Validate primitive segments based on device support
                new PrimitiveSegmentsValidator()
        );
        mAvailableVibrators = vibrators;
        mAvailableVibratorIds = new int[vibrators.size()];
+116 −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 com.android.server.vibrator;

import static android.os.VibrationEffect.Composition.DELAY_TYPE_PAUSE;
import static android.os.VibrationEffect.Composition.DELAY_TYPE_RELATIVE_START_OFFSET;

import android.os.VibrationEffect.Composition.DelayType;
import android.os.VibratorInfo;
import android.os.vibrator.Flags;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.VibrationEffectSegment;

import java.util.List;

/**
 * Adapter that converts between {@link DelayType} and the HAL supported pause delays.
 *
 * <p>Primitives that overlap due to the delays being shorter than the previous segments will be
 * dropped from the effect here. Relative timings will still use the dropped primitives to preserve
 * the design intention.
 */
final class PrimitiveDelayAdapter implements VibrationSegmentsAdapter {

    PrimitiveDelayAdapter() {
    }

    @Override
    public int adaptToVibrator(VibratorInfo info, List<VibrationEffectSegment> segments,
            int repeatIndex) {
        if (!Flags.primitiveCompositionAbsoluteDelay()) {
            return repeatIndex;
        }
        int previousStartOffset = 0;
        int segmentCount = segments.size();
        for (int i = 0; i < segmentCount; i++) {
            VibrationEffectSegment segment = segments.get(i);
            if (i == repeatIndex) {
                // Crossed the repeat line, reset start offset so repeating block is independent.
                previousStartOffset = 0;
            }

            if (!(segment instanceof PrimitiveSegment primitive)
                    || (primitive.getDelayType() == DELAY_TYPE_PAUSE)) {
                // Effect will play normally, keep track of its start offset.
                previousStartOffset = -calculateEffectDuration(info, segment);
                continue;
            }

            int pause = calculatePause(primitive, previousStartOffset);
            if (pause >= 0) {
                segments.set(i, toPrimitiveWithPause(primitive, pause));
                // Delay will be ignored from this calculation.
                previousStartOffset = -calculateEffectDuration(info, primitive);
            } else {
                // Primitive overlapping with previous segment, ignore it.
                segments.remove(i);
                if (repeatIndex > i) {
                    repeatIndex--;
                }
                segmentCount--;
                i--;

                // Keep the intended start time for future calculations. Here is an example:
                // 10 20 30 40 50 60 70 | Timeline (D = relative delay, E = effect duration)
                //  D  E  E  E  E       | D=10, E=40 | offset = 0   | pause = 10  | OK
                //     D  E  E          | D=10, E=20 | offset = -40 | pause = -30 | IGNORED
                //        D  E  E       | D=10, E=20 | offset = -30 | pause = -20 | IGNORED
                //           D  E  E    | D=10, E=20 | offset = -20 | pause = -10 | IGNORED
                //              D  E  E | D=10, E=20 | offset = -10 | pause = 0   | OK
                previousStartOffset = pause;
            }
        }
        return repeatIndex;
    }

    private static int calculatePause(PrimitiveSegment primitive, int previousStartOffset) {
        if (primitive.getDelayType() == DELAY_TYPE_RELATIVE_START_OFFSET) {
            return previousStartOffset + primitive.getDelay();
        }
        return primitive.getDelay();
    }

    private static int calculateEffectDuration(VibratorInfo info, VibrationEffectSegment segment) {
        long segmentDuration = segment.getDuration(info);
        if (segmentDuration < 0) {
            // Duration unknown, default to zero.
            return 0;
        }
        int effectDuration = (int) segmentDuration;
        if (segment instanceof PrimitiveSegment primitive) {
            // Ignore primitive delays from effect duration.
            effectDuration -= primitive.getDelay();
        }
        return effectDuration;
    }

    private static PrimitiveSegment toPrimitiveWithPause(PrimitiveSegment primitive, int pause) {
        return new PrimitiveSegment(primitive.getPrimitiveId(), primitive.getScale(),
                pause, DELAY_TYPE_PAUSE);
    }
}
Loading