diff options
39 files changed, 1258 insertions, 273 deletions
diff --git a/Android.bp b/Android.bp index eabd9c7565da..cf73451d444e 100644 --- a/Android.bp +++ b/Android.bp @@ -255,7 +255,7 @@ java_library { "android.hardware.vibrator-V1.1-java", "android.hardware.vibrator-V1.2-java", "android.hardware.vibrator-V1.3-java", - "android.hardware.vibrator-V2-java", + "android.hardware.vibrator-V3-java", "android.se.omapi-V1-java", "android.system.suspend.control.internal-java", "devicepolicyprotosnano", diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 36a335e97d33..fd0262ef5771 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -409,6 +409,7 @@ package android { field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String USE_ON_DEVICE_INTELLIGENCE = "android.permission.USE_ON_DEVICE_INTELLIGENCE"; field public static final String USE_RESERVED_DISK = "android.permission.USE_RESERVED_DISK"; field public static final String UWB_PRIVILEGED = "android.permission.UWB_PRIVILEGED"; + field @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public static final String VIBRATE_VENDOR_EFFECTS = "android.permission.VIBRATE_VENDOR_EFFECTS"; field public static final String WHITELIST_AUTO_REVOKE_PERMISSIONS = "android.permission.WHITELIST_AUTO_REVOKE_PERMISSIONS"; field public static final String WHITELIST_RESTRICTED_PERMISSIONS = "android.permission.WHITELIST_RESTRICTED_PERMISSIONS"; field public static final String WIFI_ACCESS_COEX_UNSAFE_CHANNELS = "android.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS"; @@ -11354,6 +11355,10 @@ package android.os { field @NonNull public static final android.os.Parcelable.Creator<android.os.UserManager.EnforcingUser> CREATOR; } + public abstract class VibrationEffect implements android.os.Parcelable { + method @FlaggedApi("android.os.vibrator.vendor_vibration_effects") @NonNull @RequiresPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS) public static android.os.VibrationEffect createVendorEffect(@NonNull android.os.PersistableBundle); + } + public abstract class Vibrator { method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void addVibratorStateListener(@NonNull android.os.Vibrator.OnVibratorStateChangedListener); method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void addVibratorStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.os.Vibrator.OnVibratorStateChangedListener); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 88b5275d37f8..90af25984e6b 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -2577,6 +2577,16 @@ package android.os { public static final class VibrationEffect.Composition.UnreachableAfterRepeatingIndefinitelyException extends java.lang.IllegalStateException { } + @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public static final class VibrationEffect.VendorEffect extends android.os.VibrationEffect { + method @Nullable public long[] computeCreateWaveformOffOnTimingsOrNull(); + method public long getDuration(); + method public int getEffectStrength(); + method public float getLinearScale(); + method @NonNull public android.os.PersistableBundle getVendorData(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.os.VibrationEffect.VendorEffect> CREATOR; + } + public static class VibrationEffect.VibrationParameter { method @NonNull public static android.os.VibrationEffect.VibrationParameter targetAmplitude(@FloatRange(from=0, to=1) float); method @NonNull public static android.os.VibrationEffect.VibrationParameter targetFrequency(@FloatRange(from=1) float); diff --git a/core/java/android/os/CombinedVibration.java b/core/java/android/os/CombinedVibration.java index f32a1f831a07..77d6cb762e06 100644 --- a/core/java/android/os/CombinedVibration.java +++ b/core/java/android/os/CombinedVibration.java @@ -18,6 +18,7 @@ package android.os; import android.annotation.NonNull; import android.annotation.TestApi; +import android.os.vibrator.Flags; import android.util.SparseArray; import com.android.internal.util.Preconditions; @@ -152,6 +153,9 @@ public abstract class CombinedVibration implements Parcelable { /** @hide */ public abstract boolean hasVibrator(int vibratorId); + /** @hide */ + public abstract boolean hasVendorEffects(); + /** * Returns a compact version of the {@link #toString()} result for debugging purposes. * @@ -424,6 +428,15 @@ public abstract class CombinedVibration implements Parcelable { return true; } + /** @hide */ + @Override + public boolean hasVendorEffects() { + if (!Flags.vendorVibrationEffects()) { + return false; + } + return mEffect instanceof VibrationEffect.VendorEffect; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -605,6 +618,20 @@ public abstract class CombinedVibration implements Parcelable { return mEffects.indexOfKey(vibratorId) >= 0; } + /** @hide */ + @Override + public boolean hasVendorEffects() { + if (!Flags.vendorVibrationEffects()) { + return false; + } + for (int i = 0; i < mEffects.size(); i++) { + if (mEffects.get(i) instanceof VibrationEffect.VendorEffect) { + return true; + } + } + return false; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -838,6 +865,17 @@ public abstract class CombinedVibration implements Parcelable { return false; } + /** @hide */ + @Override + public boolean hasVendorEffects() { + for (int i = 0; i < mEffects.size(); i++) { + if (mEffects.get(i).hasVendorEffects()) { + return true; + } + } + return false; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java index efbd96bc35cb..44edf298beeb 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -16,18 +16,25 @@ package android.os; +import static android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS; + +import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.ContentResolver; import android.content.Context; +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.Flags; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.RampSegment; @@ -46,6 +53,7 @@ import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.StringJoiner; +import java.util.function.BiFunction; /** * A VibrationEffect describes a haptic effect to be performed by a {@link Vibrator}. @@ -53,6 +61,9 @@ import java.util.StringJoiner; * <p>These effects may be any number of things, from single shot vibrations to complex waveforms. */ public abstract class VibrationEffect implements Parcelable { + private static final int PARCEL_TOKEN_COMPOSED = 1; + private static final int PARCEL_TOKEN_VENDOR_EFFECT = 2; + // Stevens' coefficient to scale the perceived vibration intensity. private static final float SCALE_GAMMA = 0.65f; // If a vibration is playing for longer than 1s, it's probably not haptic feedback @@ -316,6 +327,28 @@ public abstract class VibrationEffect implements Parcelable { } /** + * Create a vendor-defined vibration effect. + * + * <p>Vendor effects offer more flexibility for accessing vendor-specific vibrator capabilities, + * enabling control over any vibration parameter and more generic vibration waveforms for apps + * provided by the device vendor. + * + * <p>This requires hardware-specific implementation of the effect and will not have any + * platform fallback support. + * + * @param effect An opaque representation of the vibration effect which can also be serialized. + * @return The desired effect. + * @hide + */ + @NonNull + @SystemApi + @FlaggedApi(FLAG_VENDOR_VIBRATION_EFFECTS) + @RequiresPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS) + public static VibrationEffect createVendorEffect(@NonNull PersistableBundle effect) { + return new VendorEffect(effect, VendorEffect.DEFAULT_STRENGTH, VendorEffect.DEFAULT_SCALE); + } + + /** * Get a predefined vibration effect. * * <p>Predefined effects are a set of common vibration effects that should be identical, @@ -508,7 +541,7 @@ public abstract class VibrationEffect implements Parcelable { * Gets the estimated duration of the vibration in milliseconds. * * <p>For effects without a defined end (e.g. a Waveform with a non-negative repeat index), this - * returns Long.MAX_VALUE. For effects with an unknown duration (e.g. Prebaked effects where + * returns Long.MAX_VALUE. For effects with an unknown duration (e.g. predefined effects where * the length is device and potentially run-time dependent), this returns -1. * * @hide @@ -550,7 +583,19 @@ public abstract class VibrationEffect implements Parcelable { * * @hide */ - public abstract <T extends VibrationEffect> T resolve(int defaultAmplitude); + @NonNull + public abstract VibrationEffect resolve(int defaultAmplitude); + + /** + * Applies given effect strength to predefined and vendor-specific effects. + * + * @param effectStrength new effect strength to be applied, one of + * VibrationEffect.EFFECT_STRENGTH_*. + * @return this if there is no change, or a copy of this effect with new strength otherwise + * @hide + */ + @NonNull + public abstract VibrationEffect applyEffectStrength(int effectStrength); /** * Scale the vibration effect intensity with the given constraints. @@ -562,7 +607,20 @@ public abstract class VibrationEffect implements Parcelable { * * @hide */ - public abstract <T extends VibrationEffect> T scale(float scaleFactor); + @NonNull + public abstract VibrationEffect scale(float scaleFactor); + + /** + * Performs a linear scaling on the effect intensity with the given factor. + * + * @param scaleFactor scale factor to be applied to the intensity. Values within [0,1) will + * scale down the intensity, values larger than 1 will scale up + * @return this if there is no scaling to be done, or a copy of this effect with scaled + * vibration intensity otherwise + * @hide + */ + @NonNull + public abstract VibrationEffect scaleLinearly(float scaleFactor); /** * Ensures that the effect is repeating indefinitely or not. This is a lossy operation and @@ -651,38 +709,26 @@ public abstract class VibrationEffect implements Parcelable { /** @hide */ public static String effectIdToString(int effectId) { - switch (effectId) { - case EFFECT_CLICK: - return "CLICK"; - case EFFECT_TICK: - return "TICK"; - case EFFECT_HEAVY_CLICK: - return "HEAVY_CLICK"; - case EFFECT_DOUBLE_CLICK: - return "DOUBLE_CLICK"; - case EFFECT_POP: - return "POP"; - case EFFECT_THUD: - return "THUD"; - case EFFECT_TEXTURE_TICK: - return "TEXTURE_TICK"; - default: - return Integer.toString(effectId); - } + return switch (effectId) { + case EFFECT_CLICK -> "CLICK"; + case EFFECT_TICK -> "TICK"; + case EFFECT_HEAVY_CLICK -> "HEAVY_CLICK"; + case EFFECT_DOUBLE_CLICK -> "DOUBLE_CLICK"; + case EFFECT_POP -> "POP"; + case EFFECT_THUD -> "THUD"; + case EFFECT_TEXTURE_TICK -> "TEXTURE_TICK"; + default -> Integer.toString(effectId); + }; } /** @hide */ public static String effectStrengthToString(int effectStrength) { - switch (effectStrength) { - case EFFECT_STRENGTH_LIGHT: - return "LIGHT"; - case EFFECT_STRENGTH_MEDIUM: - return "MEDIUM"; - case EFFECT_STRENGTH_STRONG: - return "STRONG"; - default: - return Integer.toString(effectStrength); - } + return switch (effectStrength) { + case EFFECT_STRENGTH_LIGHT -> "LIGHT"; + case EFFECT_STRENGTH_MEDIUM -> "MEDIUM"; + case EFFECT_STRENGTH_STRONG -> "STRONG"; + default -> Integer.toString(effectStrength); + }; } /** @@ -712,12 +758,15 @@ public abstract class VibrationEffect implements Parcelable { private final ArrayList<VibrationEffectSegment> mSegments; private final int mRepeatIndex; + /** @hide */ Composed(@NonNull Parcel in) { - this(in.readArrayList( - VibrationEffectSegment.class.getClassLoader(), VibrationEffectSegment.class), + this(Objects.requireNonNull(in.readArrayList( + VibrationEffectSegment.class.getClassLoader(), + VibrationEffectSegment.class)), in.readInt()); } + /** @hide */ Composed(@NonNull VibrationEffectSegment segment) { this(Arrays.asList(segment), /* repeatIndex= */ -1); } @@ -844,7 +893,7 @@ public abstract class VibrationEffect implements Parcelable { } int segmentCount = mSegments.size(); if (segmentCount > MAX_HAPTIC_FEEDBACK_COMPOSITION_SIZE) { - // Vibration has some prebaked or primitive constants, it should be limited to the + // Vibration has some predefined or primitive constants, it should be limited to the // max composition size used to classify haptic feedbacks. return false; } @@ -867,34 +916,28 @@ public abstract class VibrationEffect implements Parcelable { @NonNull @Override public Composed resolve(int defaultAmplitude) { - int segmentCount = mSegments.size(); - ArrayList<VibrationEffectSegment> resolvedSegments = new ArrayList<>(segmentCount); - for (int i = 0; i < segmentCount; i++) { - resolvedSegments.add(mSegments.get(i).resolve(defaultAmplitude)); - } - if (resolvedSegments.equals(mSegments)) { - return this; - } - Composed resolved = new Composed(resolvedSegments, mRepeatIndex); - resolved.validate(); - return resolved; + return applyToSegments(VibrationEffectSegment::resolve, defaultAmplitude); + } + + /** @hide */ + @NonNull + @Override + public VibrationEffect applyEffectStrength(int effectStrength) { + return applyToSegments(VibrationEffectSegment::applyEffectStrength, effectStrength); } /** @hide */ @NonNull @Override public Composed scale(float scaleFactor) { - int segmentCount = mSegments.size(); - ArrayList<VibrationEffectSegment> scaledSegments = new ArrayList<>(segmentCount); - for (int i = 0; i < segmentCount; i++) { - scaledSegments.add(mSegments.get(i).scale(scaleFactor)); - } - if (scaledSegments.equals(mSegments)) { - return this; - } - Composed scaled = new Composed(scaledSegments, mRepeatIndex); - scaled.validate(); - return scaled; + return applyToSegments(VibrationEffectSegment::scale, scaleFactor); + } + + /** @hide */ + @NonNull + @Override + public Composed scaleLinearly(float scaleFactor) { + return applyToSegments(VibrationEffectSegment::scaleLinearly, scaleFactor); } /** @hide */ @@ -926,10 +969,9 @@ public abstract class VibrationEffect implements Parcelable { if (this == o) { return true; } - if (!(o instanceof Composed)) { + if (!(o instanceof Composed other)) { return false; } - Composed other = (Composed) o; return mSegments.equals(other.mSegments) && mRepeatIndex == other.mRepeatIndex; } @@ -969,6 +1011,7 @@ public abstract class VibrationEffect implements Parcelable { @Override public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeInt(PARCEL_TOKEN_COMPOSED); out.writeList(mSegments); out.writeInt(mRepeatIndex); } @@ -1011,6 +1054,208 @@ public abstract class VibrationEffect implements Parcelable { return stepSegment; } + + private <T> Composed applyToSegments( + BiFunction<VibrationEffectSegment, T, VibrationEffectSegment> function, T param) { + int segmentCount = mSegments.size(); + ArrayList<VibrationEffectSegment> updatedSegments = new ArrayList<>(segmentCount); + for (int i = 0; i < segmentCount; i++) { + updatedSegments.add(function.apply(mSegments.get(i), param)); + } + if (mSegments.equals(updatedSegments)) { + return this; + } + Composed updated = new Composed(updatedSegments, mRepeatIndex); + updated.validate(); + return updated; + } + } + + /** + * Implementation of {@link VibrationEffect} described by a generic {@link PersistableBundle} + * defined by vendors. + * + * @hide + */ + @TestApi + @FlaggedApi(FLAG_VENDOR_VIBRATION_EFFECTS) + public static final class VendorEffect extends VibrationEffect { + /** @hide */ + public static final int DEFAULT_STRENGTH = VibrationEffect.EFFECT_STRENGTH_MEDIUM; + /** @hide */ + public static final float DEFAULT_SCALE = 1.0f; + + private final PersistableBundle mVendorData; + private final int mEffectStrength; + private final float mLinearScale; + + /** @hide */ + VendorEffect(@NonNull Parcel in) { + this(Objects.requireNonNull( + in.readPersistableBundle(VibrationEffect.class.getClassLoader())), + in.readInt(), in.readFloat()); + } + + /** @hide */ + public VendorEffect(@NonNull PersistableBundle vendorData, int effectStrength, + float linearScale) { + mVendorData = vendorData; + mEffectStrength = effectStrength; + mLinearScale = linearScale; + } + + @NonNull + public PersistableBundle getVendorData() { + return mVendorData; + } + + public int getEffectStrength() { + return mEffectStrength; + } + + public float getLinearScale() { + return mLinearScale; + } + + /** @hide */ + @Override + @Nullable + public long[] computeCreateWaveformOffOnTimingsOrNull() { + return null; + } + + /** @hide */ + @Override + public void validate() { + Preconditions.checkArgument(!mVendorData.isEmpty(), + "Vendor effect bundle must be non-empty"); + } + + @Override + public long getDuration() { + return -1; // UNKNOWN + } + + /** @hide */ + @Override + public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) { + return vibratorInfo.hasCapability(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); + } + + /** @hide */ + @Override + public boolean isHapticFeedbackCandidate() { + return false; + } + + /** @hide */ + @NonNull + @Override + public VendorEffect resolve(int defaultAmplitude) { + return this; + } + + /** @hide */ + @NonNull + @Override + public VibrationEffect applyEffectStrength(int effectStrength) { + if (mEffectStrength == effectStrength) { + return this; + } + VendorEffect updated = new VendorEffect(mVendorData, effectStrength, mLinearScale); + updated.validate(); + return updated; + } + + /** @hide */ + @NonNull + @Override + public VendorEffect scale(float scaleFactor) { + // Vendor effect strength cannot be scaled with this method. + return this; + } + + /** @hide */ + @NonNull + @Override + public VibrationEffect scaleLinearly(float scaleFactor) { + if (Float.compare(mLinearScale, scaleFactor) == 0) { + return this; + } + VendorEffect updated = new VendorEffect(mVendorData, mEffectStrength, scaleFactor); + updated.validate(); + return updated; + } + + /** @hide */ + @NonNull + @Override + public VendorEffect applyRepeatingIndefinitely(boolean wantRepeating, int loopDelayMs) { + return this; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof VendorEffect other)) { + return false; + } + return mEffectStrength == other.mEffectStrength + && (Float.compare(mLinearScale, other.mLinearScale) == 0) + // Make sure it calls unparcel for both before calling BaseBundle.kindofEquals. + && mVendorData.size() == other.mVendorData.size() + && BaseBundle.kindofEquals(mVendorData, other.mVendorData); + } + + @Override + public int hashCode() { + // PersistableBundle does not implement hashCode, so use its size as a shortcut. + return Objects.hash(mVendorData.size(), mEffectStrength, mLinearScale); + } + + @Override + public String toString() { + return String.format(Locale.ROOT, + "VendorEffect{vendorData=%s, strength=%s, scale=%.2f}", + mVendorData, effectStrengthToString(mEffectStrength), mLinearScale); + } + + /** @hide */ + @Override + public String toDebugString() { + return String.format(Locale.ROOT, "vendorEffect=%s, strength=%s, scale=%.2f", + mVendorData.toShortString(), effectStrengthToString(mEffectStrength), + mLinearScale); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeInt(PARCEL_TOKEN_VENDOR_EFFECT); + out.writePersistableBundle(mVendorData); + out.writeInt(mEffectStrength); + out.writeFloat(mLinearScale); + } + + @NonNull + public static final Creator<VendorEffect> CREATOR = + new Creator<VendorEffect>() { + @Override + public VendorEffect createFromParcel(Parcel in) { + return new VendorEffect(in); + } + + @Override + public VendorEffect[] newArray(int size) { + return new VendorEffect[size]; + } + }; } /** @@ -1249,7 +1494,9 @@ public abstract class VibrationEffect implements Parcelable { if (mRepeatIndex >= 0) { throw new UnreachableAfterRepeatingIndefinitelyException(); } - Composed composed = (Composed) effect; + if (!(effect instanceof Composed composed)) { + throw new IllegalArgumentException("Can't add vendor effects to composition."); + } if (composed.getRepeatIndex() >= 0) { // Start repeating from the index relative to the composed waveform. mRepeatIndex = mSegments.size() + composed.getRepeatIndex(); @@ -1285,28 +1532,18 @@ public abstract class VibrationEffect implements Parcelable { * @hide */ public static String primitiveToString(@PrimitiveType int id) { - switch (id) { - case PRIMITIVE_NOOP: - return "NOOP"; - case PRIMITIVE_CLICK: - return "CLICK"; - case PRIMITIVE_THUD: - return "THUD"; - case PRIMITIVE_SPIN: - return "SPIN"; - case PRIMITIVE_QUICK_RISE: - return "QUICK_RISE"; - case PRIMITIVE_SLOW_RISE: - return "SLOW_RISE"; - case PRIMITIVE_QUICK_FALL: - return "QUICK_FALL"; - case PRIMITIVE_TICK: - return "TICK"; - case PRIMITIVE_LOW_TICK: - return "LOW_TICK"; - default: - return Integer.toString(id); - } + return switch (id) { + case PRIMITIVE_NOOP -> "NOOP"; + case PRIMITIVE_CLICK -> "CLICK"; + case PRIMITIVE_THUD -> "THUD"; + case PRIMITIVE_SPIN -> "SPIN"; + case PRIMITIVE_QUICK_RISE -> "QUICK_RISE"; + case PRIMITIVE_SLOW_RISE -> "SLOW_RISE"; + case PRIMITIVE_QUICK_FALL -> "QUICK_FALL"; + case PRIMITIVE_TICK -> "TICK"; + case PRIMITIVE_LOW_TICK -> "LOW_TICK"; + default -> Integer.toString(id); + }; } } @@ -1640,7 +1877,17 @@ public abstract class VibrationEffect implements Parcelable { new Parcelable.Creator<VibrationEffect>() { @Override public VibrationEffect createFromParcel(Parcel in) { - return new Composed(in); + switch (in.readInt()) { + case PARCEL_TOKEN_COMPOSED: + return new Composed(in); + case PARCEL_TOKEN_VENDOR_EFFECT: + if (Flags.vendorVibrationEffects()) { + return new VendorEffect(in); + } // else fall through + default: + throw new IllegalStateException( + "Unexpected vibration effect type token in parcel."); + } } @Override public VibrationEffect[] newArray(int size) { diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig index ad2f59db46ff..f4e2a7e28d8c 100644 --- a/core/java/android/os/vibrator/flags.aconfig +++ b/core/java/android/os/vibrator/flags.aconfig @@ -53,3 +53,14 @@ flag { purpose: PURPOSE_FEATURE } } + +flag { + namespace: "haptics" + name: "vendor_vibration_effects" + is_exported: true + description: "Enabled System APIs for vendor-defined vibration effects" + bug: "345454923" + metadata { + purpose: PURPOSE_FEATURE + } +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 193836ed9b15..f3dac2313c91 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2611,6 +2611,14 @@ <permission android:name="android.permission.VIBRATE_SYSTEM_CONSTANTS" android:protectionLevel="signature" /> + <!-- @SystemApi Allows access to perform vendor effects in the vibrator. + <p>Protection level: signature + @FlaggedApi("android.os.vibrator.vendor_vibration_effects") + @hide + --> + <permission android:name="android.permission.VIBRATE_VENDOR_EFFECTS" + android:protectionLevel="signature|privileged" /> + <!-- @SystemApi Allows access to the vibrator state. <p>Protection level: signature @hide diff --git a/core/tests/vibrator/Android.bp b/core/tests/vibrator/Android.bp index 3ebe150acd24..920ab5914548 100644 --- a/core/tests/vibrator/Android.bp +++ b/core/tests/vibrator/Android.bp @@ -18,6 +18,7 @@ android_test { "androidx.test.ext.junit", "androidx.test.runner", "androidx.test.rules", + "flag-junit", "mockito-target-minus-junit4", "truth", "testng", diff --git a/core/tests/vibrator/src/android/os/VibrationEffectTest.java b/core/tests/vibrator/src/android/os/VibrationEffectTest.java index e8758754f059..098ade4c1334 100644 --- a/core/tests/vibrator/src/android/os/VibrationEffectTest.java +++ b/core/tests/vibrator/src/android/os/VibrationEffectTest.java @@ -20,6 +20,8 @@ import static android.os.VibrationEffect.DEFAULT_AMPLITUDE; import static android.os.VibrationEffect.VibrationParameter.targetAmplitude; import static android.os.VibrationEffect.VibrationParameter.targetFrequency; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; @@ -29,6 +31,7 @@ import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertThrows; import android.content.ContentInterface; @@ -38,8 +41,12 @@ import android.content.res.Resources; import android.hardware.vibrator.IVibrator; import android.net.Uri; import android.os.VibrationEffect.Composition.UnreachableAfterRepeatingIndefinitelyException; +import android.os.vibrator.Flags; +import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.StepSegment; +import android.os.vibrator.VibrationEffectSegment; +import android.platform.test.annotations.RequiresFlagsEnabled; import com.android.internal.R; @@ -284,10 +291,13 @@ public class VibrationEffectTest { } @Test - public void computeLegacyPattern_notPatternPased() { - VibrationEffect effect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK); - - assertNull(effect.computeCreateWaveformOffOnTimingsOrNull()); + public void computeLegacyPattern_notPatternBased() { + assertNull(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK) + .computeCreateWaveformOffOnTimingsOrNull()); + if (Flags.vendorVibrationEffects()) { + assertNull(VibrationEffect.createVendorEffect(createNonEmptyBundle()) + .computeCreateWaveformOffOnTimingsOrNull()); + } } @Test @@ -472,6 +482,18 @@ public class VibrationEffectTest { } @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void testValidateVendorEffect() { + PersistableBundle vendorData = new PersistableBundle(); + vendorData.putInt("key", 1); + VibrationEffect.createVendorEffect(vendorData).validate(); + + PersistableBundle emptyData = new PersistableBundle(); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.createVendorEffect(emptyData).validate()); + } + + @Test public void testValidateWaveform() { VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -1).validate(); VibrationEffect.createWaveform(new long[]{10, 10}, new int[] {0, 0}, -1).validate(); @@ -634,16 +656,16 @@ public class VibrationEffectTest { @Test public void testResolveOneShot() { - VibrationEffect.Composed resolved = DEFAULT_ONE_SHOT.resolve(51); - assertEquals(0.2f, ((StepSegment) resolved.getSegments().get(0)).getAmplitude()); + VibrationEffect resolved = DEFAULT_ONE_SHOT.resolve(51); + assertEquals(0.2f, getStepSegment(resolved, 0).getAmplitude()); assertThrows(IllegalArgumentException.class, () -> DEFAULT_ONE_SHOT.resolve(1000)); } @Test public void testResolveWaveform() { - VibrationEffect.Composed resolved = TEST_WAVEFORM.resolve(102); - assertEquals(0.4f, ((StepSegment) resolved.getSegments().get(2)).getAmplitude()); + VibrationEffect resolved = TEST_WAVEFORM.resolve(102); + assertEquals(0.4f, getStepSegment(resolved, 2).getAmplitude()); assertThrows(IllegalArgumentException.class, () -> TEST_WAVEFORM.resolve(1000)); } @@ -655,63 +677,127 @@ public class VibrationEffectTest { } @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void testResolveVendorEffect() { + VibrationEffect effect = VibrationEffect.createVendorEffect(createNonEmptyBundle()); + assertEquals(effect, effect.resolve(51)); + } + + @Test public void testResolveComposed() { VibrationEffect effect = VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 1) .compose(); assertEquals(effect, effect.resolve(51)); - VibrationEffect.Composed resolved = VibrationEffect.startComposition() + VibrationEffect resolved = VibrationEffect.startComposition() .addEffect(DEFAULT_ONE_SHOT) .compose() .resolve(51); - assertEquals(0.2f, ((StepSegment) resolved.getSegments().get(0)).getAmplitude()); + assertEquals(0.2f, getStepSegment(resolved, 0).getAmplitude()); } @Test public void testScaleOneShot() { - VibrationEffect.Composed scaledUp = TEST_ONE_SHOT.scale(1.5f); - assertTrue(100 / 255f < ((StepSegment) scaledUp.getSegments().get(0)).getAmplitude()); + VibrationEffect scaledUp = TEST_ONE_SHOT.scale(1.5f); + assertTrue(100 / 255f < getStepSegment(scaledUp, 0).getAmplitude()); - VibrationEffect.Composed scaledDown = TEST_ONE_SHOT.scale(0.5f); - assertTrue(100 / 255f > ((StepSegment) scaledDown.getSegments().get(0)).getAmplitude()); + VibrationEffect scaledDown = TEST_ONE_SHOT.scale(0.5f); + assertTrue(100 / 255f > getStepSegment(scaledDown, 0).getAmplitude()); } @Test public void testScaleWaveform() { - VibrationEffect.Composed scaledUp = TEST_WAVEFORM.scale(1.5f); - assertEquals(1f, ((StepSegment) scaledUp.getSegments().get(0)).getAmplitude(), 1e-5f); + VibrationEffect scaledUp = TEST_WAVEFORM.scale(1.5f); + assertEquals(1f, getStepSegment(scaledUp, 0).getAmplitude(), 1e-5f); - VibrationEffect.Composed scaledDown = TEST_WAVEFORM.scale(0.5f); - assertTrue(1f > ((StepSegment) scaledDown.getSegments().get(0)).getAmplitude()); + VibrationEffect scaledDown = TEST_WAVEFORM.scale(0.5f); + assertTrue(1f > getStepSegment(scaledDown, 0).getAmplitude()); } @Test public void testScalePrebaked() { VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); - VibrationEffect.Composed scaledUp = effect.scale(1.5f); + VibrationEffect scaledUp = effect.scale(1.5f); assertEquals(effect, scaledUp); - VibrationEffect.Composed scaledDown = effect.scale(0.5f); + VibrationEffect scaledDown = effect.scale(0.5f); + assertEquals(effect, scaledDown); + } + + @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void testScaleVendorEffect() { + VibrationEffect effect = VibrationEffect.createVendorEffect(createNonEmptyBundle()); + + VibrationEffect scaledUp = effect.scale(1.5f); + assertEquals(effect, scaledUp); + + VibrationEffect scaledDown = effect.scale(0.5f); assertEquals(effect, scaledDown); } @Test public void testScaleComposed() { - VibrationEffect.Composed effect = - (VibrationEffect.Composed) VibrationEffect.startComposition() + VibrationEffect effect = VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 1) .addEffect(TEST_ONE_SHOT) .compose(); - VibrationEffect.Composed scaledUp = effect.scale(1.5f); - assertTrue(0.5f < ((PrimitiveSegment) scaledUp.getSegments().get(0)).getScale()); - assertTrue(100 / 255f < ((StepSegment) scaledUp.getSegments().get(1)).getAmplitude()); + VibrationEffect scaledUp = effect.scale(1.5f); + assertTrue(0.5f < getPrimitiveSegment(scaledUp, 0).getScale()); + assertTrue(100 / 255f < getStepSegment(scaledUp, 1).getAmplitude()); + + VibrationEffect scaledDown = effect.scale(0.5f); + assertTrue(0.5f > getPrimitiveSegment(scaledDown, 0).getScale()); + assertTrue(100 / 255f > getStepSegment(scaledDown, 1).getAmplitude()); + } + + @Test + public void testApplyEffectStrengthToOneShotWaveformAndPrimitives() { + VibrationEffect oneShot = VibrationEffect.createOneShot(100, 100); + VibrationEffect waveform = VibrationEffect.createWaveform(new long[] { 10, 20 }, 0); + VibrationEffect composition = VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .compose(); + + assertEquals(oneShot, oneShot.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_STRONG)); + assertEquals(waveform, + waveform.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_STRONG)); + assertEquals(composition, + composition.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_STRONG)); + } + + @Test + public void testApplyEffectStrengthToPredefinedEffect() { + VibrationEffect effect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK); + + VibrationEffect scaledUp = + effect.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_STRONG); + assertNotEquals(effect, scaledUp); + assertEquals(VibrationEffect.EFFECT_STRENGTH_STRONG, + getPrebakedSegment(scaledUp, 0).getEffectStrength()); + + VibrationEffect scaledDown = + effect.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_LIGHT); + assertNotEquals(effect, scaledDown); + assertEquals(VibrationEffect.EFFECT_STRENGTH_LIGHT, + getPrebakedSegment(scaledDown, 0).getEffectStrength()); + } - VibrationEffect.Composed scaledDown = effect.scale(0.5f); - assertTrue(0.5f > ((PrimitiveSegment) scaledDown.getSegments().get(0)).getScale()); - assertTrue(100 / 255f > ((StepSegment) scaledDown.getSegments().get(1)).getAmplitude()); + @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void testApplyEffectStrengthToVendorEffect() { + VibrationEffect effect = VibrationEffect.createVendorEffect(createNonEmptyBundle()); + + VibrationEffect scaledUp = + effect.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_STRONG); + assertNotEquals(effect, scaledUp); + + VibrationEffect scaledDown = + effect.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_LIGHT); + assertNotEquals(effect, scaledDown); } private void doTestApplyRepeatingWithNonRepeatingOriginal(@NotNull VibrationEffect original) { @@ -819,6 +905,15 @@ public class VibrationEffectTest { } @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void testApplyRepeatingIndefinitely_vendorEffect() { + VibrationEffect effect = VibrationEffect.createVendorEffect(createNonEmptyBundle()); + + assertEquals(effect, effect.applyRepeatingIndefinitely(true, 10)); + assertEquals(effect, effect.applyRepeatingIndefinitely(false, 10)); + } + + @Test public void testDuration() { assertEquals(1, VibrationEffect.createOneShot(1, 1).getDuration()); assertEquals(-1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK).getDuration()); @@ -832,6 +927,10 @@ public class VibrationEffectTest { new long[]{1, 2, 3}, new int[]{1, 2, 3}, -1).getDuration()); assertEquals(Long.MAX_VALUE, VibrationEffect.createWaveform( new long[]{1, 2, 3}, new int[]{1, 2, 3}, 0).getDuration()); + if (Flags.vendorVibrationEffects()) { + assertEquals(-1, + VibrationEffect.createVendorEffect(createNonEmptyBundle()).getDuration()); + } } @Test @@ -872,6 +971,19 @@ public class VibrationEffectTest { } @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void testAreVibrationFeaturesSupported_vendorEffects() { + VibratorInfo supportedVibratorInfo = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS) + .build(); + + assertTrue(VibrationEffect.createVendorEffect(createNonEmptyBundle()) + .areVibrationFeaturesSupported(supportedVibratorInfo)); + assertFalse(VibrationEffect.createVendorEffect(createNonEmptyBundle()) + .areVibrationFeaturesSupported(new VibratorInfo.Builder(/* id= */ 1).build())); + } + + @Test public void testIsHapticFeedbackCandidate_repeatingEffects_notCandidates() { assertFalse(VibrationEffect.createWaveform( new long[]{1, 2, 3}, new int[]{1, 2, 3}, 0).isHapticFeedbackCandidate()); @@ -952,6 +1064,13 @@ public class VibrationEffectTest { assertTrue(VibrationEffect.get(VibrationEffect.EFFECT_TICK).isHapticFeedbackCandidate()); } + @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void testIsHapticFeedbackCandidate_vendorEffects_notCandidates() { + assertFalse(VibrationEffect.createVendorEffect(createNonEmptyBundle()) + .isHapticFeedbackCandidate()); + } + private void assertArrayEq(long[] expected, long[] actual) { assertTrue( String.format("Expected pattern %s, but was %s", @@ -992,4 +1111,35 @@ public class VibrationEffectTest { return context; } + + private StepSegment getStepSegment(VibrationEffect effect, int index) { + VibrationEffectSegment segment = getEffectSegment(effect, index); + assertThat(segment).isInstanceOf(StepSegment.class); + return (StepSegment) segment; + } + + private PrimitiveSegment getPrimitiveSegment(VibrationEffect effect, int index) { + VibrationEffectSegment segment = getEffectSegment(effect, index); + assertThat(segment).isInstanceOf(PrimitiveSegment.class); + return (PrimitiveSegment) segment; + } + + private PrebakedSegment getPrebakedSegment(VibrationEffect effect, int index) { + VibrationEffectSegment segment = getEffectSegment(effect, index); + assertThat(segment).isInstanceOf(PrebakedSegment.class); + return (PrebakedSegment) segment; + } + + private VibrationEffectSegment getEffectSegment(VibrationEffect effect, int index) { + assertThat(effect).isInstanceOf(VibrationEffect.Composed.class); + VibrationEffect.Composed composed = (VibrationEffect.Composed) effect; + assertThat(index).isLessThan(composed.getSegments().size()); + return composed.getSegments().get(index); + } + + private PersistableBundle createNonEmptyBundle() { + PersistableBundle bundle = new PersistableBundle(); + bundle.putInt("key", 1); + return bundle; + } } diff --git a/services/core/Android.bp b/services/core/Android.bp index 9d4310c21cf9..363c1d8c5f04 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -148,7 +148,7 @@ java_library_static { "android.hardware.common-V2-java", "android.hardware.light-V2.0-java", "android.hardware.gnss-V2-java", - "android.hardware.vibrator-V2-java", + "android.hardware.vibrator-V3-java", "app-compat-annotations", "framework-tethering.stubs.module_lib", "keepanno-annotations", diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index bb2efa166800..36a9c80717f2 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -1355,8 +1355,7 @@ public class InputManagerService extends IInputManager.Stub int patternRepeatIndex = -1; int amplitudeCount = -1; - if (effect instanceof VibrationEffect.Composed) { - VibrationEffect.Composed composed = (VibrationEffect.Composed) effect; + if (effect instanceof VibrationEffect.Composed composed) { int segmentCount = composed.getSegments().size(); pattern = new long[segmentCount]; amplitudes = new int[segmentCount]; @@ -1381,6 +1380,8 @@ public class InputManagerService extends IInputManager.Stub } pattern[amplitudeCount++] = segment.getDuration(); } + } else { + Slog.w(TAG, "Input devices don't support effect " + effect); } if (amplitudeCount < 0) { diff --git a/services/core/java/com/android/server/vibrator/AbstractComposedVibratorStep.java b/services/core/java/com/android/server/vibrator/AbstractComposedVibratorStep.java new file mode 100644 index 000000000000..b2631597dd76 --- /dev/null +++ b/services/core/java/com/android/server/vibrator/AbstractComposedVibratorStep.java @@ -0,0 +1,89 @@ +/* + * 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 android.os.SystemClock; +import android.os.VibrationEffect; + +import java.util.List; + +/** + * Represent a step on a single vibrator that plays one or more segments from a + * {@link VibrationEffect.Composed} effect. + */ +abstract class AbstractComposedVibratorStep extends AbstractVibratorStep { + public final VibrationEffect.Composed effect; + public final int segmentIndex; + + /** + * @param conductor The {@link VibrationStepConductor} for these steps. + * @param startTime The time to schedule this step in the conductor. + * @param controller The vibrator that is playing the effect. + * @param effect The effect being played in this step. + * @param index The index of the next segment to be played by this step + * @param pendingVibratorOffDeadline The time the vibrator is expected to complete any + * previous vibration and turn off. This is used to allow this step to + * be triggered when the completion callback is received, and can + * be used to play effects back-to-back. + */ + AbstractComposedVibratorStep(VibrationStepConductor conductor, long startTime, + VibratorController controller, VibrationEffect.Composed effect, int index, + long pendingVibratorOffDeadline) { + super(conductor, startTime, controller, pendingVibratorOffDeadline); + this.effect = effect; + this.segmentIndex = index; + } + + /** + * Return the {@link VibrationStepConductor#nextVibrateStep} with start and off timings + * calculated from {@link #getVibratorOnDuration()} based on the current + * {@link SystemClock#uptimeMillis()} and jumping all played segments from the effect. + */ + protected List<Step> nextSteps(int segmentsPlayed) { + // Schedule next steps to run right away. + long nextStartTime = SystemClock.uptimeMillis(); + if (mVibratorOnResult > 0) { + // Vibrator was turned on by this step, with mVibratorOnResult as the duration. + // Schedule next steps for right after the vibration finishes. + nextStartTime += mVibratorOnResult; + } + return nextSteps(nextStartTime, segmentsPlayed); + } + + /** + * Return the {@link VibrationStepConductor#nextVibrateStep} with given start time, + * which might be calculated independently, and jumping all played segments from the effect. + * + * <p>This should be used when the vibrator on/off state is not responsible for the step + * execution timing, e.g. while playing the vibrator amplitudes. + */ + protected List<Step> nextSteps(long nextStartTime, int segmentsPlayed) { + int nextSegmentIndex = segmentIndex + segmentsPlayed; + int effectSize = effect.getSegments().size(); + int repeatIndex = effect.getRepeatIndex(); + if (nextSegmentIndex >= effectSize && repeatIndex >= 0) { + // Count the loops that were played. + int loopSize = effectSize - repeatIndex; + int loopSegmentsPlayed = nextSegmentIndex - repeatIndex; + getVibration().stats.reportRepetition(loopSegmentsPlayed / loopSize); + nextSegmentIndex = repeatIndex + ((nextSegmentIndex - effectSize) % loopSize); + } + Step nextStep = conductor.nextVibrateStep(nextStartTime, controller, effect, + nextSegmentIndex, mPendingVibratorOffDeadline); + return List.of(nextStep); + } +} diff --git a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java index 90b6f95f8740..42203b113498 100644 --- a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java @@ -16,21 +16,16 @@ package com.android.server.vibrator; +import android.annotation.NonNull; import android.os.SystemClock; -import android.os.VibrationEffect; import android.util.Slog; import java.util.Arrays; import java.util.List; -/** - * Represent a step on a single vibrator that plays one or more segments from a - * {@link VibrationEffect.Composed} effect. - */ +/** Represent a step on a single vibrator that plays a command on {@link VibratorController}. */ abstract class AbstractVibratorStep extends Step { public final VibratorController controller; - public final VibrationEffect.Composed effect; - public final int segmentIndex; long mVibratorOnResult; long mPendingVibratorOffDeadline; @@ -41,20 +36,15 @@ abstract class AbstractVibratorStep extends Step { * @param startTime The time to schedule this step in the * {@link VibrationStepConductor}. * @param controller The vibrator that is playing the effect. - * @param effect The effect being played in this step. - * @param index The index of the next segment to be played by this step * @param pendingVibratorOffDeadline The time the vibrator is expected to complete any * previous vibration and turn off. This is used to allow this step to * be triggered when the completion callback is received, and can * be used to play effects back-to-back. */ AbstractVibratorStep(VibrationStepConductor conductor, long startTime, - VibratorController controller, VibrationEffect.Composed effect, int index, - long pendingVibratorOffDeadline) { + VibratorController controller, long pendingVibratorOffDeadline) { super(conductor, startTime); this.controller = controller; - this.effect = effect; - this.segmentIndex = index; mPendingVibratorOffDeadline = pendingVibratorOffDeadline; } @@ -88,6 +78,7 @@ abstract class AbstractVibratorStep extends Step { return shouldAcceptCallback; } + @NonNull @Override public List<Step> cancel() { return Arrays.asList(new CompleteEffectVibratorStep(conductor, SystemClock.uptimeMillis(), @@ -138,43 +129,4 @@ abstract class AbstractVibratorStep extends Step { controller.setAmplitude(amplitude); getVibration().stats.reportSetAmplitude(); } - - /** - * Return the {@link VibrationStepConductor#nextVibrateStep} with start and off timings - * calculated from {@link #getVibratorOnDuration()} based on the current - * {@link SystemClock#uptimeMillis()} and jumping all played segments from the effect. - */ - protected List<Step> nextSteps(int segmentsPlayed) { - // Schedule next steps to run right away. - long nextStartTime = SystemClock.uptimeMillis(); - if (mVibratorOnResult > 0) { - // Vibrator was turned on by this step, with mVibratorOnResult as the duration. - // Schedule next steps for right after the vibration finishes. - nextStartTime += mVibratorOnResult; - } - return nextSteps(nextStartTime, segmentsPlayed); - } - - /** - * Return the {@link VibrationStepConductor#nextVibrateStep} with given start time, - * which might be calculated independently, and jumping all played segments from the effect. - * - * <p>This should be used when the vibrator on/off state is not responsible for the step - * execution timing, e.g. while playing the vibrator amplitudes. - */ - protected List<Step> nextSteps(long nextStartTime, int segmentsPlayed) { - int nextSegmentIndex = segmentIndex + segmentsPlayed; - int effectSize = effect.getSegments().size(); - int repeatIndex = effect.getRepeatIndex(); - if (nextSegmentIndex >= effectSize && repeatIndex >= 0) { - // Count the loops that were played. - int loopSize = effectSize - repeatIndex; - int loopSegmentsPlayed = nextSegmentIndex - repeatIndex; - getVibration().stats.reportRepetition(loopSegmentsPlayed / loopSize); - nextSegmentIndex = repeatIndex + ((nextSegmentIndex - effectSize) % loopSize); - } - Step nextStep = conductor.nextVibrateStep(nextStartTime, controller, effect, - nextSegmentIndex, mPendingVibratorOffDeadline); - return nextStep == null ? VibrationStepConductor.EMPTY_STEP_LIST : Arrays.asList(nextStep); - } } diff --git a/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java b/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java index 48dd992008d2..7f9c349b6d10 100644 --- a/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java @@ -16,6 +16,7 @@ package com.android.server.vibrator; +import android.annotation.NonNull; import android.os.SystemClock; import android.os.Trace; import android.os.VibrationEffect; @@ -35,8 +36,7 @@ final class CompleteEffectVibratorStep extends AbstractVibratorStep { CompleteEffectVibratorStep(VibrationStepConductor conductor, long startTime, boolean cancelled, VibratorController controller, long pendingVibratorOffDeadline) { - super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1, - pendingVibratorOffDeadline); + super(conductor, startTime, controller, pendingVibratorOffDeadline); mCancelled = cancelled; } @@ -47,6 +47,7 @@ final class CompleteEffectVibratorStep extends AbstractVibratorStep { return mCancelled; } + @NonNull @Override public List<Step> cancel() { if (mCancelled) { @@ -57,6 +58,7 @@ final class CompleteEffectVibratorStep extends AbstractVibratorStep { return super.cancel(); } + @NonNull @Override public List<Step> play() { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "CompleteEffectVibratorStep"); diff --git a/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java index 940bd08eee4b..e495af59a2f9 100644 --- a/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java @@ -16,6 +16,7 @@ package com.android.server.vibrator; +import android.annotation.NonNull; import android.os.Trace; import android.os.VibrationEffect; import android.os.vibrator.PrimitiveSegment; @@ -31,7 +32,7 @@ import java.util.List; * <p>This step will use the maximum supported number of consecutive segments of type * {@link PrimitiveSegment} starting at the current index. */ -final class ComposePrimitivesVibratorStep extends AbstractVibratorStep { +final class ComposePrimitivesVibratorStep extends AbstractComposedVibratorStep { /** * Default limit to the number of primitives in a composition, if none is defined by the HAL, * to prevent repeating effects from generating an infinite list. @@ -47,6 +48,7 @@ final class ComposePrimitivesVibratorStep extends AbstractVibratorStep { index, pendingVibratorOffDeadline); } + @NonNull @Override public List<Step> play() { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePrimitivesStep"); diff --git a/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java index 5d572be69246..e8952fafaf77 100644 --- a/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java @@ -16,6 +16,7 @@ package com.android.server.vibrator; +import android.annotation.NonNull; import android.os.Trace; import android.os.VibrationEffect; import android.os.vibrator.RampSegment; @@ -31,7 +32,7 @@ import java.util.List; * <p>This step will use the maximum supported number of consecutive segments of type * {@link RampSegment}, starting at the current index. */ -final class ComposePwleVibratorStep extends AbstractVibratorStep { +final class ComposePwleVibratorStep extends AbstractComposedVibratorStep { /** * Default limit to the number of PWLE segments, if none is defined by the HAL, to prevent * repeating effects from generating an infinite list. @@ -47,6 +48,7 @@ final class ComposePwleVibratorStep extends AbstractVibratorStep { index, pendingVibratorOffDeadline); } + @NonNull @Override public List<Step> play() { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePwleStep"); diff --git a/services/core/java/com/android/server/vibrator/DeviceAdapter.java b/services/core/java/com/android/server/vibrator/DeviceAdapter.java index 98309cd00758..bd4fc07fe816 100644 --- a/services/core/java/com/android/server/vibrator/DeviceAdapter.java +++ b/services/core/java/com/android/server/vibrator/DeviceAdapter.java @@ -21,7 +21,6 @@ import android.os.CombinedVibration; import android.os.VibrationEffect; import android.os.VibratorInfo; import android.os.vibrator.VibrationEffectSegment; -import android.util.Slog; import android.util.SparseArray; import java.util.ArrayList; @@ -82,9 +81,8 @@ final class DeviceAdapter implements CombinedVibration.VibratorAdapter { @NonNull @Override public VibrationEffect adaptToVibrator(int vibratorId, @NonNull VibrationEffect effect) { - if (!(effect instanceof VibrationEffect.Composed)) { + if (!(effect instanceof VibrationEffect.Composed composed)) { // Segments adapters can only apply to Composed effects. - Slog.wtf(TAG, "Error adapting unsupported vibration effect: " + effect); return effect; } @@ -95,7 +93,6 @@ final class DeviceAdapter implements CombinedVibration.VibratorAdapter { } VibratorInfo info = controller.getVibratorInfo(); - VibrationEffect.Composed composed = (VibrationEffect.Composed) effect; List<VibrationEffectSegment> newSegments = new ArrayList<>(composed.getSegments()); int newRepeatIndex = composed.getRepeatIndex(); diff --git a/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java b/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java index c9683d9f69ed..6456371a52fe 100644 --- a/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java +++ b/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java @@ -16,6 +16,7 @@ package com.android.server.vibrator; +import android.annotation.NonNull; import android.os.Trace; import android.util.Slog; @@ -43,6 +44,7 @@ final class FinishSequentialEffectStep extends Step { return true; } + @NonNull @Override public List<Step> play() { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "FinishSequentialEffectStep"); @@ -61,6 +63,7 @@ final class FinishSequentialEffectStep extends Step { } } + @NonNull @Override public List<Step> cancel() { cancelImmediately(); diff --git a/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java index 8094e7c5c58e..4b23216258af 100644 --- a/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java @@ -16,6 +16,7 @@ package com.android.server.vibrator; +import android.annotation.NonNull; import android.os.Trace; import android.os.VibrationEffect; import android.os.vibrator.PrebakedSegment; @@ -31,7 +32,7 @@ import java.util.List; * <p>This step automatically falls back by replacing the prebaked segment with * {@link VibrationSettings#getFallbackEffect(int)}, if available. */ -final class PerformPrebakedVibratorStep extends AbstractVibratorStep { +final class PerformPrebakedVibratorStep extends AbstractComposedVibratorStep { PerformPrebakedVibratorStep(VibrationStepConductor conductor, long startTime, VibratorController controller, VibrationEffect.Composed effect, int index, @@ -42,6 +43,7 @@ final class PerformPrebakedVibratorStep extends AbstractVibratorStep { index, pendingVibratorOffDeadline); } + @NonNull @Override public List<Step> play() { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "PerformPrebakedVibratorStep"); diff --git a/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java b/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java new file mode 100644 index 000000000000..8f36118543ed --- /dev/null +++ b/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java @@ -0,0 +1,61 @@ +/* + * 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 android.annotation.NonNull; +import android.os.Trace; +import android.os.VibrationEffect; + +import java.util.List; + +/** + * Represents a step to turn the vibrator on with a vendor-specific vibration from a + * {@link VibrationEffect.VendorEffect} effect. + */ +final class PerformVendorEffectVibratorStep extends AbstractVibratorStep { + /** + * Timeout to ensure vendor vibrations are not unbounded if vibrator callbacks are lost. + */ + static final long VENDOR_EFFECT_MAX_DURATION_MS = 60_000; // 1 min + + public final VibrationEffect.VendorEffect effect; + + PerformVendorEffectVibratorStep(VibrationStepConductor conductor, long startTime, + VibratorController controller, VibrationEffect.VendorEffect effect, + long pendingVibratorOffDeadline) { + // This step should wait for the last vibration to finish (with the timeout) and for the + // intended step start time (to respect the effect delays). + super(conductor, Math.max(startTime, pendingVibratorOffDeadline), controller, + pendingVibratorOffDeadline); + this.effect = effect; + } + + @NonNull + @Override + public List<Step> play() { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "PerformVendorEffectVibratorStep"); + try { + long vibratorOnResult = controller.on(effect, getVibration().id); + vibratorOnResult = Math.min(vibratorOnResult, VENDOR_EFFECT_MAX_DURATION_MS); + handleVibratorOnResult(vibratorOnResult); + return List.of(new CompleteEffectVibratorStep(conductor, startTime, + /* cancelled= */ false, controller, mPendingVibratorOffDeadline)); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } +} diff --git a/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java b/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java index f40c994d687e..901f9c3f7137 100644 --- a/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java @@ -16,6 +16,7 @@ package com.android.server.vibrator; +import android.annotation.NonNull; import android.os.SystemClock; import android.os.Trace; import android.util.Slog; @@ -31,8 +32,7 @@ final class RampOffVibratorStep extends AbstractVibratorStep { RampOffVibratorStep(VibrationStepConductor conductor, long startTime, float amplitudeTarget, float amplitudeDelta, VibratorController controller, long pendingVibratorOffDeadline) { - super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1, - pendingVibratorOffDeadline); + super(conductor, startTime, controller, pendingVibratorOffDeadline); mAmplitudeTarget = amplitudeTarget; mAmplitudeDelta = amplitudeDelta; } @@ -42,12 +42,14 @@ final class RampOffVibratorStep extends AbstractVibratorStep { return true; } + @NonNull @Override public List<Step> cancel() { return Arrays.asList(new TurnOffVibratorStep(conductor, SystemClock.uptimeMillis(), controller, /* isCleanUp= */ true)); } + @NonNull @Override public List<Step> play() { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "RampOffVibratorStep"); diff --git a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java index e13ec6c2d4ce..8478e7743183 100644 --- a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java @@ -16,6 +16,7 @@ package com.android.server.vibrator; +import android.annotation.NonNull; import android.os.SystemClock; import android.os.Trace; import android.os.VibrationEffect; @@ -32,7 +33,7 @@ import java.util.List; * <p>This step ignores vibration completion callbacks and control the vibrator on/off state * and amplitude to simulate waveforms represented by a sequence of {@link StepSegment}. */ -final class SetAmplitudeVibratorStep extends AbstractVibratorStep { +final class SetAmplitudeVibratorStep extends AbstractComposedVibratorStep { /** * The repeating waveform keeps the vibrator ON all the time. Use a minimum duration to * prevent short patterns from turning the vibrator ON too frequently. @@ -69,6 +70,7 @@ final class SetAmplitudeVibratorStep extends AbstractVibratorStep { return shouldAcceptCallback; } + @NonNull @Override public List<Step> play() { // TODO: consider separating the "on" steps at the start into a separate Step. diff --git a/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java b/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java index c197271f3c7d..3ceba576fca3 100644 --- a/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java +++ b/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java @@ -16,6 +16,7 @@ package com.android.server.vibrator; +import android.annotation.NonNull; import android.annotation.Nullable; import android.hardware.vibrator.IVibratorManager; import android.os.CombinedVibration; @@ -74,6 +75,7 @@ final class StartSequentialEffectStep extends Step { return mVibratorsOnMaxDuration; } + @NonNull @Override public List<Step> play() { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "StartSequentialEffectStep"); @@ -111,6 +113,7 @@ final class StartSequentialEffectStep extends Step { return nextSteps; } + @NonNull @Override public List<Step> cancel() { return VibrationStepConductor.EMPTY_STEP_LIST; @@ -173,13 +176,12 @@ final class StartSequentialEffectStep extends Step { for (int i = 0; i < vibratorCount; i++) { steps[i] = conductor.nextVibrateStep(vibrationStartTime, conductor.getVibrators().get(effectMapping.vibratorIdAt(i)), - effectMapping.effectAt(i), - /* segmentIndex= */ 0, /* vibratorOffTimeout= */ 0); + effectMapping.effectAt(i)); } if (steps.length == 1) { // No need to prepare and trigger sync effects on a single vibrator. - return startVibrating(steps[0], nextSteps); + return startVibrating(steps[0], effectMapping.effectAt(0), nextSteps); } // This synchronization of vibrators should be executed one at a time, even if we are @@ -196,8 +198,8 @@ final class StartSequentialEffectStep extends Step { effectMapping.getRequiredSyncCapabilities(), effectMapping.getVibratorIds()); - for (AbstractVibratorStep step : steps) { - long duration = startVibrating(step, nextSteps); + for (int i = 0; i < vibratorCount; i++) { + long duration = startVibrating(steps[i], effectMapping.effectAt(i), nextSteps); if (duration < 0) { // One vibrator has failed, fail this entire sync attempt. hasFailed = true; @@ -231,7 +233,12 @@ final class StartSequentialEffectStep extends Step { return hasFailed ? -1 : maxDuration; } - private long startVibrating(AbstractVibratorStep step, List<Step> nextSteps) { + private long startVibrating(@Nullable AbstractVibratorStep step, VibrationEffect effect, + List<Step> nextSteps) { + if (step == null) { + // Failed to create a step for VibrationEffect. + return -1; + } nextSteps.addAll(step.play()); long stepDuration = step.getVibratorOnDuration(); if (stepDuration < 0) { @@ -239,7 +246,7 @@ final class StartSequentialEffectStep extends Step { return stepDuration; } // Return the longest estimation for the entire effect. - return Math.max(stepDuration, step.effect.getDuration()); + return Math.max(stepDuration, effect.getDuration()); } /** @@ -249,28 +256,20 @@ final class StartSequentialEffectStep extends Step { * play all of the effects in sync. */ final class DeviceEffectMap { - private final SparseArray<VibrationEffect.Composed> mVibratorEffects; + private final SparseArray<VibrationEffect> mVibratorEffects; private final int[] mVibratorIds; private final long mRequiredSyncCapabilities; DeviceEffectMap(CombinedVibration.Mono mono) { SparseArray<VibratorController> vibrators = conductor.getVibrators(); VibrationEffect effect = mono.getEffect(); - if (effect instanceof VibrationEffect.Composed) { - mVibratorEffects = new SparseArray<>(vibrators.size()); - mVibratorIds = new int[vibrators.size()]; - - VibrationEffect.Composed composedEffect = (VibrationEffect.Composed) effect; - for (int i = 0; i < vibrators.size(); i++) { - int vibratorId = vibrators.keyAt(i); - mVibratorEffects.put(vibratorId, composedEffect); - mVibratorIds[i] = vibratorId; - } - } else { - Slog.wtf(VibrationThread.TAG, - "Unable to map device vibrators to unexpected effect: " + effect); - mVibratorEffects = new SparseArray<>(); - mVibratorIds = new int[0]; + mVibratorEffects = new SparseArray<>(vibrators.size()); + mVibratorIds = new int[vibrators.size()]; + + for (int i = 0; i < vibrators.size(); i++) { + int vibratorId = vibrators.keyAt(i); + mVibratorEffects.put(vibratorId, effect); + mVibratorIds[i] = vibratorId; } mRequiredSyncCapabilities = calculateRequiredSyncCapabilities(mVibratorEffects); } @@ -282,13 +281,7 @@ final class StartSequentialEffectStep extends Step { for (int i = 0; i < stereoEffects.size(); i++) { int vibratorId = stereoEffects.keyAt(i); if (vibrators.contains(vibratorId)) { - VibrationEffect effect = stereoEffects.valueAt(i); - if (effect instanceof VibrationEffect.Composed) { - mVibratorEffects.put(vibratorId, (VibrationEffect.Composed) effect); - } else { - Slog.wtf(VibrationThread.TAG, - "Unable to map device vibrators to unexpected effect: " + effect); - } + mVibratorEffects.put(vibratorId, stereoEffects.valueAt(i)); } } mVibratorIds = new int[mVibratorEffects.size()]; @@ -326,7 +319,7 @@ final class StartSequentialEffectStep extends Step { } /** Return the {@link VibrationEffect} at given index. */ - public VibrationEffect.Composed effectAt(int index) { + public VibrationEffect effectAt(int index) { return mVibratorEffects.valueAt(index); } @@ -338,16 +331,24 @@ final class StartSequentialEffectStep extends Step { * IVibratorManager.CAP_PREPARE_* and IVibratorManager.CAP_MIXED_TRIGGER_* capabilities. */ private long calculateRequiredSyncCapabilities( - SparseArray<VibrationEffect.Composed> effects) { + SparseArray<VibrationEffect> effects) { long prepareCap = 0; for (int i = 0; i < effects.size(); i++) { - VibrationEffectSegment firstSegment = effects.valueAt(i).getSegments().get(0); - if (firstSegment instanceof StepSegment) { - prepareCap |= IVibratorManager.CAP_PREPARE_ON; - } else if (firstSegment instanceof PrebakedSegment) { + VibrationEffect effect = effects.valueAt(i); + if (effect instanceof VibrationEffect.VendorEffect) { prepareCap |= IVibratorManager.CAP_PREPARE_PERFORM; - } else if (firstSegment instanceof PrimitiveSegment) { - prepareCap |= IVibratorManager.CAP_PREPARE_COMPOSE; + } else if (effect instanceof VibrationEffect.Composed composed) { + VibrationEffectSegment firstSegment = composed.getSegments().get(0); + if (firstSegment instanceof StepSegment) { + prepareCap |= IVibratorManager.CAP_PREPARE_ON; + } else if (firstSegment instanceof PrebakedSegment) { + prepareCap |= IVibratorManager.CAP_PREPARE_PERFORM; + } else if (firstSegment instanceof PrimitiveSegment) { + prepareCap |= IVibratorManager.CAP_PREPARE_COMPOSE; + } + } else { + Slog.wtf(VibrationThread.TAG, + "Unable to check sync capabilities to unexpected effect: " + effect); } } int triggerCap = 0; diff --git a/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java b/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java index 065ce1124674..87dc269532bd 100644 --- a/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java @@ -16,6 +16,7 @@ package com.android.server.vibrator; +import android.annotation.NonNull; import android.os.SystemClock; import android.os.Trace; @@ -36,7 +37,7 @@ final class TurnOffVibratorStep extends AbstractVibratorStep { TurnOffVibratorStep(VibrationStepConductor conductor, long startTime, VibratorController controller, boolean isCleanUp) { - super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1, startTime); + super(conductor, startTime, controller, startTime); mIsCleanUp = isCleanUp; } @@ -45,6 +46,7 @@ final class TurnOffVibratorStep extends AbstractVibratorStep { return mIsCleanUp; } + @NonNull @Override public List<Step> cancel() { return Arrays.asList(new TurnOffVibratorStep(conductor, SystemClock.uptimeMillis(), @@ -56,6 +58,7 @@ final class TurnOffVibratorStep extends AbstractVibratorStep { stopVibrating(); } + @NonNull @Override public List<Step> play() { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "TurnOffVibratorStep"); diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java index 689b495ec1ca..5c567da7844f 100644 --- a/services/core/java/com/android/server/vibrator/Vibration.java +++ b/services/core/java/com/android/server/vibrator/Vibration.java @@ -393,13 +393,14 @@ abstract class Vibration { private void dumpEffect( ProtoOutputStream proto, long fieldId, VibrationEffect effect) { - final long token = proto.start(fieldId); - VibrationEffect.Composed composed = (VibrationEffect.Composed) effect; - for (VibrationEffectSegment segment : composed.getSegments()) { - dumpEffect(proto, VibrationEffectProto.SEGMENTS, segment); + if (effect instanceof VibrationEffect.Composed composed) { + final long token = proto.start(fieldId); + for (VibrationEffectSegment segment : composed.getSegments()) { + dumpEffect(proto, VibrationEffectProto.SEGMENTS, segment); + } + proto.write(VibrationEffectProto.REPEAT, composed.getRepeatIndex()); + proto.end(token); } - proto.write(VibrationEffectProto.REPEAT, composed.getRepeatIndex()); - proto.end(token); } private void dumpEffect(ProtoOutputStream proto, long fieldId, diff --git a/services/core/java/com/android/server/vibrator/VibrationScaler.java b/services/core/java/com/android/server/vibrator/VibrationScaler.java index d9ca71003aae..39337594ff64 100644 --- a/services/core/java/com/android/server/vibrator/VibrationScaler.java +++ b/services/core/java/com/android/server/vibrator/VibrationScaler.java @@ -25,14 +25,12 @@ import android.os.VibrationEffect; import android.os.Vibrator; import android.os.vibrator.Flags; import android.os.vibrator.PrebakedSegment; -import android.os.vibrator.VibrationEffectSegment; import android.util.IndentingPrintWriter; import android.util.Slog; import android.util.SparseArray; import android.util.proto.ProtoOutputStream; import java.io.PrintWriter; -import java.util.ArrayList; import java.util.Locale; /** Controls vibration scaling. */ @@ -136,12 +134,6 @@ final class VibrationScaler { */ @NonNull public VibrationEffect scale(@NonNull VibrationEffect effect, int usageHint) { - if (!(effect instanceof VibrationEffect.Composed)) { - // This only scales composed vibration effects. - Slog.wtf(TAG, "Error scaling unsupported vibration effect: " + effect); - return effect; - } - int newEffectStrength = getEffectStrength(usageHint); ScaleLevel scaleLevel = mScaleLevels.get(getScaleLevel(usageHint)); float adaptiveScale = getAdaptiveHapticsScale(usageHint); @@ -154,26 +146,10 @@ final class VibrationScaler { scaleLevel = SCALE_LEVEL_NONE; } - VibrationEffect.Composed composedEffect = (VibrationEffect.Composed) effect; - ArrayList<VibrationEffectSegment> segments = - new ArrayList<>(composedEffect.getSegments()); - int segmentCount = segments.size(); - for (int i = 0; i < segmentCount; i++) { - segments.set(i, - segments.get(i).resolve(mDefaultVibrationAmplitude) - .applyEffectStrength(newEffectStrength) - .scale(scaleLevel.factor) - .scaleLinearly(adaptiveScale)); - } - if (segments.equals(composedEffect.getSegments())) { - // No segment was updated, return original effect. - return effect; - } - VibrationEffect.Composed scaled = - new VibrationEffect.Composed(segments, composedEffect.getRepeatIndex()); - // Make sure we validate what was scaled, since we're using the constructor directly - scaled.validate(); - return scaled; + return effect.resolve(mDefaultVibrationAmplitude) + .applyEffectStrength(newEffectStrength) + .scale(scaleLevel.factor) + .scaleLinearly(adaptiveScale); } /** diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java index f3e226e09447..8c9a92de03a9 100644 --- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java +++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java @@ -123,6 +123,24 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { @Nullable AbstractVibratorStep nextVibrateStep(long startTime, VibratorController controller, + VibrationEffect effect) { + if (Build.IS_DEBUGGABLE) { + expectIsVibrationThread(true); + } + if (effect instanceof VibrationEffect.VendorEffect vendorEffect) { + return new PerformVendorEffectVibratorStep(this, startTime, controller, vendorEffect, + /* pendingVibratorOffDeadline= */ 0); + } + if (effect instanceof VibrationEffect.Composed composed) { + return nextVibrateStep(startTime, controller, composed, /* segmentIndex= */ 0, + /* pendingVibratorOffDeadline= */ 0); + } + Slog.wtf(TAG, "Unable to create next step for unexpected effect: " + effect); + return null; + } + + @NonNull + AbstractVibratorStep nextVibrateStep(long startTime, VibratorController controller, VibrationEffect.Composed effect, int segmentIndex, long pendingVibratorOffDeadline) { if (Build.IS_DEBUGGABLE) { expectIsVibrationThread(true); diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java index 988e8fea70b9..8cc157c2ed81 100644 --- a/services/core/java/com/android/server/vibrator/VibratorController.java +++ b/services/core/java/com/android/server/vibrator/VibratorController.java @@ -20,8 +20,10 @@ import android.annotation.Nullable; import android.hardware.vibrator.IVibrator; import android.os.Binder; import android.os.IVibratorStateListener; +import android.os.Parcel; import android.os.RemoteCallbackList; import android.os.RemoteException; +import android.os.VibrationEffect; import android.os.VibratorInfo; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; @@ -262,6 +264,35 @@ final class VibratorController { } /** + * Plays vendor vibration effect, using {@code vibrationId} for completion callback to + * {@link OnVibrationCompleteListener}. + * + * <p>This will affect the state of {@link #isVibrating()}. + * + * @return The positive duration of the vibration started, if successful, zero if the vibrator + * do not support the input or a negative number if the operation failed. + */ + public long on(VibrationEffect.VendorEffect vendorEffect, long vibrationId) { + synchronized (mLock) { + Parcel vendorData = Parcel.obtain(); + try { + vendorEffect.getVendorData().writeToParcel(vendorData, /* flags= */ 0); + vendorData.setDataPosition(0); + long duration = mNativeWrapper.performVendorEffect(vendorData, + vendorEffect.getEffectStrength(), vendorEffect.getLinearScale(), + vibrationId); + if (duration > 0) { + mCurrentAmplitude = -1; + notifyListenerOnVibrating(true); + } + return duration; + } finally { + vendorData.recycle(); + } + } + } + + /** * Plays predefined vibration effect, using {@code vibrationId} for completion callback to * {@link OnVibrationCompleteListener}. * @@ -427,6 +458,9 @@ final class VibratorController { private static native long performEffect(long nativePtr, long effect, long strength, long vibrationId); + private static native long performVendorEffect(long nativePtr, Parcel vendorData, + long strength, float scale, long vibrationId); + private static native long performComposedEffect(long nativePtr, PrimitiveSegment[] effect, long vibrationId); @@ -482,6 +516,12 @@ final class VibratorController { return performEffect(mNativePtr, effect, strength, vibrationId); } + /** Turns vibrator on to perform a vendor-specific effect. */ + public long performVendorEffect(Parcel vendorData, long strength, float scale, + long vibrationId) { + return performVendorEffect(mNativePtr, vendorData, strength, scale, vibrationId); + } + /** Turns vibrator on to perform effect composed of give primitives effect. */ public long compose(PrimitiveSegment[] primitives, long vibrationId) { return performComposedEffect(mNativePtr, primitives, vibrationId); diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index bff175fec1dd..48c4a68250b1 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -540,6 +540,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { Slog.e(TAG, "token must not be null"); return null; } + if (effect.hasVendorEffects() + && !hasPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS)) { + Slog.w(TAG, "vibrate; no permission for vendor effects"); + return null; + } enforceUpdateAppOpsStatsPermission(uid); if (!isEffectValid(effect)) { return null; @@ -1304,12 +1309,13 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } private void fillVibrationFallbacks(HalVibration vib, VibrationEffect effect) { - VibrationEffect.Composed composed = (VibrationEffect.Composed) effect; + if (!(effect instanceof VibrationEffect.Composed composed)) { + return; + } int segmentCount = composed.getSegments().size(); for (int i = 0; i < segmentCount; i++) { VibrationEffectSegment segment = composed.getSegments().get(i); - if (segment instanceof PrebakedSegment) { - PrebakedSegment prebaked = (PrebakedSegment) segment; + if (segment instanceof PrebakedSegment prebaked) { VibrationEffect fallback = mVibrationSettings.getFallbackEffect( prebaked.getEffectId()); if (prebaked.shouldFallback() && fallback != null) { @@ -1392,12 +1398,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @Nullable private static PrebakedSegment extractPrebakedSegment(VibrationEffect effect) { - if (effect instanceof VibrationEffect.Composed) { - VibrationEffect.Composed composed = (VibrationEffect.Composed) effect; + if (effect instanceof VibrationEffect.Composed composed) { if (composed.getSegments().size() == 1) { VibrationEffectSegment segment = composed.getSegments().get(0); - if (segment instanceof PrebakedSegment) { - return (PrebakedSegment) segment; + if (segment instanceof PrebakedSegment prebaked) { + return prebaked; } } } diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index 3cd5f7683ac8..9fa1a53237cc 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -193,7 +193,7 @@ cc_defaults { "android.hardware.thermal-V2-ndk", "android.hardware.tv.input@1.0", "android.hardware.tv.input-V2-ndk", - "android.hardware.vibrator-V2-ndk", + "android.hardware.vibrator-V3-ndk", "android.hardware.vibrator@1.0", "android.hardware.vibrator@1.1", "android.hardware.vibrator@1.2", diff --git a/services/core/jni/com_android_server_vibrator_VibratorController.cpp b/services/core/jni/com_android_server_vibrator_VibratorController.cpp index 2804a10c317f..f12930a49ecb 100644 --- a/services/core/jni/com_android_server_vibrator_VibratorController.cpp +++ b/services/core/jni/com_android_server_vibrator_VibratorController.cpp @@ -17,7 +17,10 @@ #define LOG_TAG "VibratorController" #include <aidl/android/hardware/vibrator/IVibrator.h> +#include <android/binder_parcel.h> +#include <android/binder_parcel_jni.h> #include <android/hardware/vibrator/1.3/IVibrator.h> +#include <android/persistable_bundle_aidl.h> #include <nativehelper/JNIHelp.h> #include <utils/Log.h> #include <utils/misc.h> @@ -32,6 +35,8 @@ namespace V1_0 = android::hardware::vibrator::V1_0; namespace V1_3 = android::hardware::vibrator::V1_3; namespace Aidl = aidl::android::hardware::vibrator; +using aidl::android::os::PersistableBundle; + namespace android { static JavaVM* sJvm = nullptr; @@ -95,7 +100,7 @@ static std::shared_ptr<vibrator::HalController> findVibrator(int32_t vibratorId) return nullptr; } auto result = manager->getVibrator(vibratorId); - return result.isOk() ? std::move(result.value()) : nullptr; + return result.isOk() ? result.value() : nullptr; } class VibratorControllerWrapper { @@ -192,6 +197,29 @@ static Aidl::CompositeEffect effectFromJavaPrimitive(JNIEnv* env, jobject primit return effect; } +static Aidl::VendorEffect vendorEffectFromJavaParcel(JNIEnv* env, jobject vendorData, + jlong strength, jfloat scale) { + PersistableBundle bundle; + if (AParcel* parcel = AParcel_fromJavaParcel(env, vendorData); parcel != nullptr) { + if (binder_status_t status = bundle.readFromParcel(parcel); status == STATUS_OK) { + AParcel_delete(parcel); + } else { + jniThrowExceptionFmt(env, "android/os/BadParcelableException", + "Failed to readFromParcel, status %d (%s)", status, + strerror(-status)); + } + } else { + jniThrowExceptionFmt(env, "android/os/BadParcelableException", + "Failed to AParcel_fromJavaParcel, for nullptr"); + } + + Aidl::VendorEffect effect; + effect.vendorData = bundle; + effect.strength = static_cast<Aidl::EffectStrength>(strength); + effect.scale = static_cast<float>(scale); + return effect; +} + static void destroyNativeWrapper(void* ptr) { VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr); if (wrapper) { @@ -289,6 +317,23 @@ static jlong vibratorPerformEffect(JNIEnv* env, jclass /* clazz */, jlong ptr, j return result.isOk() ? result.value().count() : (result.isUnsupported() ? 0 : -1); } +static jlong vibratorPerformVendorEffect(JNIEnv* env, jclass /* clazz */, jlong ptr, + jobject vendorData, jlong strength, jfloat scale, + jlong vibrationId) { + VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr); + if (wrapper == nullptr) { + ALOGE("vibratorPerformVendorEffect failed because native wrapper was not initialized"); + return -1; + } + Aidl::VendorEffect effect = vendorEffectFromJavaParcel(env, vendorData, strength, scale); + auto callback = wrapper->createCallback(vibrationId); + auto performVendorEffectFn = [&effect, &callback](vibrator::HalWrapper* hal) { + return hal->performVendorEffect(effect, callback); + }; + auto result = wrapper->halCall<void>(performVendorEffectFn, "performVendorEffect"); + return result.isOk() ? std::numeric_limits<int64_t>::max() : (result.isUnsupported() ? 0 : -1); +} + static jlong vibratorPerformComposedEffect(JNIEnv* env, jclass /* clazz */, jlong ptr, jobjectArray composition, jlong vibrationId) { VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr); @@ -466,6 +511,7 @@ static const JNINativeMethod method_table[] = { {"off", "(J)V", (void*)vibratorOff}, {"setAmplitude", "(JF)V", (void*)vibratorSetAmplitude}, {"performEffect", "(JJJJ)J", (void*)vibratorPerformEffect}, + {"performVendorEffect", "(JLandroid/os/Parcel;JFJ)J", (void*)vibratorPerformVendorEffect}, {"performComposedEffect", "(J[Landroid/os/vibrator/PrimitiveSegment;J)J", (void*)vibratorPerformComposedEffect}, {"performPwleEffect", "(J[Landroid/os/vibrator/RampSegment;IJ)J", diff --git a/services/tests/PackageManagerServiceTests/server/Android.bp b/services/tests/PackageManagerServiceTests/server/Android.bp index a738acb299c1..598e27372075 100644 --- a/services/tests/PackageManagerServiceTests/server/Android.bp +++ b/services/tests/PackageManagerServiceTests/server/Android.bp @@ -63,7 +63,7 @@ android_test { libs: [ "android.hardware.power-V1-java", "android.hardware.tv.cec-V1.0-java", - "android.hardware.vibrator-V2-java", + "android.hardware.vibrator-V3-java", "android.hidl.manager-V1.0-java", "android.test.mock", "android.test.base", diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index b9e99dd2e1e4..a888dadff5c6 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -94,7 +94,7 @@ android_test { libs: [ "android.hardware.power-V1-java", "android.hardware.tv.cec-V1.0-java", - "android.hardware.vibrator-V2-java", + "android.hardware.vibrator-V3-java", "android.hidl.manager-V1.0-java", "android.test.mock", "android.test.base", diff --git a/services/tests/vibrator/Android.bp b/services/tests/vibrator/Android.bp index da21cd3cf919..757bcd8e2193 100644 --- a/services/tests/vibrator/Android.bp +++ b/services/tests/vibrator/Android.bp @@ -16,7 +16,7 @@ android_test { ], libs: [ - "android.hardware.vibrator-V2-java", + "android.hardware.vibrator-V3-java", "android.test.mock", "android.test.base", "android.test.runner", @@ -36,7 +36,6 @@ android_test { "platform-test-annotations", "service-permission.stubs.system_server", "services.core", - "flag-junit", ], jni_libs: ["libdexmakerjvmtiagent"], platform_apis: true, diff --git a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java index 3013ed025bd9..59d557777f3b 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java @@ -25,6 +25,7 @@ import android.content.pm.PackageManagerInternal; import android.hardware.vibrator.IVibrator; import android.os.CombinedVibration; import android.os.Handler; +import android.os.PersistableBundle; import android.os.VibrationEffect; import android.os.test.TestLooper; import android.os.vibrator.PrebakedSegment; @@ -32,6 +33,7 @@ import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.RampSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationEffectSegment; +import android.platform.test.annotations.RequiresFlagsEnabled; import android.util.SparseArray; import androidx.test.InstrumentationRegistry; @@ -103,6 +105,17 @@ public class DeviceAdapterTest { } @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void testVendorEffect_returnsOriginalSegment() { + PersistableBundle vendorData = new PersistableBundle(); + vendorData.putInt("key", 1); + VibrationEffect effect = VibrationEffect.createVendorEffect(vendorData); + + assertThat(mAdapter.adaptToVibrator(EMPTY_VIBRATOR_ID, effect)).isEqualTo(effect); + assertThat(mAdapter.adaptToVibrator(PWLE_VIBRATOR_ID, effect)).isEqualTo(effect); + } + + @Test public void testStepAndRampSegments_withoutPwleCapability_convertsRampsToSteps() { VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList( // Step(amplitude, frequencyHz, duration) diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java index b2644350dfdd..9ebeaa8eb3fd 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java @@ -37,6 +37,7 @@ import android.content.ContextWrapper; import android.content.pm.PackageManagerInternal; import android.os.ExternalVibrationScale; import android.os.Handler; +import android.os.PersistableBundle; import android.os.PowerManagerInternal; import android.os.UserHandle; import android.os.VibrationAttributes; @@ -232,6 +233,34 @@ public class VibrationScalerTest { } @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void scale_withVendorEffect_setsEffectStrengthBasedOnSettings() { + setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_LOW); + setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, VIBRATION_INTENSITY_HIGH); + PersistableBundle vendorData = new PersistableBundle(); + vendorData.putString("key", "value"); + VibrationEffect effect = VibrationEffect.createVendorEffect(vendorData); + + VibrationEffect.VendorEffect scaled = + (VibrationEffect.VendorEffect) mVibrationScaler.scale(effect, USAGE_NOTIFICATION); + assertEquals(scaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_STRONG); + + setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, + VIBRATION_INTENSITY_MEDIUM); + scaled = (VibrationEffect.VendorEffect) mVibrationScaler.scale(effect, USAGE_NOTIFICATION); + assertEquals(scaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_MEDIUM); + + setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, VIBRATION_INTENSITY_LOW); + scaled = (VibrationEffect.VendorEffect) mVibrationScaler.scale(effect, USAGE_NOTIFICATION); + assertEquals(scaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_LIGHT); + + setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, VIBRATION_INTENSITY_OFF); + scaled = (VibrationEffect.VendorEffect) mVibrationScaler.scale(effect, USAGE_NOTIFICATION); + // Vibration setting being bypassed will use default setting. + assertEquals(scaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_LIGHT); + } + + @Test public void scale_withOneShotAndWaveform_resolvesAmplitude() { // No scale, default amplitude still resolved setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_LOW); @@ -365,6 +394,30 @@ public class VibrationScalerTest { assertTrue(scaled.getAmplitude() > 0.5); } + @Test + @RequiresFlagsEnabled({ + android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED, + android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS, + }) + public void scale_adaptiveHapticsOnVendorEffect_setsLinearScaleParameter() { + setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH); + + mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f); + + PersistableBundle vendorData = new PersistableBundle(); + vendorData.putInt("key", 1); + VibrationEffect effect = VibrationEffect.createVendorEffect(vendorData); + + VibrationEffect.VendorEffect scaled = + (VibrationEffect.VendorEffect) mVibrationScaler.scale(effect, USAGE_RINGTONE); + assertEquals(scaled.getLinearScale(), 0.5f); + + mVibrationScaler.removeAdaptiveHapticsScale(USAGE_RINGTONE); + + scaled = (VibrationEffect.VendorEffect) mVibrationScaler.scale(effect, USAGE_RINGTONE); + assertEquals(scaled.getLinearScale(), 1.0f); + } + private void setDefaultIntensity(@VibrationAttributes.Usage int usage, @Vibrator.VibrationIntensity int intensity) { when(mVibrationConfigMock.getDefaultVibrationIntensity(eq(usage))).thenReturn(intensity); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java index d7004e72bc52..3bd56deb32f4 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java @@ -48,6 +48,7 @@ import android.hardware.vibrator.IVibratorManager; import android.os.CombinedVibration; import android.os.Handler; import android.os.IBinder; +import android.os.PersistableBundle; import android.os.PowerManager; import android.os.Process; import android.os.SystemClock; @@ -560,8 +561,37 @@ public class VibrationThreadTest { // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. Thread cancellingThread = new Thread(() -> mVibrationConductor.notifyCancelled( - new Vibration.EndInfo( - Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE), + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE), + /* immediate= */ false)); + cancellingThread.start(); + + waitForCompletion(/* timeout= */ 50); + cancellingThread.join(); + + verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE); + assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); + } + + @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void vibrate_singleVibratorVendorEffectCancel_cancelsVibrationImmediately() + throws Exception { + mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); + // Set long vendor effect duration to check it gets cancelled quickly. + mVibratorProviders.get(VIBRATOR_ID).setVendorEffectDuration(10 * TEST_TIMEOUT_MILLIS); + + VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData()); + long vibrationId = startThreadAndDispatcher(effect); + + assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(), + TEST_TIMEOUT_MILLIS)); + assertTrue(mThread.isRunningVibrationId(vibrationId)); + + // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should + // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. + Thread cancellingThread = + new Thread(() -> mVibrationConductor.notifyCancelled( + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE), /* immediate= */ false)); cancellingThread.start(); @@ -588,8 +618,7 @@ public class VibrationThreadTest { // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. Thread cancellingThread = new Thread(() -> mVibrationConductor.notifyCancelled( - new Vibration.EndInfo( - Vibration.Status.CANCELLED_BY_SCREEN_OFF), + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SCREEN_OFF), /* immediate= */ false)); cancellingThread.start(); @@ -654,6 +683,27 @@ public class VibrationThreadTest { } @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void vibrate_singleVibratorVendorEffect_runsVibration() { + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); + + VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData()); + long vibrationId = startThreadAndDispatcher(effect); + waitForCompletion(); + + verify(mManagerHooks).noteVibratorOn(eq(UID), + eq(PerformVendorEffectVibratorStep.VENDOR_EFFECT_MAX_DURATION_MS)); + verify(mManagerHooks).noteVibratorOff(eq(UID)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); + verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED); + assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse(); + + assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibrationId)) + .containsExactly(effect) + .inOrder(); + } + + @Test public void vibrate_singleVibratorComposed_runsVibration() { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); @@ -1437,16 +1487,48 @@ public class VibrationThreadTest { .combine(); long vibrationId = startThreadAndDispatcher(effect); - assertTrue(waitUntil(() -> mControllers.get(2).isVibrating(), - TEST_TIMEOUT_MILLIS)); + assertTrue(waitUntil(() -> mControllers.get(2).isVibrating(), TEST_TIMEOUT_MILLIS)); assertTrue(mThread.isRunningVibrationId(vibrationId)); // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. Thread cancellingThread = new Thread( () -> mVibrationConductor.notifyCancelled( - new Vibration.EndInfo( - Vibration.Status.CANCELLED_BY_SCREEN_OFF), + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SCREEN_OFF), + /* immediate= */ false)); + cancellingThread.start(); + + waitForCompletion(/* timeout= */ 50); + cancellingThread.join(); + + verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_SCREEN_OFF); + assertFalse(mControllers.get(1).isVibrating()); + assertFalse(mControllers.get(2).isVibrating()); + } + + @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void vibrate_multipleVendorEffectCancel_cancelsVibrationImmediately() throws Exception { + mockVibrators(1, 2); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); + mVibratorProviders.get(1).setVendorEffectDuration(10 * TEST_TIMEOUT_MILLIS); + mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); + mVibratorProviders.get(2).setVendorEffectDuration(10 * TEST_TIMEOUT_MILLIS); + + CombinedVibration effect = CombinedVibration.startParallel() + .addVibrator(1, VibrationEffect.createVendorEffect(createTestVendorData())) + .addVibrator(2, VibrationEffect.createVendorEffect(createTestVendorData())) + .combine(); + long vibrationId = startThreadAndDispatcher(effect); + + assertTrue(waitUntil(() -> mControllers.get(2).isVibrating(), TEST_TIMEOUT_MILLIS)); + assertTrue(mThread.isRunningVibrationId(vibrationId)); + + // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should + // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. + Thread cancellingThread = new Thread( + () -> mVibrationConductor.notifyCancelled( + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SCREEN_OFF), /* immediate= */ false)); cancellingThread.start(); @@ -1614,6 +1696,25 @@ public class VibrationThreadTest { } @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void vibrate_vendorEffectWithRampDown_doesNotAddRampDown() { + when(mVibrationConfigMock.getRampDownDurationMs()).thenReturn(15); + mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); + + VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData()); + long vibrationId = startThreadAndDispatcher(effect); + waitForCompletion(); + + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); + verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED); + + assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibrationId)) + .containsExactly(effect) + .inOrder(); + assertThat(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()).isEmpty(); + } + + @Test public void vibrate_composedWithRampDown_doesNotAddRampDown() { when(mVibrationConfigMock.getRampDownDurationMs()).thenReturn(15); mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL, @@ -1831,6 +1932,16 @@ public class VibrationThreadTest { return array; } + private static PersistableBundle createTestVendorData() { + PersistableBundle vendorData = new PersistableBundle(); + vendorData.putInt("id", 1); + vendorData.putDouble("scale", 0.5); + vendorData.putBoolean("loop", false); + vendorData.putLongArray("amplitudes", new long[] { 0, 255, 128 }); + vendorData.putString("label", "vibration"); + return vendorData; + } + private VibrationEffectSegment expectedOneShot(long millis) { return new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, /* frequencyHz= */ 0, (int) millis); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java index 5ae5677b9b53..1f4a469bdd5a 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -16,6 +16,8 @@ package com.android.server.vibrator; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -67,6 +69,7 @@ import android.os.IBinder; import android.os.IExternalVibrationController; import android.os.IVibratorStateListener; import android.os.Looper; +import android.os.PersistableBundle; import android.os.PowerManager; import android.os.PowerManagerInternal; import android.os.PowerSaveState; @@ -1573,6 +1576,50 @@ public class VibratorManagerServiceTest { } @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void vibrate_vendorEffectsWithoutPermission_doesNotVibrate() throws Exception { + // Deny permission to vibrate with vendor effects + denyPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS); + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); + fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_TICK); + VibratorManagerService service = createSystemReadyService(); + + PersistableBundle vendorData = new PersistableBundle(); + vendorData.putString("key", "value"); + VibrationEffect vendorEffect = VibrationEffect.createVendorEffect(vendorData); + VibrationEffect tickEffect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK); + + vibrateAndWaitUntilFinished(service, vendorEffect, RINGTONE_ATTRS); + vibrateAndWaitUntilFinished(service, tickEffect, RINGTONE_ATTRS); + + // No vendor effect played, but predefined TICK plays successfully. + assertThat(fakeVibrator.getAllVendorEffects()).isEmpty(); + assertThat(fakeVibrator.getAllEffectSegments()).hasSize(1); + assertThat(fakeVibrator.getAllEffectSegments().get(0)).isInstanceOf(PrebakedSegment.class); + } + + @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void vibrate_vendorEffectsWithPermission_successful() throws Exception { + // Deny permission to vibrate with vendor effects + grantPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS); + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); + VibratorManagerService service = createSystemReadyService(); + + PersistableBundle vendorData = new PersistableBundle(); + vendorData.putString("key", "value"); + VibrationEffect vendorEffect = VibrationEffect.createVendorEffect(vendorData); + + vibrateAndWaitUntilFinished(service, vendorEffect, RINGTONE_ATTRS); + + assertThat(fakeVibrator.getAllVendorEffects()).containsExactly(vendorEffect); + } + + @Test public void vibrate_withIntensitySettings_appliesSettingsToScaleVibrations() throws Exception { int defaultNotificationIntensity = mVibrator.getDefaultVibrationIntensity(VibrationAttributes.USAGE_NOTIFICATION); @@ -1714,6 +1761,39 @@ public class VibratorManagerServiceTest { } @Test + @RequiresFlagsEnabled({ + android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED, + android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS, + }) + public void vibrate_withIntensitySettingsAndAdaptiveHaptics_appliesSettingsToVendorEffects() + throws Exception { + setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, + Vibrator.VIBRATION_INTENSITY_LOW); + + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); + VibratorManagerService service = createSystemReadyService(); + + SparseArray<Float> vibrationScales = new SparseArray<>(); + vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f); + + mVibratorControlService.setVibrationParams( + VibrationParamGenerator.generateVibrationParams(vibrationScales), + mFakeVibratorController); + + PersistableBundle vendorData = new PersistableBundle(); + vendorData.putString("key", "value"); + VibrationEffect vendorEffect = VibrationEffect.createVendorEffect(vendorData); + vibrateAndWaitUntilFinished(service, vendorEffect, NOTIFICATION_ATTRS); + + assertThat(fakeVibrator.getAllVendorEffects()).hasSize(1); + VibrationEffect.VendorEffect scaled = fakeVibrator.getAllVendorEffects().get(0); + assertThat(scaled.getEffectStrength()).isEqualTo(VibrationEffect.EFFECT_STRENGTH_STRONG); + assertThat(scaled.getLinearScale()).isEqualTo(0.4f); + } + + @Test public void vibrate_withPowerModeChange_cancelVibrationIfNotAllowed() throws Exception { mockVibrators(1, 2); VibratorManagerService service = createSystemReadyService(); @@ -2729,7 +2809,9 @@ public class VibratorManagerServiceTest { CombinedVibration effect, VibrationAttributes attrs) { HalVibration vib = service.vibrateWithPermissionCheck(UID, deviceId, PACKAGE_NAME, effect, attrs, "some reason", service); - mPendingVibrations.add(vib); + if (vib != null) { + mPendingVibrations.add(vib); + } return vib; } diff --git a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java index 2ddb47b832ef..96c3e97bc819 100644 --- a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java +++ b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java @@ -17,8 +17,11 @@ package com.android.server.vibrator; import android.annotation.Nullable; +import android.hardware.vibrator.IVibrator; import android.os.Handler; import android.os.Looper; +import android.os.Parcel; +import android.os.PersistableBundle; import android.os.VibrationEffect; import android.os.VibratorInfo; import android.os.vibrator.PrebakedSegment; @@ -45,6 +48,7 @@ public final class FakeVibratorControllerProvider { private final Map<Long, PrebakedSegment> mEnabledAlwaysOnEffects = new HashMap<>(); private final Map<Long, List<VibrationEffectSegment>> mEffectSegments = new TreeMap<>(); + private final Map<Long, List<VibrationEffect.VendorEffect>> mVendorEffects = new TreeMap<>(); private final Map<Long, List<Integer>> mBraking = new HashMap<>(); private final List<Float> mAmplitudes = new ArrayList<>(); private final List<Boolean> mExternalControlStates = new ArrayList<>(); @@ -69,11 +73,16 @@ public final class FakeVibratorControllerProvider { private float mFrequencyResolution = Float.NaN; private float mQFactor = Float.NaN; private float[] mMaxAmplitudes; + private long mVendorEffectDuration = EFFECT_DURATION; void recordEffectSegment(long vibrationId, VibrationEffectSegment segment) { mEffectSegments.computeIfAbsent(vibrationId, k -> new ArrayList<>()).add(segment); } + void recordVendorEffect(long vibrationId, VibrationEffect.VendorEffect vendorEffect) { + mVendorEffects.computeIfAbsent(vibrationId, k -> new ArrayList<>()).add(vendorEffect); + } + void recordBraking(long vibrationId, int braking) { mBraking.computeIfAbsent(vibrationId, k -> new ArrayList<>()).add(braking); } @@ -130,6 +139,21 @@ public final class FakeVibratorControllerProvider { } @Override + public long performVendorEffect(Parcel vendorData, long strength, float scale, + long vibrationId) { + if ((mCapabilities & IVibrator.CAP_PERFORM_VENDOR_EFFECTS) == 0) { + return 0; + } + PersistableBundle bundle = PersistableBundle.CREATOR.createFromParcel(vendorData); + recordVendorEffect(vibrationId, + new VibrationEffect.VendorEffect(bundle, (int) strength, scale)); + applyLatency(mOnLatency); + scheduleListener(mVendorEffectDuration, vibrationId); + // HAL has unknown duration for vendor effects. + return Long.MAX_VALUE; + } + + @Override public long compose(PrimitiveSegment[] primitives, long vibrationId) { if (mSupportedPrimitives == null) { return 0; @@ -328,6 +352,11 @@ public final class FakeVibratorControllerProvider { mMaxAmplitudes = maxAmplitudes; } + /** Set the duration of vendor effects in fake vibrator hardware. */ + public void setVendorEffectDuration(long durationMs) { + mVendorEffectDuration = durationMs; + } + /** * Return the amplitudes set by this controller, including zeroes for each time the vibrator was * turned off. @@ -366,6 +395,29 @@ public final class FakeVibratorControllerProvider { } return result; } + + /** Return list of {@link VibrationEffect.VendorEffect} played by this controller, in order. */ + public List<VibrationEffect.VendorEffect> getVendorEffects(long vibrationId) { + if (mVendorEffects.containsKey(vibrationId)) { + return new ArrayList<>(mVendorEffects.get(vibrationId)); + } else { + return new ArrayList<>(); + } + } + + /** + * Returns a list of all vibrations' effect segments, for external-use where vibration IDs + * aren't exposed. + */ + public List<VibrationEffect.VendorEffect> getAllVendorEffects() { + // Returns segments in order of vibrationId, which increases over time. TreeMap gives order. + ArrayList<VibrationEffect.VendorEffect> result = new ArrayList<>(); + for (List<VibrationEffect.VendorEffect> subList : mVendorEffects.values()) { + result.addAll(subList); + } + return result; + } + /** Return list of states set for external control to the fake vibrator hardware. */ public List<Boolean> getExternalControlStates() { return mExternalControlStates; |