diff options
19 files changed, 669 insertions, 148 deletions
diff --git a/core/java/android/os/CombinedVibration.java b/core/java/android/os/CombinedVibration.java index 77d6cb762e06..f1d3957cc919 100644 --- a/core/java/android/os/CombinedVibration.java +++ b/core/java/android/os/CombinedVibration.java @@ -17,6 +17,7 @@ package android.os; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.TestApi; import android.os.vibrator.Flags; import android.util.SparseArray; @@ -28,6 +29,7 @@ import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.StringJoiner; +import java.util.function.Function; /** * A CombinedVibration describes a combination of haptic effects to be performed by one or more @@ -114,6 +116,17 @@ public abstract class CombinedVibration implements Parcelable { public abstract long getDuration(); /** + * Gets the estimated duration of the combined vibration in milliseconds. + * + * <p>For effects with hardware-dependent constants (e.g. primitive compositions), this returns + * the estimated duration based on the {@link VibratorInfo}. For all other effects this will + * return the same as {@link #getDuration()}. + * + * @hide + */ + public abstract long getDuration(@Nullable SparseArray<VibratorInfo> vibratorInfos); + + /** * Returns true if this effect could represent a touch haptic feedback. * * <p>It is strongly recommended that an instance of {@link VibrationAttributes} is specified @@ -383,6 +396,23 @@ public abstract class CombinedVibration implements Parcelable { /** @hide */ @Override + public long getDuration(@Nullable SparseArray<VibratorInfo> vibratorInfos) { + if (vibratorInfos == null) { + return getDuration(); + } + long maxDuration = 0; + for (int i = 0; i < vibratorInfos.size(); i++) { + long duration = mEffect.getDuration(vibratorInfos.valueAt(i)); + if ((duration == Long.MAX_VALUE) || (duration < 0)) { + return duration; + } + maxDuration = Math.max(maxDuration, duration); + } + return maxDuration; + } + + /** @hide */ + @Override public boolean isHapticFeedbackCandidate() { return mEffect.isHapticFeedbackCandidate(); } @@ -531,10 +561,27 @@ public abstract class CombinedVibration implements Parcelable { @Override public long getDuration() { + return getDuration(idx -> mEffects.valueAt(idx).getDuration()); + } + + /** @hide */ + @Override + public long getDuration(@Nullable SparseArray<VibratorInfo> vibratorInfos) { + if (vibratorInfos == null) { + return getDuration(); + } + return getDuration(idx -> { + VibrationEffect effect = mEffects.valueAt(idx); + VibratorInfo info = vibratorInfos.get(mEffects.keyAt(idx)); + return effect.getDuration(info); + }); + } + + private long getDuration(Function<Integer, Long> durationFn) { long maxDuration = Long.MIN_VALUE; boolean hasUnknownStep = false; for (int i = 0; i < mEffects.size(); i++) { - long duration = mEffects.valueAt(i).getDuration(); + long duration = durationFn.apply(i); if (duration == Long.MAX_VALUE) { // If any duration is repeating, this combination duration is also repeating. return duration; @@ -750,12 +797,21 @@ public abstract class CombinedVibration implements Parcelable { @Override public long getDuration() { + return getDuration(CombinedVibration::getDuration); + } + + /** @hide */ + @Override + public long getDuration(@Nullable SparseArray<VibratorInfo> vibratorInfos) { + return getDuration(effect -> effect.getDuration(vibratorInfos)); + } + + private long getDuration(Function<CombinedVibration, Long> durationFn) { boolean hasUnknownStep = false; long durations = 0; final int effectCount = mEffects.size(); for (int i = 0; i < effectCount; i++) { - CombinedVibration effect = mEffects.get(i); - long duration = effect.getDuration(); + long duration = durationFn.apply(mEffects.get(i)); if (duration == Long.MAX_VALUE) { // If any duration is repeating, this combination duration is also repeating. return duration; diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java index ffc58c537f2a..61dd11fd4122 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -55,6 +55,7 @@ import java.util.Locale; import java.util.Objects; import java.util.StringJoiner; import java.util.function.BiFunction; +import java.util.function.Function; /** * A VibrationEffect describes a haptic effect to be performed by a {@link Vibrator}. @@ -565,6 +566,19 @@ public abstract class VibrationEffect implements Parcelable { public abstract long getDuration(); /** + * Gets the estimated duration of the segment for given vibrator, in milliseconds. + * + * <p>For effects with hardware-dependent constants (e.g. primitive compositions), this returns + * the estimated duration based on the given {@link VibratorInfo}. For all other effects this + * will return the same as {@link #getDuration()}. + * + * @hide + */ + public long getDuration(@Nullable VibratorInfo vibratorInfo) { + return getDuration(); + } + + /** * Checks if a vibrator with a given {@link VibratorInfo} can play this effect as intended. * * <p>See {@link VibratorInfo#areVibrationFeaturesSupported(VibrationEffect)} for more @@ -904,13 +918,23 @@ public abstract class VibrationEffect implements Parcelable { @Override public long getDuration() { + return getDuration(VibrationEffectSegment::getDuration); + } + + /** @hide */ + @Override + public long getDuration(@Nullable VibratorInfo vibratorInfo) { + return getDuration(segment -> segment.getDuration(vibratorInfo)); + } + + private long getDuration(Function<VibrationEffectSegment, Long> durationFn) { if (mRepeatIndex >= 0) { return Long.MAX_VALUE; } int segmentCount = mSegments.size(); long totalDuration = 0; for (int i = 0; i < segmentCount; i++) { - long segmentDuration = mSegments.get(i).getDuration(); + long segmentDuration = durationFn.apply(mSegments.get(i)); if (segmentDuration < 0) { return segmentDuration; } diff --git a/core/java/android/os/vibrator/PrebakedSegment.java b/core/java/android/os/vibrator/PrebakedSegment.java index 39f841226e4e..b17e82a704bf 100644 --- a/core/java/android/os/vibrator/PrebakedSegment.java +++ b/core/java/android/os/vibrator/PrebakedSegment.java @@ -16,6 +16,17 @@ package android.os.vibrator; +import static android.os.VibrationEffect.Composition.PRIMITIVE_CLICK; +import static android.os.VibrationEffect.Composition.PRIMITIVE_THUD; +import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK; +import static android.os.VibrationEffect.EFFECT_CLICK; +import static android.os.VibrationEffect.EFFECT_DOUBLE_CLICK; +import static android.os.VibrationEffect.EFFECT_HEAVY_CLICK; +import static android.os.VibrationEffect.EFFECT_POP; +import static android.os.VibrationEffect.EFFECT_TEXTURE_TICK; +import static android.os.VibrationEffect.EFFECT_THUD; +import static android.os.VibrationEffect.EFFECT_TICK; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; @@ -78,6 +89,32 @@ public final class PrebakedSegment extends VibrationEffectSegment { /** @hide */ @Override + public long getDuration(@Nullable VibratorInfo vibratorInfo) { + if (vibratorInfo == null) { + return getDuration(); + } + return switch (mEffectId) { + case EFFECT_TICK, + EFFECT_CLICK, + EFFECT_HEAVY_CLICK -> estimateFromPrimitiveDuration(vibratorInfo, PRIMITIVE_CLICK); + case EFFECT_TEXTURE_TICK -> estimateFromPrimitiveDuration(vibratorInfo, PRIMITIVE_TICK); + case EFFECT_THUD -> estimateFromPrimitiveDuration(vibratorInfo, PRIMITIVE_THUD); + case EFFECT_DOUBLE_CLICK -> { + long clickDuration = vibratorInfo.getPrimitiveDuration(PRIMITIVE_CLICK); + yield clickDuration > 0 ? 2 * clickDuration : getDuration(); + } + default -> getDuration(); + }; + } + + private long estimateFromPrimitiveDuration(VibratorInfo vibratorInfo, int primitiveId) { + int duration = vibratorInfo.getPrimitiveDuration(primitiveId); + // Unsupported primitives should be ignored here. + return duration > 0 ? duration : getDuration(); + } + + /** @hide */ + @Override public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) { if (vibratorInfo.isEffectSupported(mEffectId) == Vibrator.VIBRATION_EFFECT_SUPPORT_YES) { return true; @@ -89,34 +126,30 @@ public final class PrebakedSegment extends VibrationEffectSegment { } // The vibrator does not have hardware support for the effect, but the effect has fallback // support. Check if a fallback will be available for the effect ID. - switch (mEffectId) { - case VibrationEffect.EFFECT_CLICK: - case VibrationEffect.EFFECT_DOUBLE_CLICK: - case VibrationEffect.EFFECT_HEAVY_CLICK: - case VibrationEffect.EFFECT_TICK: - // Any of these effects are always supported via some form of fallback. - return true; - default: - return false; - } + return switch (mEffectId) { + // Any of these effects are always supported via some form of fallback. + case EFFECT_CLICK, + EFFECT_DOUBLE_CLICK, + EFFECT_HEAVY_CLICK, + EFFECT_TICK -> true; + default -> false; + }; } /** @hide */ @Override public boolean isHapticFeedbackCandidate() { - switch (mEffectId) { - case VibrationEffect.EFFECT_CLICK: - case VibrationEffect.EFFECT_DOUBLE_CLICK: - case VibrationEffect.EFFECT_HEAVY_CLICK: - case VibrationEffect.EFFECT_POP: - case VibrationEffect.EFFECT_TEXTURE_TICK: - case VibrationEffect.EFFECT_THUD: - case VibrationEffect.EFFECT_TICK: - return true; - default: - // VibrationEffect.RINGTONES are not segments that could represent a haptic feedback - return false; - } + return switch (mEffectId) { + case EFFECT_CLICK, + EFFECT_DOUBLE_CLICK, + EFFECT_HEAVY_CLICK, + EFFECT_POP, + EFFECT_TEXTURE_TICK, + EFFECT_THUD, + EFFECT_TICK -> true; + // VibrationEffect.RINGTONES are not segments that could represent a haptic feedback + default -> false; + }; } /** @hide */ @@ -153,27 +186,25 @@ public final class PrebakedSegment extends VibrationEffectSegment { } private static boolean isValidEffectStrength(int strength) { - switch (strength) { - case VibrationEffect.EFFECT_STRENGTH_LIGHT: - case VibrationEffect.EFFECT_STRENGTH_MEDIUM: - case VibrationEffect.EFFECT_STRENGTH_STRONG: - return true; - default: - return false; - } + return switch (strength) { + case VibrationEffect.EFFECT_STRENGTH_LIGHT, + VibrationEffect.EFFECT_STRENGTH_MEDIUM, + VibrationEffect.EFFECT_STRENGTH_STRONG -> true; + default -> false; + }; } /** @hide */ @Override public void validate() { switch (mEffectId) { - case VibrationEffect.EFFECT_CLICK: - case VibrationEffect.EFFECT_DOUBLE_CLICK: - case VibrationEffect.EFFECT_HEAVY_CLICK: - case VibrationEffect.EFFECT_POP: - case VibrationEffect.EFFECT_TEXTURE_TICK: - case VibrationEffect.EFFECT_THUD: - case VibrationEffect.EFFECT_TICK: + case EFFECT_CLICK: + case EFFECT_DOUBLE_CLICK: + case EFFECT_HEAVY_CLICK: + case EFFECT_POP: + case EFFECT_TEXTURE_TICK: + case EFFECT_THUD: + case EFFECT_TICK: break; default: int[] ringtones = VibrationEffect.RINGTONES; diff --git a/core/java/android/os/vibrator/PrimitiveSegment.java b/core/java/android/os/vibrator/PrimitiveSegment.java index 3c84bcda639b..91653edd1ba5 100644 --- a/core/java/android/os/vibrator/PrimitiveSegment.java +++ b/core/java/android/os/vibrator/PrimitiveSegment.java @@ -77,6 +77,16 @@ public final class PrimitiveSegment extends VibrationEffectSegment { /** @hide */ @Override + public long getDuration(@Nullable VibratorInfo vibratorInfo) { + if (vibratorInfo == null) { + return getDuration(); + } + int duration = vibratorInfo.getPrimitiveDuration(mPrimitiveId); + return duration > 0 ? duration + mDelay : getDuration(); + } + + /** @hide */ + @Override public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) { return vibratorInfo.isPrimitiveSupported(mPrimitiveId); } diff --git a/core/java/android/os/vibrator/VibrationConfig.java b/core/java/android/os/vibrator/VibrationConfig.java index e6e5a27bd731..88be96a4aef3 100644 --- a/core/java/android/os/vibrator/VibrationConfig.java +++ b/core/java/android/os/vibrator/VibrationConfig.java @@ -86,6 +86,7 @@ public class VibrationConfig { private final int mDefaultKeyboardVibrationIntensity; private final boolean mKeyboardVibrationSettingsSupported; + private final int mVibrationPipelineMaxDurationMs; /** @hide */ public VibrationConfig(@Nullable Resources resources) { @@ -106,6 +107,8 @@ public class VibrationConfig { com.android.internal.R.bool.config_ignoreVibrationsOnWirelessCharger); mKeyboardVibrationSettingsSupported = loadBoolean(resources, com.android.internal.R.bool.config_keyboardVibrationSettingsSupported); + mVibrationPipelineMaxDurationMs = loadInteger(resources, + com.android.internal.R.integer.config_vibrationPipelineMaxDuration, 0); mDefaultAlarmVibrationIntensity = loadDefaultIntensity(resources, com.android.internal.R.integer.config_defaultAlarmVibrationIntensity); @@ -221,6 +224,23 @@ public class VibrationConfig { } /** + * The max duration, in milliseconds, allowed for pipelining vibration requests. + * + * <p>If the ongoing vibration duration is shorter than this threshold then it should be allowed + * to finish before the next vibration can start. If the ongoing vibration is longer than this + * then it should be cancelled when it's superseded for the new one. + * + * @return the max duration allowed for vibration effect to finish before the next request, or + * zero to disable effect pipelining. + */ + public int getVibrationPipelineMaxDurationMs() { + if (mVibrationPipelineMaxDurationMs < 0) { + return 0; + } + return mVibrationPipelineMaxDurationMs; + } + + /** * Whether or not vibrations are ignored if the device is on a wireless charger. * * <p>This may be the case if vibration during wireless charging causes unwanted results, like diff --git a/core/java/android/os/vibrator/VibrationEffectSegment.java b/core/java/android/os/vibrator/VibrationEffectSegment.java index e1fb4e361008..dadc849dae0a 100644 --- a/core/java/android/os/vibrator/VibrationEffectSegment.java +++ b/core/java/android/os/vibrator/VibrationEffectSegment.java @@ -17,6 +17,7 @@ package android.os.vibrator; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; @@ -58,10 +59,23 @@ public abstract class VibrationEffectSegment implements Parcelable { */ public abstract long getDuration(); - /** - * Checks if a given {@link Vibrator} can play this segment as intended. See - * {@link Vibrator#areVibrationFeaturesSupported(VibrationEffect)} for more information about - * what counts as supported by a vibrator, and what counts as not. + /** + * Gets the estimated duration of the segment for given vibrator, in milliseconds. + * + * <p>For segments with hardware-dependent constants (e.g. primitives), this returns the + * estimated duration based on the given {@link VibratorInfo}. For all other effects this will + * return the same as {@link #getDuration()}. + * + * @hide + */ + public long getDuration(@Nullable VibratorInfo vibratorInfo) { + return getDuration(); + } + + /** + * Checks if a given {@link android.os.Vibrator} can play this segment as intended. See + * {@link android.os.Vibrator#areVibrationFeaturesSupported(VibrationEffect)} for more + * information about what counts as supported by a vibrator, and what counts as not. * * @hide */ diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig index e5d891a420dd..7ceb948945fd 100644 --- a/core/java/android/os/vibrator/flags.aconfig +++ b/core/java/android/os/vibrator/flags.aconfig @@ -135,3 +135,13 @@ flag { purpose: PURPOSE_FEATURE } } + +flag { + namespace: "haptics" + name: "vibration_pipeline_enabled" + description: "Enables functionality to pipeline vibration effects to avoid cancelling short vibrations" + bug: "344494220" + metadata { + purpose: PURPOSE_FEATURE + } +} diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 169cf594f42e..91b482051065 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -287,6 +287,11 @@ vibration params. --> <integer name="config_requestVibrationParamsTimeout">50</integer> + <!-- The max duration (in milliseconds) that the vibrator service will allow effects to be + pipelined (i.e. service will wait for ongoing vibration to finish instead of cancelling it + to start the new one). Value should be positive. Zero will disable effect pipelining. --> + <integer name="config_vibrationPipelineMaxDuration">25</integer> + <!-- Array containing the usages that should request vibration params before they are played. These usages don't have strong latency requirements, e.g. ringtone and notification, and can be slightly delayed. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index ab1b49173921..0348b4685a66 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2141,6 +2141,7 @@ <java-symbol type="integer" name="config_vibrationWaveformRampStepDuration" /> <java-symbol type="bool" name="config_ignoreVibrationsOnWirelessCharger" /> <java-symbol type="integer" name="config_vibrationWaveformRampDownDuration" /> + <java-symbol type="integer" name="config_vibrationPipelineMaxDuration" /> <java-symbol type="integer" name="config_radioScanningTimeout" /> <java-symbol type="integer" name="config_requestVibrationParamsTimeout" /> <java-symbol type="array" name="config_requestVibrationParamsForUsages" /> diff --git a/core/tests/vibrator/src/android/os/CombinedVibrationTest.java b/core/tests/vibrator/src/android/os/CombinedVibrationTest.java index 244fcff7d27d..37ddfd21c98d 100644 --- a/core/tests/vibrator/src/android/os/CombinedVibrationTest.java +++ b/core/tests/vibrator/src/android/os/CombinedVibrationTest.java @@ -22,6 +22,9 @@ import static junit.framework.Assert.assertTrue; import static org.testng.Assert.assertThrows; +import android.hardware.vibrator.IVibrator; +import android.util.SparseArray; + import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -134,6 +137,54 @@ public class CombinedVibrationTest { } @Test + public void testDurationMono_withVibratorSupportingPrimitives() { + SparseArray<VibratorInfo> infos = new SparseArray<>(2); + infos.put(1, new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 5) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 5) + .build()); + infos.put(2, new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1) + .build()); + + // Use max duration from all vibrators. + assertEquals(10, CombinedVibration.createParallel( + VibrationEffect.get(VibrationEffect.EFFECT_CLICK)).getDuration(infos)); + assertEquals(111, CombinedVibration.createParallel( + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100) + .compose()) + .getDuration(infos)); + } + + @Test + public void testDurationMono_withVibratorNotSupportingPrimitives() { + SparseArray<VibratorInfo> infos = new SparseArray<>(2); + infos.put(1, new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) + .build()); + infos.put(2, new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1) + .build()); + + // Use max duration from all vibrators. + assertEquals(-1, CombinedVibration.createParallel( + VibrationEffect.get(VibrationEffect.EFFECT_CLICK)).getDuration(infos)); + assertEquals(-1, CombinedVibration.createParallel( + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100) + .compose()) + .getDuration(infos)); + } + + @Test public void testDurationStereo() { assertEquals(6, CombinedVibration.startParallel() .addVibrator(1, VibrationEffect.createOneShot(1, 1)) @@ -156,6 +207,75 @@ public class CombinedVibrationTest { } @Test + public void testDurationStereo_withVibratorSupportingPrimitives() { + SparseArray<VibratorInfo> infos = new SparseArray<>(2); + infos.put(1, new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 5) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 5) + .build()); + infos.put(2, new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1) + .build()); + + // Use specific vibrator durations, then max effect duration + assertEquals(111, CombinedVibration.startParallel() + .addVibrator(1, VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100) + .compose()) + .addVibrator(2, VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100) + .compose()) + .combine() + .getDuration(infos)); + assertEquals(110, CombinedVibration.startParallel() + .addVibrator(1, VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100) + .compose()) + .combine() + .getDuration(infos)); + } + + @Test + public void testDurationStereo_withVibratorNotSupportingPrimitives() { + SparseArray<VibratorInfo> infos = new SparseArray<>(2); + infos.put(1, new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) + .build()); + infos.put(2, new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1) + .build()); + + // One vibrator does not support primitives + assertEquals(-1, CombinedVibration.startParallel() + .addVibrator(1, VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100) + .compose()) + .addVibrator(2, VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100) + .compose()) + .combine() + .getDuration(infos)); + // Invalid vibrator ID + assertEquals(-1, CombinedVibration.startParallel() + .addVibrator(3, VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100) + .compose()) + .combine() + .getDuration(infos)); + } + + @Test public void testDurationSequential() { assertEquals(26, CombinedVibration.startSequential() .addNext(1, VibrationEffect.createOneShot(10, 10), 10) @@ -178,6 +298,59 @@ public class CombinedVibrationTest { } @Test + public void testDurationSequential_withVibratorSupportingPrimitives() { + SparseArray<VibratorInfo> infos = new SparseArray<>(2); + infos.put(1, new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 5) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 5) + .build()); + infos.put(2, new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1) + .build()); + + // Add each duration and delay + assertEquals(321, CombinedVibration.startSequential() + .addNext(1, VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100) + .compose(), 100) + .addNext(2, VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100) + .compose()) + .combine() + .getDuration(infos)); + } + + @Test + public void testDurationSequential_withVibratorNotSupportingPrimitives() { + SparseArray<VibratorInfo> infos = new SparseArray<>(2); + infos.put(1, new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) + .build()); + infos.put(2, new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1) + .build()); + + assertEquals(-1, CombinedVibration.startSequential() + .addNext(1, VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100) + .compose(), 100) + .addNext(2, VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100) + .compose()) + .combine() + .getDuration(infos)); + } + + @Test public void testIsHapticFeedbackCandidateMono() { assertTrue(CombinedVibration.createParallel( VibrationEffect.createOneShot(1, 1)).isHapticFeedbackCandidate()); diff --git a/core/tests/vibrator/src/android/os/VibrationEffectTest.java b/core/tests/vibrator/src/android/os/VibrationEffectTest.java index f5b04ee759a5..8acf2ed87e95 100644 --- a/core/tests/vibrator/src/android/os/VibrationEffectTest.java +++ b/core/tests/vibrator/src/android/os/VibrationEffectTest.java @@ -1078,6 +1078,52 @@ public class VibrationEffectTest { } @Test + public void testDuration_withVibratorSupportingPrimitives() { + VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 5) + .build(); + + VibrationEffect composition = VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 100) + .compose(); + + assertEquals(1, VibrationEffect.createOneShot(1, 1).getDuration()); + assertEquals(10, VibrationEffect.get(VibrationEffect.EFFECT_CLICK).getDuration(info)); + assertEquals(115, composition.getDuration(info)); + assertEquals(Long.MAX_VALUE, + VibrationEffect.startComposition() + .repeatEffectIndefinitely(composition) + .compose() + .getDuration(info)); + if (Flags.vendorVibrationEffects()) { + assertEquals(-1, + VibrationEffect.createVendorEffect(createNonEmptyBundle()).getDuration(info)); + } + } + + @Test + public void testDuration_withVibratorNotSupportingPrimitives() { + VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) + .build(); + + VibrationEffect composition = VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .compose(); + + assertEquals(-1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK).getDuration(info)); + assertEquals(-1, composition.getDuration(info)); + assertEquals(Long.MAX_VALUE, + VibrationEffect.startComposition() + .repeatEffectIndefinitely(composition) + .compose() + .getDuration(info)); + } + + @Test public void testAreVibrationFeaturesSupported_allSegmentsSupported() { VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1) .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) diff --git a/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java index 7dd9e55f8f3e..f9ec5f0b2305 100644 --- a/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java +++ b/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java @@ -24,6 +24,7 @@ import static junit.framework.Assert.assertTrue; import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertThrows; +import android.hardware.vibrator.IVibrator; import android.os.Parcel; import android.os.VibrationEffect; import android.os.VibratorInfo; @@ -114,39 +115,82 @@ public class PrebakedSegmentTest { @Test public void testDuration() { - assertEquals(-1, new PrebakedSegment( - VibrationEffect.EFFECT_CLICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM) - .getDuration()); - assertEquals(-1, new PrebakedSegment( - VibrationEffect.EFFECT_TICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM) - .getDuration()); - assertEquals(-1, new PrebakedSegment( - VibrationEffect.EFFECT_DOUBLE_CLICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM) - .getDuration()); - assertEquals(-1, new PrebakedSegment( - VibrationEffect.EFFECT_THUD, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM) + assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_CLICK).getDuration()); + assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_TICK).getDuration()); + assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_THUD).getDuration()); + assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_DOUBLE_CLICK) .getDuration()); } @Test + public void testDuration_withVibratorSupportingPrimitives_returnsPrimitiveDuration() { + int tickDuration = 5; + int clickDuration = 10; + int thudDuration = 15; + + VibratorInfo vibratorInfo = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, tickDuration) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, clickDuration) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, thudDuration) + .build(); + + assertEquals(5, createSegmentWithFallback(VibrationEffect.EFFECT_TEXTURE_TICK) + .getDuration(vibratorInfo)); + assertEquals(10, createSegmentWithFallback(VibrationEffect.EFFECT_TICK) + .getDuration(vibratorInfo)); + assertEquals(10, createSegmentWithFallback(VibrationEffect.EFFECT_CLICK) + .getDuration(vibratorInfo)); + assertEquals(10, createSegmentWithFallback(VibrationEffect.EFFECT_HEAVY_CLICK) + .getDuration(vibratorInfo)); + assertEquals(20, createSegmentWithFallback(VibrationEffect.EFFECT_DOUBLE_CLICK) + .getDuration(vibratorInfo)); + assertEquals(15, createSegmentWithFallback(VibrationEffect.EFFECT_THUD) + .getDuration(vibratorInfo)); + + // Unknown effects + assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_POP) + .getDuration(vibratorInfo)); + assertEquals(-1, createSegmentWithFallback(VibrationEffect.RINGTONES[0]) + .getDuration(vibratorInfo)); + } + + @Test + public void testDuration_withVibratorNotSupportingPrimitives_returnsUnknown() { + VibratorInfo vibratorInfo = createVibratorInfoWithSupportedEffects( + VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_POP); + + assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_TEXTURE_TICK) + .getDuration(vibratorInfo)); + assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_TICK) + .getDuration(vibratorInfo)); + assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_CLICK) + .getDuration(vibratorInfo)); + assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_HEAVY_CLICK) + .getDuration(vibratorInfo)); + assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_DOUBLE_CLICK) + .getDuration(vibratorInfo)); + assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_THUD) + .getDuration(vibratorInfo)); + assertEquals(-1, createSegmentWithFallback(VibrationEffect.EFFECT_POP) + .getDuration(vibratorInfo)); + assertEquals(-1, createSegmentWithFallback(VibrationEffect.RINGTONES[0]) + .getDuration(vibratorInfo)); + } + + @Test public void testIsHapticFeedbackCandidate_prebakedConstants_areCandidates() { - assertTrue(new PrebakedSegment( - VibrationEffect.EFFECT_CLICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM) + assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_CLICK) .isHapticFeedbackCandidate()); - assertTrue(new PrebakedSegment( - VibrationEffect.EFFECT_TICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM) + assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_TICK) .isHapticFeedbackCandidate()); - assertTrue(new PrebakedSegment( - VibrationEffect.EFFECT_DOUBLE_CLICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM) + assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_DOUBLE_CLICK) .isHapticFeedbackCandidate()); - assertTrue(new PrebakedSegment( - VibrationEffect.EFFECT_HEAVY_CLICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM) + assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_HEAVY_CLICK) .isHapticFeedbackCandidate()); - assertTrue(new PrebakedSegment( - VibrationEffect.EFFECT_THUD, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM) + assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_THUD) .isHapticFeedbackCandidate()); - assertTrue(new PrebakedSegment( - VibrationEffect.EFFECT_TEXTURE_TICK, true, VibrationEffect.EFFECT_STRENGTH_MEDIUM) + assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_TEXTURE_TICK) .isHapticFeedbackCandidate()); } @@ -271,8 +315,7 @@ public class PrebakedSegmentTest { @Test public void testIsHapticFeedbackCandidate_prebakedRingtones_notCandidates() { - assertFalse(new PrebakedSegment( - VibrationEffect.RINGTONES[1], true, VibrationEffect.EFFECT_STRENGTH_MEDIUM) + assertFalse(createSegmentWithFallback(VibrationEffect.RINGTONES[1]) .isHapticFeedbackCandidate()); } diff --git a/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java index 97f1d5e77ddb..a6d9dc51d7bb 100644 --- a/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java +++ b/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java @@ -201,6 +201,22 @@ public class PrimitiveSegmentTest { } @Test + public void testDuration_withVibratorSupportingPrimitives_returnsVibratorDurationWithDelay() { + VibratorInfo vibratorInfo = createVibratorInfoWithSupportedPrimitive( + VibrationEffect.Composition.PRIMITIVE_CLICK, /* durationMs= */ 10); + assertEquals(15, new PrimitiveSegment( + VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 5).getDuration(vibratorInfo)); + } + + @Test + public void testDuration_withVibratorNotSupportingPrimitive_returnsUnknown() { + VibratorInfo vibratorInfo = createVibratorInfoWithSupportedPrimitive( + VibrationEffect.Composition.PRIMITIVE_CLICK); + assertEquals(-1, new PrimitiveSegment( + VibrationEffect.Composition.PRIMITIVE_NOOP, 1, 5).getDuration(vibratorInfo)); + } + + @Test public void testVibrationFeaturesSupport_primitiveSupportedByVibrator() { assertTrue(createSegment(VibrationEffect.Composition.PRIMITIVE_CLICK) .areVibrationFeaturesSupported( @@ -252,9 +268,14 @@ public class PrimitiveSegmentTest { } private static VibratorInfo createVibratorInfoWithSupportedPrimitive(int primitiveId) { + return createVibratorInfoWithSupportedPrimitive(primitiveId, /* durationMs= */ 10); + } + + private static VibratorInfo createVibratorInfoWithSupportedPrimitive(int primitiveId, + int durationMs) { return new VibratorInfo.Builder(/* id= */ 1) .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) - .setSupportedPrimitive(primitiveId, 10) + .setSupportedPrimitive(primitiveId, durationMs) .build(); } } diff --git a/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java index bea82931dda7..df874bcb73ca 100644 --- a/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java +++ b/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java @@ -195,7 +195,14 @@ public class RampSegmentTest { @Test public void testDuration() { + VibratorInfo infoWithSupport = + createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ true); + VibratorInfo infoWithoutSupport = + createVibInfo(/* hasAmplitudeControl= */ false, /* hasFrequencyControl= */ false); + assertEquals(10, new RampSegment(0.5f, 1, 0, 0, 10).getDuration()); + assertEquals(10, new RampSegment(0.5f, 1, 0, 0, 10).getDuration(infoWithSupport)); + assertEquals(10, new RampSegment(0.5f, 1, 0, 0, 10).getDuration(infoWithoutSupport)); } @Test diff --git a/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java index 411074a75e2e..914117c10c87 100644 --- a/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java +++ b/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java @@ -213,7 +213,13 @@ public class StepSegmentTest { @Test public void testDuration() { + VibratorInfo infoWithSupport = createVibInfoForAmplitude(/* hasAmplitudeControl= */ true); + VibratorInfo infoWithoutSupport = + createVibInfoForAmplitude(/* hasAmplitudeControl= */ false); + assertEquals(5, new StepSegment(0, 0, 5).getDuration()); + assertEquals(5, new StepSegment(0, 0, 5).getDuration(infoWithSupport)); + assertEquals(5, new StepSegment(0, 0, 5).getDuration(infoWithoutSupport)); } @Test diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java index fbcc856d0974..d192e64c897f 100644 --- a/services/core/java/com/android/server/vibrator/HalVibration.java +++ b/services/core/java/com/android/server/vibrator/HalVibration.java @@ -21,6 +21,8 @@ import android.annotation.Nullable; import android.os.CombinedVibration; import android.os.VibrationAttributes; import android.os.VibrationEffect; +import android.os.VibratorInfo; +import android.os.vibrator.Flags; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.VibrationEffectSegment; import android.util.SparseArray; @@ -145,19 +147,30 @@ final class HalVibration extends Vibration { originalEffect, mScaleLevel, mAdaptiveScale); } - /** - * Returns true if this vibration can pipeline with the specified one. - * - * <p>Note that currently, repeating vibrations can't pipeline with following vibrations, - * because the cancel() call to stop the repetition will cancel a pending vibration too. This - * can be changed if we have a use-case to reason around behavior for. It may also be nice to - * pipeline very short vibrations together, regardless of the flag. - */ - public boolean canPipelineWith(HalVibration vib) { - return callerInfo.uid == vib.callerInfo.uid && callerInfo.attrs.isFlagSet( - VibrationAttributes.FLAG_PIPELINED_EFFECT) - && vib.callerInfo.attrs.isFlagSet(VibrationAttributes.FLAG_PIPELINED_EFFECT) - && (mOriginalEffect.getDuration() != Long.MAX_VALUE); + /** Returns true if this vibration can pipeline with the specified one. */ + public boolean canPipelineWith(HalVibration vib, + @Nullable SparseArray<VibratorInfo> vibratorInfos, int durationThresholdMs) { + long effectDuration = Flags.vibrationPipelineEnabled() && (vibratorInfos != null) + ? mEffectToPlay.getDuration(vibratorInfos) + : mEffectToPlay.getDuration(); + if (effectDuration == Long.MAX_VALUE) { + // Repeating vibrations can't pipeline with following vibrations, because the cancel() + // call to stop the repetition will cancel a pending vibration too. This can be changed + // if we have a use-case, requiring changes to how pipelined vibrations are cancelled. + return false; + } + if (Flags.vibrationPipelineEnabled() + && (effectDuration > 0) && (effectDuration < durationThresholdMs)) { + // Duration is known and it's less than the pipeline threshold, so allow it. + // No need to check UID, as we want to avoid cancelling any short effect and let the + // vibrator hardware gracefully finish the vibration. + return true; + } + // Check the same app is requesting multiple vibrations with the pipeline flag, + // independently of the effect durations. + return callerInfo.uid == vib.callerInfo.uid + && callerInfo.attrs.isFlagSet(VibrationAttributes.FLAG_PIPELINED_EFFECT) + && vib.callerInfo.attrs.isFlagSet(VibrationAttributes.FLAG_PIPELINED_EFFECT); } private void fillFallbacksForEffect(CombinedVibration effect, diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 9b7bdece69f9..7d5d34dbf7ab 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -168,12 +168,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @VisibleForTesting final VibrationSettings mVibrationSettings; + private final VibrationConfig mVibrationConfig; private final VibrationScaler mVibrationScaler; private final VibratorControlService mVibratorControlService; private final InputDeviceDelegate mInputDeviceDelegate; private final DeviceAdapter mDeviceAdapter; @GuardedBy("mLock") + @Nullable private SparseArray<VibratorInfo> mVibratorInfos; + @GuardedBy("mLock") @Nullable private VibratorInfo mCombinedVibratorInfo; @GuardedBy("mLock") @Nullable private HapticFeedbackVibrationProvider mHapticFeedbackVibrationProvider; @@ -247,9 +250,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mHandler = injector.createHandler(Looper.myLooper()); mFrameworkStatsLogger = injector.getFrameworkStatsLogger(mHandler); - VibrationConfig vibrationConfig = new VibrationConfig(context.getResources()); - mVibrationSettings = new VibrationSettings(mContext, mHandler, vibrationConfig); - mVibrationScaler = new VibrationScaler(vibrationConfig, mVibrationSettings); + mVibrationConfig = new VibrationConfig(context.getResources()); + mVibrationSettings = new VibrationSettings(mContext, mHandler, mVibrationConfig); + mVibrationScaler = new VibrationScaler(mVibrationConfig, mVibrationSettings); mVibratorControlService = new VibratorControlService(mContext, injector.createVibratorControllerHolder(), mVibrationScaler, mVibrationSettings, mFrameworkStatsLogger, mLock); @@ -295,7 +298,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mVibratorIds = vibratorIds; mVibrators = new SparseArray<>(mVibratorIds.length); for (int vibratorId : vibratorIds) { - mVibrators.put(vibratorId, injector.createVibratorController(vibratorId, listener)); + VibratorController vibratorController = + injector.createVibratorController(vibratorId, listener); + mVibrators.put(vibratorId, vibratorController); } } @@ -334,6 +339,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mVibrators.valueAt(i).reloadVibratorInfoIfNeeded(); } + synchronized (mLock) { + mVibratorInfos = transformAllVibratorsLocked(VibratorController::getVibratorInfo); + VibratorInfo[] infos = new VibratorInfo[mVibratorInfos.size()]; + for (int i = 0; i < mVibratorInfos.size(); i++) { + infos[i] = mVibratorInfos.valueAt(i); + } + mCombinedVibratorInfo = VibratorInfoFactory.create(/* id= */ -1, infos); + } + mVibrationSettings.onSystemReady(); mInputDeviceDelegate.onSystemReady(); @@ -633,7 +647,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { endExternalVibrateLocked(Status.CANCELLED_SUPERSEDED, callerInfo, /* continueExternalControl= */ false); } else if (mCurrentVibration != null) { - if (mCurrentVibration.getVibration().canPipelineWith(vib)) { + if (mCurrentVibration.getVibration().canPipelineWith(vib, mVibratorInfos, + mVibrationConfig.getVibrationPipelineMaxDurationMs())) { // Don't cancel the current vibration if it's pipeline-able. // Note that if there is a pending next vibration that can't be // pipelined, it will have already cancelled the current one, so we @@ -1871,33 +1886,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + @Nullable private VibratorInfo getCombinedVibratorInfo() { synchronized (mLock) { - // Used a cached resolving vibrator if one exists. - if (mCombinedVibratorInfo != null) { - return mCombinedVibratorInfo; - } - - // Return an empty resolving vibrator if the service has no vibrator. - if (mVibratorIds.length == 0) { - return mCombinedVibratorInfo = VibratorInfo.EMPTY_VIBRATOR_INFO; - } - - // Combine the vibrator infos of all the service's vibrator to create a single resolving - // vibrator that is based on the combined info. - VibratorInfo[] infos = new VibratorInfo[mVibratorIds.length]; - for (int i = 0; i < mVibratorIds.length; i++) { - VibratorInfo info = getVibratorInfo(mVibratorIds[i]); - // If any one of the service's vibrator does not have a valid vibrator info, stop - // trying to create and cache a combined resolving vibrator. Combine the infos only - // when infos for all vibrators are available. - if (info == null) { - return null; - } - infos[i] = info; - } - - return mCombinedVibratorInfo = VibratorInfoFactory.create(/* id= */ -1, infos); + // This is only initialized at system ready, when all vibrator infos are fully loaded. + return mCombinedVibratorInfo; } } 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 b7821623855c..7f5da41bdf10 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -16,8 +16,6 @@ package com.android.server.vibrator; -import static android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED; - import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertArrayEquals; @@ -28,6 +26,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; @@ -89,17 +88,14 @@ import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibrationEffectSegment; -import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.view.HapticFeedbackConstants; import android.view.InputDevice; -import android.view.flags.Flags; import androidx.test.InstrumentationRegistry; @@ -168,9 +164,7 @@ public class VibratorManagerServiceTest { @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); - - @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock private VibratorManagerService.NativeWrapper mNativeWrapperMock; @@ -800,7 +794,7 @@ public class VibratorManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_CANCEL_BY_APPOPS) + @EnableFlags(android.os.vibrator.Flags.FLAG_CANCEL_BY_APPOPS) public void vibrate_thenDeniedAppOps_getsCancelled() throws Throwable { mockVibrators(1); VibratorManagerService service = createSystemReadyService(); @@ -894,7 +888,7 @@ public class VibratorManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_ADD_UI_FOR_SOUNDS_FROM_BACKGROUND_USERS) + @EnableFlags(android.multiuser.Flags.FLAG_ADD_UI_FOR_SOUNDS_FROM_BACKGROUND_USERS) public void vibrate_thenFgUserRequestsMute_getsCancelled() throws Throwable { mockVibrators(1); VibratorManagerService service = createSystemReadyService(); @@ -1331,6 +1325,37 @@ public class VibratorManagerServiceTest { } @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VIBRATION_PIPELINE_ENABLED) + public void vibrate_withPipelineFlagEnabledAndShortEffect_continuesOngoingEffect() + throws Exception { + assumeTrue(mVibrationConfig.getVibrationPipelineMaxDurationMs() > 0); + + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + fakeVibrator.setSupportedPrimitives( + VibrationEffect.Composition.PRIMITIVE_CLICK, + VibrationEffect.Composition.PRIMITIVE_THUD); + fakeVibrator.setPrimitiveDuration( + mVibrationConfig.getVibrationPipelineMaxDurationMs() - 1); + VibratorManagerService service = createSystemReadyService(); + + HalVibration firstVibration = vibrateWithUid(service, /* uid= */ 123, + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .compose(), HAPTIC_FEEDBACK_ATTRS); + HalVibration secondVibration = vibrateWithUid(service, /* uid= */ 456, + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD) + .compose(), HAPTIC_FEEDBACK_ATTRS); + secondVibration.waitForEnd(); + + assertThat(fakeVibrator.getAllEffectSegments()).hasSize(2); + assertThat(firstVibration.getStatus()).isEqualTo(Status.FINISHED); + assertThat(secondVibration.getStatus()).isEqualTo(Status.FINISHED); + } + + @Test public void vibrate_withInputDevices_vibratesInputDevices() throws Exception { mockVibrators(1); FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); @@ -1512,6 +1537,7 @@ public class VibratorManagerServiceTest { } @Test + @EnableFlags(android.view.flags.Flags.FLAG_SCROLL_FEEDBACK_API) public void performHapticFeedback_doesNotRequireVibrateOrBypassPermissions() throws Exception { // Deny permissions that would have been required for regular vibrations, and check that // the vibration proceed as expected to verify that haptic feedback does not need these @@ -1520,8 +1546,6 @@ public class VibratorManagerServiceTest { denyPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS); denyPermission(android.Manifest.permission.MODIFY_PHONE_STATE); denyPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING); - // Flag override to enable the scroll feedack constants to bypass interruption policies. - mSetFlagsRule.enableFlags(Flags.FLAG_SCROLL_FEEDBACK_API); mHapticFeedbackVibrationMap.put( HapticFeedbackConstants.SCROLL_TICK, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)); @@ -1544,6 +1568,10 @@ public class VibratorManagerServiceTest { } @Test + @EnableFlags({ + android.view.flags.Flags.FLAG_SCROLL_FEEDBACK_API, + android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED, + }) public void performHapticFeedbackForInputDevice_doesNotRequireVibrateOrBypassPermissions() throws Exception { // Deny permissions that would have been required for regular vibrations, and check that @@ -1553,9 +1581,6 @@ public class VibratorManagerServiceTest { denyPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS); denyPermission(android.Manifest.permission.MODIFY_PHONE_STATE); denyPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING); - // Flag override to enable the scroll feedback constants to bypass interruption policies. - mSetFlagsRule.enableFlags(Flags.FLAG_SCROLL_FEEDBACK_API); - mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); mHapticFeedbackVibrationMapSourceRotary.put( HapticFeedbackConstants.SCROLL_TICK, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)); @@ -1628,12 +1653,14 @@ public class VibratorManagerServiceTest { } @Test + @EnableFlags({ + android.view.flags.Flags.FLAG_SCROLL_FEEDBACK_API, + android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED, + }) public void performHapticFeedbackForInputDevice_restrictedConstantsWithoutPermission_doesNotVibrate() throws Exception { // Deny permission to vibrate with restricted constants denyPermission(android.Manifest.permission.VIBRATE_SYSTEM_CONSTANTS); - mSetFlagsRule.enableFlags(Flags.FLAG_SCROLL_FEEDBACK_API); - mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); // Public constant, no permission required mHapticFeedbackVibrationMapSourceRotary.put( HapticFeedbackConstants.CONFIRM, @@ -1697,9 +1724,9 @@ public class VibratorManagerServiceTest { } @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED) public void performHapticFeedbackForInputDevice_restrictedConstantsWithPermission_playsVibration() throws Exception { - mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); // Grant permission to vibrate with restricted constants grantPermission(android.Manifest.permission.VIBRATE_SYSTEM_CONSTANTS); // Public constant, no permission required @@ -1732,9 +1759,11 @@ public class VibratorManagerServiceTest { } @Test + @EnableFlags({ + android.view.flags.Flags.FLAG_SCROLL_FEEDBACK_API, + android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED, + }) public void performHapticFeedback_doesNotVibrateWhenVibratorInfoNotReady() throws Exception { - mSetFlagsRule.enableFlags(Flags.FLAG_SCROLL_FEEDBACK_API); - mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); denyPermission(android.Manifest.permission.VIBRATE); mHapticFeedbackVibrationMap.put( HapticFeedbackConstants.KEYBOARD_TAP, @@ -1767,9 +1796,11 @@ public class VibratorManagerServiceTest { } @Test + @EnableFlags({ + android.view.flags.Flags.FLAG_SCROLL_FEEDBACK_API, + android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED, + }) public void performHapticFeedback_doesNotVibrateForInvalidConstant() throws Exception { - mSetFlagsRule.enableFlags(Flags.FLAG_SCROLL_FEEDBACK_API); - mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); denyPermission(android.Manifest.permission.VIBRATE); mockVibrators(1); VibratorManagerService service = createSystemReadyService(); @@ -1791,7 +1822,7 @@ public class VibratorManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @EnableFlags(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); @@ -1816,7 +1847,7 @@ public class VibratorManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) public void vibrate_vendorEffectsWithPermission_successful() throws Exception { // Grant permission to vibrate with vendor effects grantPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS); @@ -1904,7 +1935,7 @@ public class VibratorManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + @EnableFlags(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) public void vibrate_withAdaptiveHaptics_appliesCorrectAdaptiveScales() throws Exception { // Keep user settings the same as device default so only adaptive scale is applied. setUserSetting(Settings.System.ALARM_VIBRATION_INTENSITY, @@ -1947,7 +1978,7 @@ public class VibratorManagerServiceTest { } @Test - @RequiresFlagsEnabled({ + @EnableFlags({ android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED, android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS, }) @@ -2418,7 +2449,7 @@ public class VibratorManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + @EnableFlags(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) public void onExternalVibration_withAdaptiveHaptics_returnsCorrectAdaptiveScales() { mockVibrators(1); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL, @@ -2465,7 +2496,7 @@ public class VibratorManagerServiceTest { } @Test - @RequiresFlagsDisabled(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + @DisableFlags(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) public void onExternalVibration_withAdaptiveHapticsFlagDisabled_alwaysReturnScaleNone() { mockVibrators(1); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL, @@ -2585,7 +2616,7 @@ public class VibratorManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_ADD_UI_FOR_SOUNDS_FROM_BACKGROUND_USERS) + @EnableFlags(android.multiuser.Flags.FLAG_ADD_UI_FOR_SOUNDS_FROM_BACKGROUND_USERS) public void onExternalVibration_thenFgUserRequestsMute_doNotCancelVibration() throws Throwable { mockVibrators(1); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); @@ -3105,9 +3136,20 @@ public class VibratorManagerServiceTest { return vibrateWithDevice(service, Context.DEVICE_ID_DEFAULT, effect, attrs); } + private HalVibration vibrateWithUid(VibratorManagerService service, int uid, + VibrationEffect effect, VibrationAttributes attrs) { + return vibrateWithUidAndDevice(service, uid, Context.DEVICE_ID_DEFAULT, + CombinedVibration.createParallel(effect), attrs); + } + private HalVibration vibrateWithDevice(VibratorManagerService service, int deviceId, CombinedVibration effect, VibrationAttributes attrs) { - HalVibration vib = service.vibrateWithPermissionCheck(UID, deviceId, PACKAGE_NAME, effect, + return vibrateWithUidAndDevice(service, UID, deviceId, effect, attrs); + } + + private HalVibration vibrateWithUidAndDevice(VibratorManagerService service, int uid, + int deviceId, CombinedVibration effect, VibrationAttributes attrs) { + HalVibration vib = service.vibrateWithPermissionCheck(uid, deviceId, PACKAGE_NAME, effect, attrs, "some reason", service); if (vib != null) { mPendingVibrations.add(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 6dc1b10ec930..75a9cedfd8c4 100644 --- a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java +++ b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java @@ -80,6 +80,7 @@ public final class FakeVibratorControllerProvider { private float[] mFrequenciesHz; private float[] mOutputAccelerationsGs; private long mVendorEffectDuration = EFFECT_DURATION; + private long mPrimitiveDuration = EFFECT_DURATION; void recordEffectSegment(long vibrationId, VibrationEffectSegment segment) { mEffectSegments.computeIfAbsent(vibrationId, k -> new ArrayList<>()).add(segment); @@ -171,7 +172,7 @@ public final class FakeVibratorControllerProvider { } long duration = 0; for (PrimitiveSegment primitive : primitives) { - duration += EFFECT_DURATION + primitive.getDelay(); + duration += mPrimitiveDuration + primitive.getDelay(); recordEffectSegment(vibrationId, primitive); } applyLatency(mOnLatency); @@ -381,6 +382,11 @@ public final class FakeVibratorControllerProvider { mVendorEffectDuration = durationMs; } + /** Set the duration of primitives in fake vibrator hardware. */ + public void setPrimitiveDuration(long primitiveDuration) { + mPrimitiveDuration = primitiveDuration; + } + /** * Set the maximum number of envelope effects control points supported in fake vibrator * hardware. |