Introduce vibrator service effect pipeline support
Add device config to allow very short vibrations to complete before
playing a newly requested one. This will improve the user experience of
very frequent and short haptic feedback, e.g. the ones created by typing
on a virtual keyboard or using a slider.
The constant cancellation of short effects can create an unpleasant
haptic experience, and in some hardwares it can take longer to brake the
ongoing signal than it would take to wait for it to complete gracefully.
Bug: 344494220
Flag: android.os.vibrator.vibration_pipeline_enabled
Test: FrameworksVibratorCoreTests
FrameworksVibratorServicesTests
Change-Id: I28b2a3bc6e2dd2bd1c3beb731fdb205bcc9312c7
diff --git a/core/java/android/os/CombinedVibration.java b/core/java/android/os/CombinedVibration.java
index 77d6cb7..f1d3957 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.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 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 @@
/** @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 @@
@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 @@
@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 ffc58c5..61dd11f 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -55,6 +55,7 @@
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 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 @@
@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 39f8412..b17e82a 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 @@
/** @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 @@
}
// 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 @@
}
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 3c84bcd..91653ed 100644
--- a/core/java/android/os/vibrator/PrimitiveSegment.java
+++ b/core/java/android/os/vibrator/PrimitiveSegment.java
@@ -77,6 +77,16 @@
/** @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 e6e5a27..88be96a 100644
--- a/core/java/android/os/vibrator/VibrationConfig.java
+++ b/core/java/android/os/vibrator/VibrationConfig.java
@@ -86,6 +86,7 @@
private final int mDefaultKeyboardVibrationIntensity;
private final boolean mKeyboardVibrationSettingsSupported;
+ private final int mVibrationPipelineMaxDurationMs;
/** @hide */
public VibrationConfig(@Nullable Resources resources) {
@@ -106,6 +107,8 @@
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 @@
}
/**
+ * 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 e1fb4e3..dadc849 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 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 e5d891a..7ceb948 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -135,3 +135,13 @@
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 169cf59..91b4820 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 ab1b491..0348b46 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 244fcff..37ddfd2 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 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 @@
}
@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 @@
}
@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 @@
}
@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 f5b04ee..8acf2ed 100644
--- a/core/tests/vibrator/src/android/os/VibrationEffectTest.java
+++ b/core/tests/vibrator/src/android/os/VibrationEffectTest.java
@@ -1078,6 +1078,52 @@
}
@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 7dd9e55..f9ec5f0 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 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 @@
@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 @@
@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 97f1d5e..a6d9dc5 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 @@
}
@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 @@
}
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 bea8293..df874bcb 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 @@
@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 411074a..914117c 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 @@
@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 fbcc856..d192e64 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.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 @@
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 9b7bdec..7d5d34d 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 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
}
}
+ @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 b782162..7f5da41 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.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.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 @@
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
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 @@
}
@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 @@
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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@Test
- @RequiresFlagsEnabled({
+ @EnableFlags({
android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED,
android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS,
})
@@ -2418,7 +2449,7 @@
}
@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 @@
}
@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 @@
}
@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 @@
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 6dc1b10..75a9cedf 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 @@
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 @@
}
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 @@
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.