Merge "Introduce vibrator service effect pipeline support" into main
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.