Loading core/api/current.txt +3 −0 Original line number Diff line number Diff line Loading @@ -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 core/java/android/os/VibrationEffect.java +90 −1 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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; Loading Loading @@ -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); } Loading Loading @@ -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); }; } } /** Loading core/java/android/os/vibrator/PrimitiveSegment.java +43 −9 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import android.os.VibratorInfo; import com.android.internal.util.Preconditions; import java.util.Locale; import java.util.Objects; /** Loading @@ -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() { Loading @@ -70,6 +81,11 @@ public final class PrimitiveSegment extends VibrationEffectSegment { return mDelay; } /** @hide */ public int getDelayType() { return mDelayType; } @Override public long getDuration() { return -1; Loading Loading @@ -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 */ Loading @@ -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 */ Loading @@ -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 Loading @@ -150,6 +165,7 @@ public final class PrimitiveSegment extends VibrationEffectSegment { dest.writeInt(mPrimitiveId); dest.writeFloat(mScale); dest.writeInt(mDelay); dest.writeInt(mDelayType); } @Override Loading @@ -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 Loading @@ -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 Loading services/core/java/com/android/server/vibrator/DeviceAdapter.java +5 −2 Original line number Diff line number Diff line Loading @@ -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(), Loading @@ -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()]; Loading services/core/java/com/android/server/vibrator/PrimitiveDelayAdapter.java 0 → 100644 +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
core/api/current.txt +3 −0 Original line number Diff line number Diff line Loading @@ -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
core/java/android/os/VibrationEffect.java +90 −1 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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; Loading Loading @@ -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); } Loading Loading @@ -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); }; } } /** Loading
core/java/android/os/vibrator/PrimitiveSegment.java +43 −9 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import android.os.VibratorInfo; import com.android.internal.util.Preconditions; import java.util.Locale; import java.util.Objects; /** Loading @@ -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() { Loading @@ -70,6 +81,11 @@ public final class PrimitiveSegment extends VibrationEffectSegment { return mDelay; } /** @hide */ public int getDelayType() { return mDelayType; } @Override public long getDuration() { return -1; Loading Loading @@ -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 */ Loading @@ -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 */ Loading @@ -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 Loading @@ -150,6 +165,7 @@ public final class PrimitiveSegment extends VibrationEffectSegment { dest.writeInt(mPrimitiveId); dest.writeFloat(mScale); dest.writeInt(mDelay); dest.writeInt(mDelayType); } @Override Loading @@ -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 Loading @@ -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 Loading
services/core/java/com/android/server/vibrator/DeviceAdapter.java +5 −2 Original line number Diff line number Diff line Loading @@ -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(), Loading @@ -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()]; Loading
services/core/java/com/android/server/vibrator/PrimitiveDelayAdapter.java 0 → 100644 +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); } }