Create single thread to play any Vibration
This thread takes in a CombinedVibrationEffect and plays all vibrations.
The Prebaked effect now also holds a fallback VibrationEffect, which is
resolved and scaled together with the original effect and is passed on
to the VibrateThread. All class attributes are not final, making this
effect immutable as all the others.
The Vibration now takes in a CombinedVibrationEffect, in preparation to
be used by the VibratorManagerService in multiple vibrators.
The new thread is replacing all vibration from VibratorService, which
means they are all triggering the HAL asynchronously.
Bug: 167946816
Bug: 131311651
Test: VibrationThreadTest, VibratorServiceTest, VibrationEffectTest, VibrationScalerTest
Change-Id: Ic27b35e63ca35ad47083f94da9ca7bd75b683d43
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 91212be..f873ce7 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1329,11 +1329,10 @@
public static class VibrationEffect.Prebaked extends android.os.VibrationEffect implements android.os.Parcelable {
ctor public VibrationEffect.Prebaked(android.os.Parcel);
- ctor public VibrationEffect.Prebaked(int, boolean);
+ ctor public VibrationEffect.Prebaked(int, boolean, int);
method public long getDuration();
method public int getEffectStrength();
method public int getId();
- method public void setEffectStrength(int);
method public boolean shouldFallback();
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.os.VibrationEffect.Prebaked> CREATOR;
diff --git a/core/java/android/os/CombinedVibrationEffect.java b/core/java/android/os/CombinedVibrationEffect.java
index 7ec7fff..869a727 100644
--- a/core/java/android/os/CombinedVibrationEffect.java
+++ b/core/java/android/os/CombinedVibrationEffect.java
@@ -87,8 +87,14 @@
}
/** @hide */
+ public abstract long getDuration();
+
+ /** @hide */
public abstract void validate();
+ /** @hide */
+ public abstract boolean hasVibrator(int vibratorId);
+
/**
* A combination of haptic effects that should be played in multiple vibrators in sync.
*
@@ -265,6 +271,11 @@
return mEffect;
}
+ @Override
+ public long getDuration() {
+ return mEffect.getDuration();
+ }
+
/** @hide */
@Override
public void validate() {
@@ -272,12 +283,17 @@
}
@Override
+ public boolean hasVibrator(int vibratorId) {
+ return true;
+ }
+
+ @Override
public boolean equals(Object o) {
if (!(o instanceof Mono)) {
return false;
}
Mono other = (Mono) o;
- return other.mEffect.equals(other.mEffect);
+ return mEffect.equals(other.mEffect);
}
@Override
@@ -345,6 +361,15 @@
return mEffects;
}
+ @Override
+ public long getDuration() {
+ long maxDuration = Long.MIN_VALUE;
+ for (int i = 0; i < mEffects.size(); i++) {
+ maxDuration = Math.max(maxDuration, mEffects.valueAt(i).getDuration());
+ }
+ return maxDuration;
+ }
+
/** @hide */
@Override
public void validate() {
@@ -356,6 +381,11 @@
}
@Override
+ public boolean hasVibrator(int vibratorId) {
+ return mEffects.indexOfKey(vibratorId) >= 0;
+ }
+
+ @Override
public boolean equals(Object o) {
if (!(o instanceof Stereo)) {
return false;
@@ -445,6 +475,26 @@
return mDelays;
}
+ @Override
+ public long getDuration() {
+ long durations = 0;
+ final int effectCount = mEffects.size();
+ for (int i = 0; i < effectCount; i++) {
+ CombinedVibrationEffect effect = mEffects.get(i);
+ long duration = effect.getDuration();
+ if (duration < 0) {
+ // If any duration is unknown, this combination duration is also unknown.
+ return duration;
+ }
+ durations += duration;
+ }
+ long delays = 0;
+ for (int i = 0; i < effectCount; i++) {
+ delays += mDelays.get(i);
+ }
+ return durations + delays;
+ }
+
/** @hide */
@Override
public void validate() {
@@ -452,13 +502,15 @@
"There should be at least one effect set for a combined effect");
Preconditions.checkArgument(mEffects.size() == mDelays.size(),
"Effect and delays should have equal length");
- for (long delay : mDelays) {
- if (delay < 0) {
+ final int effectCount = mEffects.size();
+ for (int i = 0; i < effectCount; i++) {
+ if (mDelays.get(i) < 0) {
throw new IllegalArgumentException("Delays must all be >= 0"
+ " (delays=" + mDelays + ")");
}
}
- for (CombinedVibrationEffect effect : mEffects) {
+ for (int i = 0; i < effectCount; i++) {
+ CombinedVibrationEffect effect = mEffects.get(i);
if (effect instanceof Sequential) {
throw new IllegalArgumentException(
"There should be no nested sequential effects in a combined effect");
@@ -468,6 +520,17 @@
}
@Override
+ public boolean hasVibrator(int vibratorId) {
+ final int effectCount = mEffects.size();
+ for (int i = 0; i < effectCount; i++) {
+ if (mEffects.get(i).hasVibrator(vibratorId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
public boolean equals(Object o) {
if (!(o instanceof Sequential)) {
return false;
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index b57418d..b9b7a6e 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -317,7 +317,7 @@
*/
@TestApi
public static VibrationEffect get(int effectId, boolean fallback) {
- VibrationEffect effect = new Prebaked(effectId, fallback);
+ VibrationEffect effect = new Prebaked(effectId, fallback, EffectStrength.MEDIUM);
effect.validate();
return effect;
}
@@ -792,22 +792,30 @@
public static class Prebaked extends VibrationEffect implements Parcelable {
private final int mEffectId;
private final boolean mFallback;
-
- private int mEffectStrength;
+ private final int mEffectStrength;
+ @Nullable
+ private final VibrationEffect mFallbackEffect;
public Prebaked(Parcel in) {
- this(in.readInt(), in.readByte() != 0, in.readInt());
+ mEffectId = in.readInt();
+ mFallback = in.readByte() != 0;
+ mEffectStrength = in.readInt();
+ mFallbackEffect = in.readParcelable(VibrationEffect.class.getClassLoader());
}
- public Prebaked(int effectId, boolean fallback) {
- this(effectId, fallback, EffectStrength.MEDIUM);
- }
-
- /** @hide */
public Prebaked(int effectId, boolean fallback, int effectStrength) {
mEffectId = effectId;
mFallback = fallback;
mEffectStrength = effectStrength;
+ mFallbackEffect = null;
+ }
+
+ /** @hide */
+ public Prebaked(int effectId, int effectStrength, @NonNull VibrationEffect fallbackEffect) {
+ mEffectId = effectId;
+ mFallback = true;
+ mEffectStrength = effectStrength;
+ mFallbackEffect = fallbackEffect;
}
public int getId() {
@@ -829,26 +837,27 @@
/** @hide */
@Override
- public VibrationEffect resolve(int defaultAmplitude) {
- // Prebaked effects already have default amplitude set, so ignore this.
+ public Prebaked resolve(int defaultAmplitude) {
+ if (mFallbackEffect != null) {
+ VibrationEffect resolvedFallback = mFallbackEffect.resolve(defaultAmplitude);
+ if (!mFallbackEffect.equals(resolvedFallback)) {
+ return new Prebaked(mEffectId, mEffectStrength, resolvedFallback);
+ }
+ }
return this;
}
/** @hide */
@Override
public Prebaked scale(float scaleFactor) {
- // Prebaked effects cannot be scaled, so ignore this.
- return this;
- }
-
- /**
- * Set the effect strength of the prebaked effect.
- */
- public void setEffectStrength(int strength) {
- if (!isValidEffectStrength(strength)) {
- throw new IllegalArgumentException("Invalid effect strength: " + strength);
+ if (mFallbackEffect != null) {
+ VibrationEffect scaledFallback = mFallbackEffect.scale(scaleFactor);
+ if (!mFallbackEffect.equals(scaledFallback)) {
+ return new Prebaked(mEffectId, mEffectStrength, scaledFallback);
+ }
}
- mEffectStrength = strength;
+ // Prebaked effect strength cannot be scaled with this method.
+ return this;
}
/**
@@ -858,6 +867,16 @@
return mEffectStrength;
}
+ /**
+ * Return the fallback effect, if set.
+ *
+ * @hide
+ */
+ @Nullable
+ public VibrationEffect getFallbackEffect() {
+ return mFallbackEffect;
+ }
+
private static boolean isValidEffectStrength(int strength) {
switch (strength) {
case EffectStrength.LIGHT:
@@ -901,15 +920,13 @@
VibrationEffect.Prebaked other = (VibrationEffect.Prebaked) o;
return mEffectId == other.mEffectId
&& mFallback == other.mFallback
- && mEffectStrength == other.mEffectStrength;
+ && mEffectStrength == other.mEffectStrength
+ && Objects.equals(mFallbackEffect, other.mFallbackEffect);
}
@Override
public int hashCode() {
- int result = 17;
- result += 37 * mEffectId;
- result += 37 * mEffectStrength;
- return result;
+ return Objects.hash(mEffectId, mFallback, mEffectStrength, mFallbackEffect);
}
@Override
@@ -917,6 +934,7 @@
return "Prebaked{mEffectId=" + mEffectId
+ ", mEffectStrength=" + mEffectStrength
+ ", mFallback=" + mFallback
+ + ", mFallbackEffect=" + mFallbackEffect
+ "}";
}
@@ -927,6 +945,7 @@
out.writeInt(mEffectId);
out.writeByte((byte) (mFallback ? 1 : 0));
out.writeInt(mEffectStrength);
+ out.writeParcelable(mFallbackEffect, flags);
}
public static final @NonNull Parcelable.Creator<Prebaked> CREATOR =
@@ -990,8 +1009,10 @@
// Just return this if there's no scaling to be done.
return this;
}
+ final int primitiveCount = mPrimitiveEffects.size();
List<Composition.PrimitiveEffect> scaledPrimitives = new ArrayList<>();
- for (Composition.PrimitiveEffect primitive : mPrimitiveEffects) {
+ for (int i = 0; i < primitiveCount; i++) {
+ Composition.PrimitiveEffect primitive = mPrimitiveEffects.get(i);
scaledPrimitives.add(new Composition.PrimitiveEffect(
primitive.id, scale(primitive.scale, scaleFactor), primitive.delay));
}
@@ -1001,11 +1022,12 @@
/** @hide */
@Override
public void validate() {
- for (Composition.PrimitiveEffect effect : mPrimitiveEffects) {
- Composition.checkPrimitive(effect.id);
- Preconditions.checkArgumentInRange(
- effect.scale, 0.0f, 1.0f, "scale");
- Preconditions.checkArgumentNonNegative(effect.delay,
+ final int primitiveCount = mPrimitiveEffects.size();
+ for (int i = 0; i < primitiveCount; i++) {
+ Composition.PrimitiveEffect primitive = mPrimitiveEffects.get(i);
+ Composition.checkPrimitive(primitive.id);
+ Preconditions.checkArgumentInRange(primitive.scale, 0.0f, 1.0f, "scale");
+ Preconditions.checkArgumentNonNegative(primitive.delay,
"Primitive delay must be zero or positive");
}
}
diff --git a/core/tests/coretests/src/android/os/VibrationEffectTest.java b/core/tests/coretests/src/android/os/VibrationEffectTest.java
index c357414..1d56e17 100644
--- a/core/tests/coretests/src/android/os/VibrationEffectTest.java
+++ b/core/tests/coretests/src/android/os/VibrationEffectTest.java
@@ -39,6 +39,7 @@
import com.android.internal.R;
+import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
@@ -168,15 +169,30 @@
}
@Test
- public void testScalePrebaked_ignoresScaleAndReturnsSameEffect() {
- VibrationEffect initial = VibrationEffect.get(VibrationEffect.RINGTONES[1]);
- assertSame(initial, initial.scale(0.5f));
+ public void testScalePrebaked_scalesFallbackEffect() {
+ VibrationEffect.Prebaked prebaked =
+ (VibrationEffect.Prebaked) VibrationEffect.get(VibrationEffect.RINGTONES[1]);
+ assertSame(prebaked, prebaked.scale(0.5f));
+
+ prebaked = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+ VibrationEffect.EFFECT_STRENGTH_MEDIUM, TEST_ONE_SHOT);
+ VibrationEffect.OneShot scaledFallback =
+ (VibrationEffect.OneShot) prebaked.scale(0.5f).getFallbackEffect();
+ assertEquals(34, scaledFallback.getAmplitude(), AMPLITUDE_SCALE_TOLERANCE);
}
@Test
- public void testResolvePrebaked_ignoresDefaultAmplitudeAndReturnsSameEffect() {
- VibrationEffect initial = VibrationEffect.get(VibrationEffect.RINGTONES[1]);
- assertSame(initial, initial.resolve(1000));
+ public void testResolvePrebaked_resolvesFallbackEffectIfSet() {
+ VibrationEffect.Prebaked prebaked =
+ (VibrationEffect.Prebaked) VibrationEffect.get(VibrationEffect.RINGTONES[1]);
+ assertSame(prebaked, prebaked.resolve(1000));
+
+ prebaked = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+ VibrationEffect.EFFECT_STRENGTH_MEDIUM,
+ VibrationEffect.createOneShot(1, VibrationEffect.DEFAULT_AMPLITUDE));
+ VibrationEffect.OneShot resolvedFallback =
+ (VibrationEffect.OneShot) prebaked.resolve(10).getFallbackEffect();
+ assertEquals(10, resolvedFallback.getAmplitude());
}
@Test
@@ -352,6 +368,36 @@
INTENSITY_SCALE_TOLERANCE);
}
+ @Test
+ public void getEffectStrength_returnsValueFromConstructor() {
+ VibrationEffect.Prebaked effect = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+ VibrationEffect.EFFECT_STRENGTH_LIGHT, null);
+ Assert.assertEquals(VibrationEffect.EFFECT_STRENGTH_LIGHT, effect.getEffectStrength());
+ }
+
+ @Test
+ public void getFallbackEffect_withFallbackDisabled_isNull() {
+ VibrationEffect fallback = VibrationEffect.createOneShot(100, 100);
+ VibrationEffect.Prebaked effect = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+ false, VibrationEffect.EFFECT_STRENGTH_LIGHT);
+ Assert.assertNull(effect.getFallbackEffect());
+ }
+
+ @Test
+ public void getFallbackEffect_withoutEffectSet_isNull() {
+ VibrationEffect.Prebaked effect = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+ true, VibrationEffect.EFFECT_STRENGTH_LIGHT);
+ Assert.assertNull(effect.getFallbackEffect());
+ }
+
+ @Test
+ public void getFallbackEffect_withFallback_returnsValueFromConstructor() {
+ VibrationEffect fallback = VibrationEffect.createOneShot(100, 100);
+ VibrationEffect.Prebaked effect = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+ VibrationEffect.EFFECT_STRENGTH_LIGHT, fallback);
+ Assert.assertEquals(fallback, effect.getFallbackEffect());
+ }
+
private Resources mockRingtoneResources() {
return mockRingtoneResources(new String[] {
RINGTONE_URI_1,
diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java
index 6a9715e..2c83da5 100644
--- a/services/core/java/com/android/server/VibratorService.java
+++ b/services/core/java/com/android/server/VibratorService.java
@@ -28,6 +28,7 @@
import android.hardware.vibrator.IVibrator;
import android.os.BatteryStats;
import android.os.Binder;
+import android.os.CombinedVibrationEffect;
import android.os.ExternalVibration;
import android.os.Handler;
import android.os.IBinder;
@@ -37,18 +38,15 @@
import android.os.Looper;
import android.os.PowerManager;
import android.os.Process;
-import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.ShellCallback;
import android.os.ShellCommand;
-import android.os.SystemClock;
import android.os.Trace;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.VibratorInfo;
-import android.os.WorkSource;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
@@ -56,11 +54,11 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
import com.android.internal.util.DumpUtils;
-import com.android.internal.util.FrameworkStatsLog;
import com.android.server.vibrator.InputDeviceDelegate;
import com.android.server.vibrator.Vibration;
import com.android.server.vibrator.VibrationScaler;
import com.android.server.vibrator.VibrationSettings;
+import com.android.server.vibrator.VibrationThread;
import com.android.server.vibrator.VibratorController;
import com.android.server.vibrator.VibratorController.OnVibrationCompleteListener;
@@ -90,10 +88,10 @@
private final LinkedList<Vibration.DebugInfo> mPreviousExternalVibrations;
private final LinkedList<Vibration.DebugInfo> mPreviousVibrations;
private final int mPreviousVibrationsLimit;
- private final WorkSource mTmpWorkSource = new WorkSource();
private final Handler mH;
private final Object mLock = new Object();
private final VibratorController mVibratorController;
+ private final VibrationCallbacks mVibrationCallbacks = new VibrationCallbacks();
private final Context mContext;
private final PowerManager.WakeLock mWakeLock;
@@ -104,16 +102,40 @@
private VibrationScaler mVibrationScaler;
private InputDeviceDelegate mInputDeviceDelegate;
- private volatile VibrateWaveformThread mThread;
+ private volatile VibrationThread mThread;
@GuardedBy("mLock")
private Vibration mCurrentVibration;
- @GuardedBy("mLock")
- private VibrationDeathRecipient mCurrentVibrationDeathRecipient;
private int mCurVibUid = -1;
private ExternalVibrationHolder mCurrentExternalVibration;
/**
+ * Implementation of {@link VibrationThread.VibrationCallbacks} that reports finished
+ * vibrations.
+ */
+ private final class VibrationCallbacks implements VibrationThread.VibrationCallbacks {
+
+ @Override
+ public void prepareSyncedVibration(int requiredCapabilities, int[] vibratorIds) {
+ }
+
+ @Override
+ public void triggerSyncedVibration(long vibrationId) {
+ }
+
+ @Override
+ public void onVibrationEnded(long vibrationId, Vibration.Status status) {
+ if (DEBUG) {
+ Slog.d(TAG, "Vibration thread finished with status " + status);
+ }
+ synchronized (mLock) {
+ mThread = null;
+ reportFinishVibrationLocked(status);
+ }
+ }
+ }
+
+ /**
* Implementation of {@link OnVibrationCompleteListener} with a weak reference to this service.
*/
private static final class VibrationCompleteListener implements OnVibrationCompleteListener {
@@ -127,41 +149,11 @@
public void onComplete(int vibratorId, long vibrationId) {
VibratorService service = mServiceRef.get();
if (service != null) {
- service.onVibrationComplete(vibrationId);
+ service.onVibrationComplete(vibratorId, vibrationId);
}
}
}
- /** Death recipient to bind {@link Vibration}. */
- private final class VibrationDeathRecipient implements IBinder.DeathRecipient {
-
- private final Vibration mVibration;
-
- private VibrationDeathRecipient(Vibration vibration) {
- mVibration = vibration;
- }
-
- @Override
- public void binderDied() {
- synchronized (mLock) {
- if (mVibration == mCurrentVibration) {
- if (DEBUG) {
- Slog.d(TAG, "Vibration finished because binder died, cleaning up");
- }
- doCancelVibrateLocked(Vibration.Status.CANCELLED);
- }
- }
- }
-
- private void linkToDeath() throws RemoteException {
- mVibration.token.linkToDeath(this, 0);
- }
-
- private void unlinkToDeath() {
- mVibration.token.unlinkToDeath(this, 0);
- }
- }
-
/** Holder for a {@link ExternalVibration}. */
private final class ExternalVibrationHolder {
@@ -262,13 +254,20 @@
/** Callback for when vibration is complete, to be called by native. */
@VisibleForTesting
- public void onVibrationComplete(long vibrationId) {
+ public void onVibrationComplete(int vibratorId, long vibrationId) {
synchronized (mLock) {
if (mCurrentVibration != null && mCurrentVibration.id == vibrationId) {
if (DEBUG) {
- Slog.d(TAG, "Vibration finished by callback, cleaning up");
+ Slog.d(TAG, "Vibration onComplete callback, notifying VibrationThread");
}
- doCancelVibrateLocked(Vibration.Status.FINISHED);
+ if (mThread != null) {
+ // Let the thread playing the vibration handle the callback, since it might be
+ // expecting the vibrator to turn off multiple times during a single vibration.
+ mThread.vibratorComplete(vibratorId);
+ } else {
+ // No vibration is playing in the thread, but clean up service just in case.
+ doCancelVibrateLocked(Vibration.Status.FINISHED);
+ }
}
}
}
@@ -354,6 +353,17 @@
return true;
}
+ private VibrationEffect fixupVibrationEffect(VibrationEffect effect) {
+ if (effect instanceof VibrationEffect.Prebaked
+ && ((VibrationEffect.Prebaked) effect).shouldFallback()) {
+ VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) effect;
+ VibrationEffect fallback = mVibrationSettings.getFallbackEffect(prebaked.getId());
+ return new VibrationEffect.Prebaked(prebaked.getId(), prebaked.getEffectStrength(),
+ fallback);
+ }
+ return effect;
+ }
+
private VibrationAttributes fixupVibrationAttributes(VibrationAttributes attrs) {
if (attrs == null) {
attrs = DEFAULT_ATTRIBUTES;
@@ -388,16 +398,16 @@
if (!verifyVibrationEffect(effect)) {
return;
}
-
+ effect = fixupVibrationEffect(effect);
attrs = fixupVibrationAttributes(attrs);
- Vibration vib = new Vibration(token, mNextVibrationId.getAndIncrement(), effect, attrs,
- uid, opPkg, reason);
+ Vibration vib = new Vibration(token, mNextVibrationId.getAndIncrement(),
+ CombinedVibrationEffect.createSynced(effect), attrs, uid, opPkg, reason);
// If our current vibration is longer than the new vibration and is the same amplitude,
// then just let the current one finish.
synchronized (mLock) {
VibrationEffect currentEffect =
- mCurrentVibration == null ? null : mCurrentVibration.getEffect();
+ mCurrentVibration == null ? null : getEffect(mCurrentVibration);
if (effect instanceof VibrationEffect.OneShot
&& currentEffect instanceof VibrationEffect.OneShot) {
VibrationEffect.OneShot newOneShot = (VibrationEffect.OneShot) effect;
@@ -446,7 +456,6 @@
endVibrationLocked(vib, Vibration.Status.IGNORED_BACKGROUND);
return;
}
- linkVibrationLocked(vib);
final long ident = Binder.clearCallingIdentity();
try {
doCancelVibrateLocked(Vibration.Status.CANCELLED);
@@ -474,6 +483,10 @@
return effect.getDuration() == Long.MAX_VALUE;
}
+ private static <T extends VibrationEffect> T getEffect(Vibration vib) {
+ return (T) ((CombinedVibrationEffect.Mono) vib.getEffect()).getEffect();
+ }
+
private void endVibrationLocked(Vibration vib, Vibration.Status status) {
final LinkedList<Vibration.DebugInfo> previousVibrations;
switch (vib.attrs.getUsage()) {
@@ -527,7 +540,6 @@
@GuardedBy("mLock")
private void doCancelVibrateLocked(Vibration.Status status) {
- Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doCancelVibrateLocked");
try {
if (mThread != null) {
@@ -547,18 +559,6 @@
}
}
- // Callback for whenever the current vibration has finished played out
- public void onVibrationFinished() {
- if (DEBUG) {
- Slog.d(TAG, "Vibration finished, cleaning up");
- }
- synchronized (mLock) {
- // Make sure the vibration is really done. This also reports that the vibration is
- // finished.
- doCancelVibrateLocked(Vibration.Status.FINISHED);
- }
- }
-
@GuardedBy("mLock")
private void startVibrationLocked(final Vibration vib) {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "startVibrationLocked");
@@ -579,24 +579,20 @@
try {
// Set current vibration before starting it, so callback will work.
mCurrentVibration = vib;
- VibrationEffect effect = vib.getEffect();
- if (effect instanceof VibrationEffect.OneShot) {
- Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
- doVibratorOn(vib);
- } else if (effect instanceof VibrationEffect.Waveform) {
- Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
- doVibratorWaveformEffectLocked(vib);
- } else if (effect instanceof VibrationEffect.Prebaked) {
- Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
- doVibratorPrebakedEffectLocked(vib);
- } else if (effect instanceof VibrationEffect.Composed) {
- Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
- doVibratorComposedEffectLocked(vib);
- } else {
- Slog.e(TAG, "Unknown vibration type, ignoring");
- endVibrationLocked(vib, Vibration.Status.IGNORED_UNKNOWN_VIBRATION);
- // The set current vibration is not actually playing, so drop it.
+ VibrationEffect effect = getEffect(vib);
+ Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
+ boolean inputDevicesAvailable = mInputDeviceDelegate.vibrateIfAvailable(
+ vib.uid, vib.opPkg, vib.getEffect(), vib.reason, vib.attrs);
+ if (inputDevicesAvailable) {
+ // The set current vibration is no longer being played by this service, so drop it.
mCurrentVibration = null;
+ endVibrationLocked(vib, Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
+ } else {
+ // mThread better be null here. doCancelVibrate should always be
+ // called before startVibrationInnerLocked
+ mThread = new VibrationThread(vib, mVibratorController, mWakeLock,
+ mBatteryStatsService, mVibrationCallbacks);
+ mThread.start();
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
@@ -667,13 +663,13 @@
@GuardedBy("mLock")
private void reportFinishVibrationLocked(Vibration.Status status) {
+ Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "reportFinishVibrationLocked");
try {
if (mCurrentVibration != null) {
endVibrationLocked(mCurrentVibration, status);
mAppOps.finishOp(AppOpsManager.OP_VIBRATE, mCurrentVibration.uid,
mCurrentVibration.opPkg);
- unlinkVibrationLocked();
mCurrentVibration = null;
}
} finally {
@@ -681,30 +677,6 @@
}
}
- @GuardedBy("mLock")
- private void linkVibrationLocked(Vibration vib) {
- // Unlink previously linked vibration, if any.
- unlinkVibrationLocked();
- // Only link against waveforms since they potentially don't have a finish if
- // they're repeating. Let other effects just play out until they're done.
- if (vib.getEffect() instanceof VibrationEffect.Waveform) {
- try {
- mCurrentVibrationDeathRecipient = new VibrationDeathRecipient(vib);
- mCurrentVibrationDeathRecipient.linkToDeath();
- } catch (RemoteException e) {
- return;
- }
- }
- }
-
- @GuardedBy("mLock")
- private void unlinkVibrationLocked() {
- if (mCurrentVibrationDeathRecipient != null) {
- mCurrentVibrationDeathRecipient.unlinkToDeath();
- mCurrentVibrationDeathRecipient = null;
- }
- }
-
private void updateVibrators() {
synchronized (mLock) {
mInputDeviceDelegate.updateInputDeviceVibrators(
@@ -715,40 +687,12 @@
}
}
- private void doVibratorOn(Vibration vib) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doVibratorOn");
- try {
- final VibrationEffect.OneShot oneShot = (VibrationEffect.OneShot) vib.getEffect();
- if (DEBUG) {
- Slog.d(TAG, "Turning vibrator on for " + oneShot.getDuration() + " ms"
- + " with amplitude " + oneShot.getAmplitude() + ".");
- }
- boolean inputDevicesAvailable = mInputDeviceDelegate.vibrateIfAvailable(
- vib.uid, vib.opPkg, oneShot, vib.reason, vib.attrs);
- if (inputDevicesAvailable) {
- // The set current vibration is no longer being played by this service, so drop it.
- mCurrentVibration = null;
- endVibrationLocked(vib, Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
- } else {
- noteVibratorOnLocked(vib.uid, oneShot.getDuration());
- // Note: ordering is important here! Many haptic drivers will reset their
- // amplitude when enabled, so we always have to enable first, then set the
- // amplitude.
- mVibratorController.on(oneShot.getDuration(), vib.id);
- mVibratorController.setAmplitude(oneShot.getAmplitude());
- }
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
- }
-
private void doVibratorOff() {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doVibratorOff");
try {
if (DEBUG) {
Slog.d(TAG, "Turning vibrator off.");
}
- noteVibratorOffLocked();
boolean inputDevicesAvailable = mInputDeviceDelegate.cancelVibrateIfAvailable();
if (!inputDevicesAvailable) {
mVibratorController.off();
@@ -758,95 +702,6 @@
}
}
- @GuardedBy("mLock")
- private void doVibratorWaveformEffectLocked(Vibration vib) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doVibratorWaveformEffectLocked");
- try {
- boolean inputDevicesAvailable = mInputDeviceDelegate.vibrateIfAvailable(
- vib.uid, vib.opPkg, vib.getEffect(), vib.reason, vib.attrs);
- if (inputDevicesAvailable) {
- // The set current vibration is no longer being played by this service, so drop it.
- mCurrentVibration = null;
- endVibrationLocked(vib, Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
- } else {
- // mThread better be null here. doCancelVibrate should always be
- // called before startNextVibrationLocked or startVibrationLocked.
- mThread = new VibrateWaveformThread(vib);
- mThread.start();
- }
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
- }
-
- @GuardedBy("mLock")
- private void doVibratorPrebakedEffectLocked(Vibration vib) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doVibratorPrebakedEffectLocked");
- try {
- final VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) vib.getEffect();
- // Input devices don't support prebaked effect, so skip trying it with them and allow
- // fallback to be attempted.
- if (!mInputDeviceDelegate.isAvailable()) {
- long duration = mVibratorController.on(prebaked, vib.id);
- if (duration > 0) {
- noteVibratorOnLocked(vib.uid, duration);
- return;
- }
- }
- endVibrationLocked(vib, Vibration.Status.IGNORED_UNSUPPORTED);
- // The set current vibration is not actually playing, so drop it.
- mCurrentVibration = null;
-
- if (!prebaked.shouldFallback()) {
- return;
- }
- VibrationEffect effect = mVibrationSettings.getFallbackEffect(prebaked.getId());
- if (effect == null) {
- Slog.w(TAG, "Failed to play prebaked effect, no fallback");
- return;
- }
- Vibration fallbackVib = new Vibration(vib.token, mNextVibrationId.getAndIncrement(),
- effect, vib.attrs, vib.uid, vib.opPkg, vib.reason + " (fallback)");
- // Set current vibration before starting it, so callback will work.
- mCurrentVibration = fallbackVib;
- linkVibrationLocked(fallbackVib);
- applyVibrationIntensityScalingLocked(fallbackVib);
- startVibrationInnerLocked(fallbackVib);
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
- }
-
- @GuardedBy("mLock")
- private void doVibratorComposedEffectLocked(Vibration vib) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doVibratorComposedEffectLocked");
-
- try {
- final VibrationEffect.Composed composed = (VibrationEffect.Composed) vib.getEffect();
- boolean inputDevicesAvailable = mInputDeviceDelegate.vibrateIfAvailable(
- vib.uid, vib.opPkg, composed, vib.reason, vib.attrs);
- if (inputDevicesAvailable) {
- // The set current vibration is no longer being played by this service, so drop it.
- mCurrentVibration = null;
- endVibrationLocked(vib, Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
- return;
- } else if (!mVibratorController.hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) {
- // The set current vibration is not actually playing, so drop it.
- mCurrentVibration = null;
- endVibrationLocked(vib, Vibration.Status.IGNORED_UNSUPPORTED);
- return;
- }
-
- mVibratorController.on(composed, vib.id);
-
- // Composed effects don't actually give us an estimated duration, so we just guess here.
- noteVibratorOnLocked(vib.uid, 10 * composed.getPrimitiveEffects().size());
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
-
- }
-
private boolean isSystemHapticFeedback(Vibration vib) {
if (vib.attrs.getUsage() != VibrationAttributes.USAGE_TOUCH) {
return false;
@@ -854,27 +709,6 @@
return vib.uid == Process.SYSTEM_UID || vib.uid == 0 || mSystemUiPackage.equals(vib.opPkg);
}
- private void noteVibratorOnLocked(int uid, long millis) {
- try {
- mBatteryStatsService.noteVibratorOn(uid, millis);
- FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED, uid, null,
- FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__ON, millis);
- mCurVibUid = uid;
- } catch (RemoteException e) {
- }
- }
-
- private void noteVibratorOffLocked() {
- if (mCurVibUid >= 0) {
- try {
- mBatteryStatsService.noteVibratorOff(mCurVibUid);
- FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED,
- mCurVibUid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__OFF, 0);
- } catch (RemoteException e) { }
- mCurVibUid = -1;
- }
- }
-
private void dumpInternal(PrintWriter pw) {
pw.println("Vibrator Service:");
synchronized (mLock) {
@@ -972,156 +806,6 @@
proto.flush();
}
- /** Thread that plays a single {@link VibrationEffect.Waveform}. */
- private class VibrateWaveformThread extends Thread {
- private final VibrationEffect.Waveform mWaveform;
- private final Vibration mVibration;
-
- private boolean mForceStop;
-
- VibrateWaveformThread(Vibration vib) {
- mWaveform = (VibrationEffect.Waveform) vib.getEffect();
- mVibration = new Vibration(vib.token, /* id= */ 0, /* effect= */ null, vib.attrs,
- vib.uid, vib.opPkg, vib.reason);
- mTmpWorkSource.set(vib.uid);
- mWakeLock.setWorkSource(mTmpWorkSource);
- }
-
- private void delayLocked(long wakeUpTime) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "delayLocked");
- try {
- long durationRemaining = wakeUpTime - SystemClock.uptimeMillis();
- while (durationRemaining > 0) {
- try {
- this.wait(durationRemaining);
- } catch (InterruptedException e) {
- }
- if (mForceStop) {
- break;
- }
- durationRemaining = wakeUpTime - SystemClock.uptimeMillis();
- }
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
- }
-
- public void run() {
- Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY);
- mWakeLock.acquire();
- try {
- boolean finished = playWaveform();
- if (finished) {
- onVibrationFinished();
- }
- } finally {
- mWakeLock.release();
- }
- }
-
- /**
- * Play the waveform.
- *
- * @return true if it finished naturally, false otherwise (e.g. it was canceled).
- */
- public boolean playWaveform() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playWaveform");
- try {
- synchronized (this) {
- final long[] timings = mWaveform.getTimings();
- final int[] amplitudes = mWaveform.getAmplitudes();
- final int len = timings.length;
- final int repeat = mWaveform.getRepeatIndex();
-
- int index = 0;
- long nextStepStartTime = SystemClock.uptimeMillis();
- long nextVibratorStopTime = 0;
- while (!mForceStop) {
- if (index < len) {
- final int amplitude = amplitudes[index];
- final long duration = timings[index++];
- if (duration <= 0) {
- continue;
- }
- if (amplitude != 0) {
- long now = SystemClock.uptimeMillis();
- if (nextVibratorStopTime <= now) {
- // Telling the vibrator to start multiple times usually causes
- // effects to feel "choppy" because the motor resets at every on
- // command. Instead we figure out how long our next "on" period
- // is going to be, tell the motor to stay on for the full
- // duration, and then wake up to change the amplitude at the
- // appropriate intervals.
- long onDuration = getTotalOnDuration(
- timings, amplitudes, index - 1, repeat);
- mVibration.updateEffect(
- VibrationEffect.createOneShot(onDuration, amplitude));
- doVibratorOn(mVibration);
- nextVibratorStopTime = now + onDuration;
- } else {
- // Vibrator is already ON, so just change its amplitude.
- mVibratorController.setAmplitude(amplitude);
- }
- } else {
- // Previous vibration should have already finished, but we make sure
- // the vibrator will be off for the next step when amplitude is 0.
- doVibratorOff();
- }
-
- // We wait until the time this waveform step was supposed to end,
- // calculated from the time it was supposed to start. All start times
- // are calculated from the waveform original start time by adding the
- // input durations. Any scheduling or processing delay should not affect
- // this step's perceived total duration. They will be amortized here.
- nextStepStartTime += duration;
- delayLocked(nextStepStartTime);
- } else if (repeat < 0) {
- break;
- } else {
- index = repeat;
- }
- }
- return !mForceStop;
- }
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
- }
-
- public void cancel() {
- synchronized (this) {
- mThread.mForceStop = true;
- mThread.notify();
- }
- }
-
- /**
- * Get the duration the vibrator will be on starting at startIndex until the next time it's
- * off.
- */
- private long getTotalOnDuration(
- long[] timings, int[] amplitudes, int startIndex, int repeatIndex) {
- int i = startIndex;
- long timing = 0;
- while (amplitudes[i] != 0) {
- timing += timings[i++];
- if (i >= timings.length) {
- if (repeatIndex >= 0) {
- i = repeatIndex;
- // prevent infinite loop
- repeatIndex = -1;
- } else {
- break;
- }
- }
- if (i == startIndex) {
- return 1000;
- }
- }
- return timing;
- }
- }
-
/** Point of injection for test dependencies */
@VisibleForTesting
static class Injector {
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 71fcd1d..950c225 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -1866,6 +1866,13 @@
}
VibrationInfo(VibrationEffect effect) {
+ // First replace prebaked effects with its fallback, if any available.
+ if (effect instanceof VibrationEffect.Prebaked) {
+ VibrationEffect fallback = ((VibrationEffect.Prebaked) effect).getFallbackEffect();
+ if (fallback != null) {
+ effect = fallback;
+ }
+ }
if (effect instanceof VibrationEffect.OneShot) {
VibrationEffect.OneShot oneShot = (VibrationEffect.OneShot) effect;
mPattern = new long[] { 0, oneShot.getDuration() };
@@ -1892,8 +1899,7 @@
throw new ArrayIndexOutOfBoundsException();
}
} else {
- // TODO: Add support for prebaked effects
- Slog.w(TAG, "Pre-baked effects aren't supported on input devices");
+ Slog.w(TAG, "Pre-baked and composed effects aren't supported on input devices");
}
}
}
diff --git a/services/core/java/com/android/server/vibrator/InputDeviceDelegate.java b/services/core/java/com/android/server/vibrator/InputDeviceDelegate.java
index edbc058..3968723 100644
--- a/services/core/java/com/android/server/vibrator/InputDeviceDelegate.java
+++ b/services/core/java/com/android/server/vibrator/InputDeviceDelegate.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.hardware.input.InputManager;
+import android.os.CombinedVibrationEffect;
import android.os.Handler;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
@@ -84,11 +85,21 @@
*
* @return {@link #isAvailable()}
*/
- public boolean vibrateIfAvailable(int uid, String opPkg, VibrationEffect effect,
+ public boolean vibrateIfAvailable(int uid, String opPkg, CombinedVibrationEffect effect,
String reason, VibrationAttributes attrs) {
synchronized (mLock) {
- for (int i = 0; i < mInputDeviceVibrators.size(); i++) {
- mInputDeviceVibrators.valueAt(i).vibrate(uid, opPkg, effect, reason, attrs);
+ // TODO(b/159207608): Pass on the combined vibration once InputManager is merged
+ if (effect instanceof CombinedVibrationEffect.Mono) {
+ VibrationEffect e = ((CombinedVibrationEffect.Mono) effect).getEffect();
+ if (e instanceof VibrationEffect.Prebaked) {
+ VibrationEffect fallback = ((VibrationEffect.Prebaked) e).getFallbackEffect();
+ if (fallback != null) {
+ e = fallback;
+ }
+ }
+ for (int i = 0; i < mInputDeviceVibrators.size(); i++) {
+ mInputDeviceVibrators.valueAt(i).vibrate(uid, opPkg, e, reason, attrs);
+ }
}
return mInputDeviceVibrators.size() > 0;
}
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index b0266d0..fe3b03ab 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.os.CombinedVibrationEffect;
import android.os.IBinder;
import android.os.SystemClock;
import android.os.VibrationAttributes;
@@ -72,14 +73,14 @@
/** The actual effect to be played. */
@Nullable
- private VibrationEffect mEffect;
+ private CombinedVibrationEffect mEffect;
/**
* The original effect that was requested. Typically these two things differ because the effect
* was scaled based on the users vibration intensity settings.
*/
@Nullable
- private VibrationEffect mOriginalEffect;
+ private CombinedVibrationEffect mOriginalEffect;
/**
* Start/end times in unix epoch time. Only to be used for debugging purposes and to correlate
@@ -90,7 +91,7 @@
private long mEndTimeDebug;
private Status mStatus;
- public Vibration(IBinder token, int id, VibrationEffect effect,
+ public Vibration(IBinder token, int id, CombinedVibrationEffect effect,
VibrationAttributes attrs, int uid, String opPkg, String reason) {
this.token = token;
this.mEffect = effect;
@@ -124,7 +125,7 @@
* Replace this vibration effect if given {@code scaledEffect} is different, preserving the
* original one for debug purposes.
*/
- public void updateEffect(@NonNull VibrationEffect newEffect) {
+ public void updateEffect(@NonNull CombinedVibrationEffect newEffect) {
if (newEffect.equals(mEffect)) {
return;
}
@@ -139,7 +140,7 @@
/** Return the effect that should be played by this vibration. */
@Nullable
- public VibrationEffect getEffect() {
+ public CombinedVibrationEffect getEffect() {
return mEffect;
}
@@ -154,8 +155,8 @@
public static final class DebugInfo {
private final long mStartTimeDebug;
private final long mEndTimeDebug;
- private final VibrationEffect mEffect;
- private final VibrationEffect mOriginalEffect;
+ private final CombinedVibrationEffect mEffect;
+ private final CombinedVibrationEffect mOriginalEffect;
private final float mScale;
private final VibrationAttributes mAttrs;
private final int mUid;
@@ -163,8 +164,8 @@
private final String mReason;
private final Status mStatus;
- public DebugInfo(long startTimeDebug, long endTimeDebug, VibrationEffect effect,
- VibrationEffect originalEffect, float scale, VibrationAttributes attrs,
+ public DebugInfo(long startTimeDebug, long endTimeDebug, CombinedVibrationEffect effect,
+ CombinedVibrationEffect originalEffect, float scale, VibrationAttributes attrs,
int uid, String opPkg, String reason, Status status) {
mStartTimeDebug = startTimeDebug;
mEndTimeDebug = endTimeDebug;
@@ -228,7 +229,22 @@
proto.end(token);
}
- private void dumpEffect(ProtoOutputStream proto, long fieldId, VibrationEffect effect) {
+ private void dumpEffect(
+ ProtoOutputStream proto, long fieldId, CombinedVibrationEffect combinedEffect) {
+ VibrationEffect effect;
+ // TODO(b/177805090): add proper support for dumping combined effects to proto
+ if (combinedEffect instanceof CombinedVibrationEffect.Mono) {
+ effect = ((CombinedVibrationEffect.Mono) combinedEffect).getEffect();
+ } else if (combinedEffect instanceof CombinedVibrationEffect.Stereo) {
+ effect = ((CombinedVibrationEffect.Stereo) combinedEffect).getEffects().valueAt(0);
+ } else if (combinedEffect instanceof CombinedVibrationEffect.Sequential) {
+ dumpEffect(proto, fieldId,
+ ((CombinedVibrationEffect.Sequential) combinedEffect).getEffects().get(0));
+ return;
+ } else {
+ // Unknown combined effect, skip dump.
+ return;
+ }
final long token = proto.start(fieldId);
if (effect instanceof VibrationEffect.OneShot) {
dumpEffect(proto, VibrationEffectProto.ONESHOT, (VibrationEffect.OneShot) effect);
diff --git a/services/core/java/com/android/server/vibrator/VibrationScaler.java b/services/core/java/com/android/server/vibrator/VibrationScaler.java
index 5f7e47d..0fa4fe1 100644
--- a/services/core/java/com/android/server/vibrator/VibrationScaler.java
+++ b/services/core/java/com/android/server/vibrator/VibrationScaler.java
@@ -18,12 +18,16 @@
import android.content.Context;
import android.hardware.vibrator.V1_0.EffectStrength;
+import android.os.CombinedVibrationEffect;
import android.os.IExternalVibratorService;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.Slog;
import android.util.SparseArray;
+import java.util.List;
+import java.util.Objects;
+
/** Controls vibration scaling. */
// TODO(b/159207608): Make this package-private once vibrator services are moved to this package
public final class VibrationScaler {
@@ -87,6 +91,43 @@
}
/**
+ * Scale a {@link CombinedVibrationEffect} based on the given usage hint for this vibration.
+ *
+ * @param combinedEffect the effect to be scaled
+ * @param usageHint one of VibrationAttributes.USAGE_*
+ * @return The same given effect, if no changes were made, or a new
+ * {@link CombinedVibrationEffect} with resolved and scaled amplitude
+ */
+ public <T extends CombinedVibrationEffect> T scale(CombinedVibrationEffect combinedEffect,
+ int usageHint) {
+ if (combinedEffect instanceof CombinedVibrationEffect.Mono) {
+ VibrationEffect effect = ((CombinedVibrationEffect.Mono) combinedEffect).getEffect();
+ return (T) CombinedVibrationEffect.createSynced(scale(effect, usageHint));
+ } else if (combinedEffect instanceof CombinedVibrationEffect.Stereo) {
+ SparseArray<VibrationEffect> effects =
+ ((CombinedVibrationEffect.Stereo) combinedEffect).getEffects();
+ CombinedVibrationEffect.SyncedCombination combination =
+ CombinedVibrationEffect.startSynced();
+ for (int i = 0; i < effects.size(); i++) {
+ combination.addVibrator(effects.keyAt(i), scale(effects.valueAt(i), usageHint));
+ }
+ return (T) combination.combine();
+ } else if (combinedEffect instanceof CombinedVibrationEffect.Sequential) {
+ List<CombinedVibrationEffect> effects =
+ ((CombinedVibrationEffect.Sequential) combinedEffect).getEffects();
+ CombinedVibrationEffect.SequentialCombination combination =
+ CombinedVibrationEffect.startSequential();
+ for (CombinedVibrationEffect effect : effects) {
+ combination.addNext(scale(effect, usageHint));
+ }
+ return (T) combination.combine();
+ } else {
+ // Unknown combination, return same effect.
+ return (T) combinedEffect;
+ }
+ }
+
+ /**
* Scale a {@link VibrationEffect} based on the given usage hint for this vibration.
*
* @param effect the effect to be scaled
@@ -100,13 +141,23 @@
int intensity = mSettingsController.getCurrentIntensity(usageHint);
int newStrength = intensityToEffectStrength(intensity);
VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) effect;
+ int strength = prebaked.getEffectStrength();
+ VibrationEffect fallback = prebaked.getFallbackEffect();
- if (prebaked.getEffectStrength() == newStrength) {
+ if (fallback != null) {
+ VibrationEffect scaledFallback = scale(fallback, usageHint);
+ if (strength == newStrength && Objects.equals(fallback, scaledFallback)) {
+ return (T) prebaked;
+ }
+
+ return (T) new VibrationEffect.Prebaked(prebaked.getId(), newStrength,
+ scaledFallback);
+ } else if (strength == newStrength) {
return (T) prebaked;
+ } else {
+ return (T) new VibrationEffect.Prebaked(prebaked.getId(), prebaked.shouldFallback(),
+ newStrength);
}
-
- return (T) new VibrationEffect.Prebaked(
- prebaked.getId(), prebaked.shouldFallback(), newStrength);
}
effect = effect.resolve(mDefaultVibrationAmplitude);
@@ -124,8 +175,6 @@
return effect.scale(scale.factor);
}
-
-
/** Mapping of Vibrator.VIBRATION_INTENSITY_* values to {@link EffectStrength}. */
private static int intensityToEffectStrength(int intensity) {
switch (intensity) {
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
new file mode 100644
index 0000000..a4d888b
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -0,0 +1,791 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import android.annotation.Nullable;
+import android.os.CombinedVibrationEffect;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.os.VibrationEffect;
+import android.os.WorkSource;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.IBatteryStats;
+import com.android.internal.util.FrameworkStatsLog;
+
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.PriorityQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/** Plays a {@link Vibration} in dedicated thread. */
+// TODO(b/159207608): Make this package-private once vibrator services are moved to this package
+public final class VibrationThread extends Thread implements IBinder.DeathRecipient {
+ private static final String TAG = "VibrationThread";
+ private static final boolean DEBUG = false;
+
+ /**
+ * Extra timeout added to the end of each synced vibration step as a timeout for the callback
+ * wait, to ensure it finishes even when callbacks from individual vibrators are lost.
+ */
+ private static final long CALLBACKS_EXTRA_TIMEOUT = 100;
+
+ /** Callbacks for playing a {@link Vibration}. */
+ public interface VibrationCallbacks {
+
+ /**
+ * Callback triggered before starting a synchronized vibration step. This will be called
+ * with {@code requiredCapabilities = 0} if no synchronization is required.
+ *
+ * @param requiredCapabilities The required syncing capabilities for this preparation step.
+ * Expects a combination of values from
+ * IVibratorManager.CAP_PREPARE_* and
+ * IVibratorManager.CAP_MIXED_TRIGGER_*.
+ * @param vibratorIds The id of the vibrators to be prepared.
+ */
+ void prepareSyncedVibration(int requiredCapabilities, int[] vibratorIds);
+
+ /** Callback triggered after synchronized vibrations were prepared. */
+ void triggerSyncedVibration(long vibrationId);
+
+ /** Callback triggered when vibration thread is complete. */
+ void onVibrationEnded(long vibrationId, Vibration.Status status);
+ }
+
+ private final Object mLock = new Object();
+ private final WorkSource mWorkSource = new WorkSource();
+ private final PowerManager.WakeLock mWakeLock;
+ private final IBatteryStats mBatteryStatsService;
+ private final Vibration mVibration;
+ private final VibrationCallbacks mCallbacks;
+ private final SparseArray<VibratorController> mVibrators;
+
+ @GuardedBy("mLock")
+ @Nullable
+ private VibrateStep mCurrentVibrateStep;
+ @GuardedBy("this")
+ private boolean mForceStop;
+
+ // TODO(b/159207608): Remove this constructor once VibratorService is removed
+ public VibrationThread(Vibration vib, VibratorController vibrator,
+ PowerManager.WakeLock wakeLock, IBatteryStats batteryStatsService,
+ VibrationCallbacks callbacks) {
+ this(vib, toSparseArray(vibrator), wakeLock, batteryStatsService, callbacks);
+ }
+
+ public VibrationThread(Vibration vib, SparseArray<VibratorController> availableVibrators,
+ PowerManager.WakeLock wakeLock, IBatteryStats batteryStatsService,
+ VibrationCallbacks callbacks) {
+ mVibration = vib;
+ mCallbacks = callbacks;
+ mWakeLock = wakeLock;
+ mWorkSource.set(vib.uid);
+ mWakeLock.setWorkSource(mWorkSource);
+ mBatteryStatsService = batteryStatsService;
+
+ CombinedVibrationEffect effect = vib.getEffect();
+ mVibrators = new SparseArray<>();
+ for (int i = 0; i < availableVibrators.size(); i++) {
+ if (effect.hasVibrator(availableVibrators.keyAt(i))) {
+ mVibrators.put(availableVibrators.keyAt(i), availableVibrators.valueAt(i));
+ }
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ cancel();
+ }
+
+ @Override
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY);
+ mWakeLock.acquire();
+ try {
+ mVibration.token.linkToDeath(this, 0);
+ Vibration.Status status = playVibration();
+ mCallbacks.onVibrationEnded(mVibration.id, status);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error linking vibration to token death", e);
+ } finally {
+ mVibration.token.unlinkToDeath(this, 0);
+ mWakeLock.release();
+ }
+ }
+
+ /** Cancel current vibration and shuts down the thread gracefully. */
+ public void cancel() {
+ synchronized (this) {
+ mForceStop = true;
+ notify();
+ }
+ }
+
+ /** Notify current vibration that a step has completed on given vibrator. */
+ public void vibratorComplete(int vibratorId) {
+ synchronized (mLock) {
+ if (mCurrentVibrateStep != null) {
+ mCurrentVibrateStep.vibratorComplete(vibratorId);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ SparseArray<VibratorController> getVibrators() {
+ return mVibrators;
+ }
+
+ private Vibration.Status playVibration() {
+ Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playVibration");
+ try {
+ List<Step> steps = generateSteps(mVibration.getEffect());
+ if (steps.isEmpty()) {
+ // No vibrator matching any incoming vibration effect.
+ return Vibration.Status.IGNORED;
+ }
+ Vibration.Status status = Vibration.Status.FINISHED;
+ final int stepCount = steps.size();
+ for (int i = 0; i < stepCount; i++) {
+ Step step = steps.get(i);
+ synchronized (mLock) {
+ if (step instanceof VibrateStep) {
+ mCurrentVibrateStep = (VibrateStep) step;
+ } else {
+ mCurrentVibrateStep = null;
+ }
+ }
+ status = step.play();
+ if (status != Vibration.Status.FINISHED) {
+ // This step was ignored by the vibrators, probably effects were unsupported.
+ break;
+ }
+ if (mForceStop) {
+ break;
+ }
+ }
+ if (mForceStop) {
+ return Vibration.Status.CANCELLED;
+ }
+ return status;
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ }
+ }
+
+ private List<Step> generateSteps(CombinedVibrationEffect effect) {
+ if (effect instanceof CombinedVibrationEffect.Sequential) {
+ CombinedVibrationEffect.Sequential sequential =
+ (CombinedVibrationEffect.Sequential) effect;
+ List<Step> steps = new ArrayList<>();
+ final int sequentialEffectCount = sequential.getEffects().size();
+ for (int i = 0; i < sequentialEffectCount; i++) {
+ int delay = sequential.getDelays().get(i);
+ if (delay > 0) {
+ steps.add(new DelayStep(delay));
+ }
+ steps.addAll(generateSteps(sequential.getEffects().get(i)));
+ }
+ final int stepCount = steps.size();
+ for (int i = 0; i < stepCount; i++) {
+ if (steps.get(i) instanceof VibrateStep) {
+ return steps;
+ }
+ }
+ // No valid vibrate step was generated, ignore effect completely.
+ return Lists.newArrayList();
+ }
+ VibrateStep vibrateStep = null;
+ if (effect instanceof CombinedVibrationEffect.Mono) {
+ vibrateStep = createVibrateStep(mapToAvailableVibrators(
+ ((CombinedVibrationEffect.Mono) effect).getEffect()));
+ } else if (effect instanceof CombinedVibrationEffect.Stereo) {
+ vibrateStep = createVibrateStep(filterByAvailableVibrators(
+ ((CombinedVibrationEffect.Stereo) effect).getEffects()));
+ }
+ return vibrateStep == null ? Lists.newArrayList() : Lists.newArrayList(vibrateStep);
+ }
+
+ @Nullable
+ private VibrateStep createVibrateStep(SparseArray<VibrationEffect> effects) {
+ if (effects.size() == 0) {
+ return null;
+ }
+ if (effects.size() == 1) {
+ // Create simplified step that handles a single vibrator.
+ return new SingleVibrateStep(mVibrators.get(effects.keyAt(0)), effects.valueAt(0));
+ }
+ return new SyncedVibrateStep(effects);
+ }
+
+ private SparseArray<VibrationEffect> mapToAvailableVibrators(VibrationEffect effect) {
+ SparseArray<VibrationEffect> mappedEffects = new SparseArray<>(mVibrators.size());
+ for (int i = 0; i < mVibrators.size(); i++) {
+ mappedEffects.put(mVibrators.keyAt(i), effect);
+ }
+ return mappedEffects;
+ }
+
+ private SparseArray<VibrationEffect> filterByAvailableVibrators(
+ SparseArray<VibrationEffect> effects) {
+ SparseArray<VibrationEffect> filteredEffects = new SparseArray<>();
+ for (int i = 0; i < effects.size(); i++) {
+ if (mVibrators.contains(effects.keyAt(i))) {
+ filteredEffects.put(effects.keyAt(i), effects.valueAt(i));
+ }
+ }
+ return filteredEffects;
+ }
+
+ private static SparseArray<VibratorController> toSparseArray(VibratorController controller) {
+ SparseArray<VibratorController> array = new SparseArray<>(1);
+ array.put(controller.getVibratorInfo().getId(), controller);
+ return array;
+ }
+
+ /**
+ * Get the duration the vibrator will be on for given {@code waveform}, starting at {@code
+ * startIndex} until the next time it's vibrating amplitude is zero.
+ */
+ private static long getVibratorOnDuration(VibrationEffect.Waveform waveform, int startIndex) {
+ long[] timings = waveform.getTimings();
+ int[] amplitudes = waveform.getAmplitudes();
+ int repeatIndex = waveform.getRepeatIndex();
+ int i = startIndex;
+ long timing = 0;
+ while (timings[i] == 0 || amplitudes[i] != 0) {
+ timing += timings[i++];
+ if (i >= timings.length) {
+ if (repeatIndex >= 0) {
+ i = repeatIndex;
+ // prevent infinite loop
+ repeatIndex = -1;
+ } else {
+ break;
+ }
+ }
+ if (i == startIndex) {
+ return 1000;
+ }
+ }
+ return timing;
+ }
+
+ /**
+ * Sleeps until given {@code wakeUpTime}.
+ *
+ * <p>This stops immediately when {@link #cancel()} is called.
+ */
+ private void waitUntil(long wakeUpTime) {
+ synchronized (this) {
+ long durationRemaining = wakeUpTime - SystemClock.uptimeMillis();
+ while (durationRemaining > 0) {
+ try {
+ VibrationThread.this.wait(durationRemaining);
+ } catch (InterruptedException e) {
+ }
+ if (mForceStop) {
+ break;
+ }
+ durationRemaining = wakeUpTime - SystemClock.uptimeMillis();
+ }
+ }
+ }
+
+ /**
+ * Sleeps until given {@link CountDownLatch} has finished or {@code wakeUpTime} was reached.
+ *
+ * <p>This stops immediately when {@link #cancel()} is called.
+ */
+ private void awaitUntil(CountDownLatch counter, long wakeUpTime) {
+ synchronized (this) {
+ long durationRemaining = wakeUpTime - SystemClock.uptimeMillis();
+ while (counter.getCount() > 0 && durationRemaining > 0) {
+ try {
+ counter.await(durationRemaining, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ }
+ if (mForceStop) {
+ break;
+ }
+ durationRemaining = wakeUpTime - SystemClock.uptimeMillis();
+ }
+ }
+ }
+
+ private void noteVibratorOn(long duration) {
+ try {
+ mBatteryStatsService.noteVibratorOn(mVibration.uid, duration);
+ FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED,
+ mVibration.uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__ON,
+ duration);
+ } catch (RemoteException e) {
+ }
+ }
+
+ private void noteVibratorOff() {
+ try {
+ mBatteryStatsService.noteVibratorOff(mVibration.uid);
+ FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED,
+ mVibration.uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__OFF,
+ /* duration= */ 0);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /** Represent a single synchronized step while playing a {@link CombinedVibrationEffect}. */
+ private interface Step {
+ Vibration.Status play();
+ }
+
+ /** Represent a synchronized vibration step. */
+ private interface VibrateStep extends Step {
+ /** Callback to notify a vibrator has finished playing a effect. */
+ void vibratorComplete(int vibratorId);
+ }
+
+ /** Represent a vibration on a single vibrator. */
+ private final class SingleVibrateStep implements VibrateStep {
+ private final VibratorController mVibrator;
+ private final VibrationEffect mEffect;
+ private final CountDownLatch mCounter;
+
+ SingleVibrateStep(VibratorController vibrator, VibrationEffect effect) {
+ mVibrator = vibrator;
+ mEffect = effect;
+ mCounter = new CountDownLatch(1);
+ }
+
+ @Override
+ public void vibratorComplete(int vibratorId) {
+ if (mVibrator.getVibratorInfo().getId() != vibratorId) {
+ return;
+ }
+ if (mEffect instanceof VibrationEffect.OneShot
+ || mEffect instanceof VibrationEffect.Waveform) {
+ // Oneshot and Waveform are controlled by amplitude steps, ignore callbacks.
+ return;
+ }
+ mVibrator.off();
+ mCounter.countDown();
+ }
+
+ @Override
+ public Vibration.Status play() {
+ Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "SingleVibrateStep");
+ long duration = -1;
+ try {
+ if (DEBUG) {
+ Slog.d(TAG, "SingleVibrateStep starting...");
+ }
+ long startTime = SystemClock.uptimeMillis();
+ duration = vibratePredefined(mEffect);
+
+ if (duration > 0) {
+ noteVibratorOn(duration);
+ // Vibration is playing with no need to control amplitudes, just wait for native
+ // callback or timeout.
+ awaitUntil(mCounter, startTime + duration + CALLBACKS_EXTRA_TIMEOUT);
+ return Vibration.Status.FINISHED;
+ }
+
+ startTime = SystemClock.uptimeMillis();
+ AmplitudeStep amplitudeStep = vibrateWithAmplitude(mEffect, startTime);
+ if (amplitudeStep == null) {
+ // Vibration could not be played with or without amplitude steps.
+ return Vibration.Status.IGNORED_UNSUPPORTED;
+ }
+
+ duration = mEffect instanceof VibrationEffect.Prebaked
+ ? ((VibrationEffect.Prebaked) mEffect).getFallbackEffect().getDuration()
+ : mEffect.getDuration();
+ if (duration < Long.MAX_VALUE) {
+ // Only report vibration stats if we know how long we will be vibrating.
+ noteVibratorOn(duration);
+ }
+ while (amplitudeStep != null) {
+ waitUntil(amplitudeStep.startTime);
+ if (mForceStop) {
+ mVibrator.off();
+ return Vibration.Status.CANCELLED;
+ }
+ amplitudeStep.play();
+ amplitudeStep = amplitudeStep.nextStep();
+ }
+
+ return Vibration.Status.FINISHED;
+ } finally {
+ if (duration > 0 && duration < Long.MAX_VALUE) {
+ noteVibratorOff();
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "SingleVibrateStep step done.");
+ }
+ Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ }
+ }
+
+ /**
+ * Try to vibrate given effect using prebaked or composed predefined effects.
+ *
+ * @return the duration, in millis, expected for the vibration, or -1 if effect cannot be
+ * played with predefined effects.
+ */
+ private long vibratePredefined(VibrationEffect effect) {
+ if (effect instanceof VibrationEffect.Prebaked) {
+ VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) effect;
+ long duration = mVibrator.on(prebaked, mVibration.id);
+ if (duration > 0) {
+ return duration;
+ }
+ if (prebaked.getFallbackEffect() != null) {
+ return vibratePredefined(prebaked.getFallbackEffect());
+ }
+ } else if (effect instanceof VibrationEffect.Composed) {
+ VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
+ return mVibrator.on(composed, mVibration.id);
+ }
+ // OneShot and Waveform effects require amplitude change after calling vibrator.on.
+ return -1;
+ }
+
+ /**
+ * Try to vibrate given effect using {@link AmplitudeStep} to control vibration amplitude.
+ *
+ * @return the {@link AmplitudeStep} to start this vibration, or {@code null} if vibration
+ * do not require amplitude control.
+ */
+ private AmplitudeStep vibrateWithAmplitude(VibrationEffect effect, long startTime) {
+ int vibratorId = mVibrator.getVibratorInfo().getId();
+ if (effect instanceof VibrationEffect.OneShot) {
+ VibrationEffect.OneShot oneShot = (VibrationEffect.OneShot) effect;
+ return new AmplitudeStep(vibratorId, oneShot, startTime, startTime);
+ } else if (effect instanceof VibrationEffect.Waveform) {
+ VibrationEffect.Waveform waveform = (VibrationEffect.Waveform) effect;
+ return new AmplitudeStep(vibratorId, waveform, startTime, startTime);
+ } else if (effect instanceof VibrationEffect.Prebaked) {
+ VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) effect;
+ if (prebaked.getFallbackEffect() != null) {
+ return vibrateWithAmplitude(prebaked.getFallbackEffect(), startTime);
+ }
+ }
+ return null;
+ }
+ }
+
+ /** Represent a synchronized vibration step on multiple vibrators. */
+ private final class SyncedVibrateStep implements VibrateStep {
+ private final SparseArray<VibrationEffect> mEffects;
+ private final CountDownLatch mActiveVibratorCounter;
+
+ private final int mRequiredCapabilities;
+ private final int[] mVibratorIds;
+
+ SyncedVibrateStep(SparseArray<VibrationEffect> effects) {
+ mEffects = effects;
+ mActiveVibratorCounter = new CountDownLatch(mEffects.size());
+ // TODO(b/159207608): Calculate required capabilities for syncing this step.
+ mRequiredCapabilities = 0;
+ mVibratorIds = new int[effects.size()];
+ for (int i = 0; i < effects.size(); i++) {
+ mVibratorIds[i] = effects.keyAt(i);
+ }
+ }
+
+ @Override
+ public void vibratorComplete(int vibratorId) {
+ VibrationEffect effect = mEffects.get(vibratorId);
+ if (effect == null) {
+ return;
+ }
+ if (effect instanceof VibrationEffect.OneShot
+ || effect instanceof VibrationEffect.Waveform) {
+ // Oneshot and Waveform are controlled by amplitude steps, ignore callbacks.
+ return;
+ }
+ mVibrators.get(vibratorId).off();
+ mActiveVibratorCounter.countDown();
+ }
+
+ @Override
+ public Vibration.Status play() {
+ Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "SyncedVibrateStep");
+ long timeout = -1;
+ try {
+ if (DEBUG) {
+ Slog.d(TAG, "SyncedVibrateStep starting...");
+ }
+ final PriorityQueue<AmplitudeStep> nextSteps = new PriorityQueue<>(mEffects.size());
+ long startTime = SystemClock.uptimeMillis();
+ mCallbacks.prepareSyncedVibration(mRequiredCapabilities, mVibratorIds);
+ timeout = startVibrating(startTime, nextSteps);
+ mCallbacks.triggerSyncedVibration(mVibration.id);
+ noteVibratorOn(timeout);
+
+ while (!nextSteps.isEmpty()) {
+ AmplitudeStep step = nextSteps.poll();
+ waitUntil(step.startTime);
+ if (mForceStop) {
+ stopAllVibrators();
+ return Vibration.Status.CANCELLED;
+ }
+ step.play();
+ AmplitudeStep nextStep = step.nextStep();
+ if (nextStep == null) {
+ // This vibrator has finished playing the effect for this step.
+ mActiveVibratorCounter.countDown();
+ } else {
+ nextSteps.add(nextStep);
+ }
+ }
+
+ // All OneShot and Waveform effects have finished. Just wait for the other effects
+ // to end via native callbacks before finishing this synced step.
+ awaitUntil(mActiveVibratorCounter, startTime + timeout + CALLBACKS_EXTRA_TIMEOUT);
+ return Vibration.Status.FINISHED;
+ } finally {
+ if (timeout > 0) {
+ noteVibratorOff();
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "SyncedVibrateStep done.");
+ }
+ Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ }
+ }
+
+ /**
+ * Starts playing effects on designated vibrators.
+ *
+ * <p>This includes the {@link VibrationEffect.OneShot} and {@link VibrationEffect.Waveform}
+ * effects, that should start in sync with all other effects in this step. The waveforms are
+ * controlled by {@link AmplitudeStep} added to the {@code nextSteps} queue.
+ *
+ * @return A duration, in millis, to wait for the completion of all vibrations. This ignores
+ * any repeating waveform duration and returns the duration of a single run.
+ */
+ private long startVibrating(long startTime, PriorityQueue<AmplitudeStep> nextSteps) {
+ long maxDuration = 0;
+ for (int i = 0; i < mEffects.size(); i++) {
+ VibratorController controller = mVibrators.get(mEffects.keyAt(i));
+ VibrationEffect effect = mEffects.valueAt(i);
+ maxDuration = Math.max(maxDuration,
+ startVibrating(controller, effect, startTime, nextSteps));
+ }
+ return maxDuration;
+ }
+
+ /**
+ * Play a single effect on a single vibrator.
+ *
+ * @return A duration, in millis, to wait for the completion of this effect. This ignores
+ * any repeating waveform duration and returns the duration of a single run to be used as
+ * timeout for callbacks.
+ */
+ private long startVibrating(VibratorController controller, VibrationEffect effect,
+ long startTime, PriorityQueue<AmplitudeStep> nextSteps) {
+ int vibratorId = controller.getVibratorInfo().getId();
+ long duration;
+ if (effect instanceof VibrationEffect.OneShot) {
+ VibrationEffect.OneShot oneShot = (VibrationEffect.OneShot) effect;
+ duration = oneShot.getDuration();
+ controller.on(duration, mVibration.id);
+ nextSteps.add(
+ new AmplitudeStep(vibratorId, oneShot, startTime, startTime + duration));
+ } else if (effect instanceof VibrationEffect.Waveform) {
+ VibrationEffect.Waveform waveform = (VibrationEffect.Waveform) effect;
+ duration = getVibratorOnDuration(waveform, 0);
+ if (duration > 0) {
+ // Waveform starts by turning vibrator on. Do it in this sync vibrate step.
+ controller.on(duration, mVibration.id);
+ }
+ nextSteps.add(
+ new AmplitudeStep(vibratorId, waveform, startTime, startTime + duration));
+ } else if (effect instanceof VibrationEffect.Prebaked) {
+ VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) effect;
+ duration = controller.on(prebaked, mVibration.id);
+ if (duration <= 0 && prebaked.getFallbackEffect() != null) {
+ return startVibrating(controller, prebaked.getFallbackEffect(), startTime,
+ nextSteps);
+ }
+ } else if (effect instanceof VibrationEffect.Composed) {
+ VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
+ duration = controller.on(composed, mVibration.id);
+ } else {
+ duration = 0;
+ }
+ return duration;
+ }
+
+ private void stopAllVibrators() {
+ for (int vibratorId : mVibratorIds) {
+ VibratorController controller = mVibrators.get(vibratorId);
+ if (controller != null) {
+ controller.off();
+ }
+ }
+ }
+ }
+
+ /** Represent a step to set amplitude on a single vibrator. */
+ private final class AmplitudeStep implements Step, Comparable<AmplitudeStep> {
+ public final int vibratorId;
+ public final VibrationEffect.Waveform waveform;
+ public final int currentIndex;
+ public final long startTime;
+ public final long vibratorStopTime;
+
+ AmplitudeStep(int vibratorId, VibrationEffect.OneShot oneShot,
+ long startTime, long vibratorStopTime) {
+ this(vibratorId, (VibrationEffect.Waveform) VibrationEffect.createWaveform(
+ new long[]{oneShot.getDuration()},
+ new int[]{oneShot.getAmplitude()}, /* repeat= */ -1),
+ startTime,
+ vibratorStopTime);
+ }
+
+ AmplitudeStep(int vibratorId, VibrationEffect.Waveform waveform,
+ long startTime, long vibratorStopTime) {
+ this(vibratorId, waveform, /* index= */ 0, startTime, vibratorStopTime);
+ }
+
+ AmplitudeStep(int vibratorId, VibrationEffect.Waveform waveform,
+ int index, long startTime, long vibratorStopTime) {
+ this.vibratorId = vibratorId;
+ this.waveform = waveform;
+ this.currentIndex = index;
+ this.startTime = startTime;
+ this.vibratorStopTime = vibratorStopTime;
+ }
+
+ @Override
+ public Vibration.Status play() {
+ Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "AmplitudeStep");
+ try {
+ if (DEBUG) {
+ Slog.d(TAG, "AmplitudeStep starting on vibrator " + vibratorId + "...");
+ }
+ VibratorController controller = mVibrators.get(vibratorId);
+ if (currentIndex < 0) {
+ controller.off();
+ if (DEBUG) {
+ Slog.d(TAG, "Vibrator turned off and finishing");
+ }
+ return Vibration.Status.FINISHED;
+ }
+ if (waveform.getTimings()[currentIndex] == 0) {
+ // Skip waveform entries with zero timing.
+ return Vibration.Status.FINISHED;
+ }
+ int amplitude = waveform.getAmplitudes()[currentIndex];
+ if (amplitude == 0) {
+ controller.off();
+ if (DEBUG) {
+ Slog.d(TAG, "Vibrator turned off");
+ }
+ return Vibration.Status.FINISHED;
+ }
+ if (startTime >= vibratorStopTime) {
+ // Vibrator has stopped. Turn vibrator back on for the duration of another
+ // cycle before setting the amplitude.
+ long onDuration = getVibratorOnDuration(waveform, currentIndex);
+ if (onDuration > 0) {
+ controller.on(onDuration, mVibration.id);
+ if (DEBUG) {
+ Slog.d(TAG, "Vibrator turned on for " + onDuration + "ms");
+ }
+ }
+ }
+ controller.setAmplitude(amplitude);
+ if (DEBUG) {
+ Slog.d(TAG, "Amplitude changed to " + amplitude);
+ }
+ return Vibration.Status.FINISHED;
+ } finally {
+ if (DEBUG) {
+ Slog.d(TAG, "AmplitudeStep done.");
+ }
+ Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ }
+ }
+
+ @Override
+ public int compareTo(AmplitudeStep o) {
+ return Long.compare(startTime, o.startTime);
+ }
+
+ /** Return next {@link AmplitudeStep} from this waveform, of {@code null} if finished. */
+ @Nullable
+ public AmplitudeStep nextStep() {
+ if (currentIndex < 0) {
+ // Waveform has ended, no more steps to run.
+ return null;
+ }
+ long nextWakeUpTime = startTime + waveform.getTimings()[currentIndex];
+ int nextIndex = currentIndex + 1;
+ if (nextIndex >= waveform.getTimings().length) {
+ nextIndex = waveform.getRepeatIndex();
+ }
+ return new AmplitudeStep(vibratorId, waveform, nextIndex, nextWakeUpTime,
+ nextVibratorStopTime());
+ }
+
+ /** Return next time the vibrator will stop after this step is played. */
+ private long nextVibratorStopTime() {
+ if (currentIndex < 0 || waveform.getTimings()[currentIndex] == 0
+ || startTime < vibratorStopTime) {
+ return vibratorStopTime;
+ }
+ return startTime + getVibratorOnDuration(waveform, currentIndex);
+ }
+ }
+
+ /** Represent a delay step with fixed duration, that starts counting when it starts playing. */
+ private final class DelayStep implements Step {
+ private final int mDelay;
+
+ DelayStep(int delay) {
+ mDelay = delay;
+ }
+
+ @Override
+ public Vibration.Status play() {
+ Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "DelayStep");
+ try {
+ if (DEBUG) {
+ Slog.d(TAG, "DelayStep of " + mDelay + "ms starting...");
+ }
+ waitUntil(SystemClock.uptimeMillis() + mDelay);
+ return Vibration.Status.FINISHED;
+ } finally {
+ if (DEBUG) {
+ Slog.d(TAG, "DelayStep done.");
+ }
+ Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java
index 311c73b..53f52e2 100644
--- a/services/core/java/com/android/server/vibrator/VibratorController.java
+++ b/services/core/java/com/android/server/vibrator/VibratorController.java
@@ -142,6 +142,11 @@
}
}
+ @VisibleForTesting
+ public NativeWrapper getNativeWrapper() {
+ return mNativeWrapper;
+ }
+
/** Return the {@link VibratorInfo} representing the vibrator controlled by this instance. */
public VibratorInfo getVibratorInfo() {
return mVibratorInfo;
@@ -240,6 +245,8 @@
* {@link OnVibrationCompleteListener}.
*
* <p>This will affect the state of {@link #isVibrating()}.
+ *
+ * @return The duration of the effect playing, or 0 if unsupported.
*/
public long on(VibrationEffect.Prebaked effect, long vibrationId) {
synchronized (mLock) {
@@ -257,15 +264,20 @@
* {@link OnVibrationCompleteListener}.
*
* <p>This will affect the state of {@link #isVibrating()}.
+ *
+ * @return The duration of the effect playing, or 0 if unsupported.
*/
- public void on(VibrationEffect.Composed effect, long vibrationId) {
+ public long on(VibrationEffect.Composed effect, long vibrationId) {
if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) {
- return;
+ return 0;
}
synchronized (mLock) {
mNativeWrapper.compose(effect.getPrimitiveEffects().toArray(
new VibrationEffect.Composition.PrimitiveEffect[0]), vibrationId);
notifyVibratorOnLocked();
+ // Compose don't actually give us an estimated duration, so we just guess here.
+ // TODO(b/177807015): use exposed durations from IVibrator here instead
+ return 20 * effect.getPrimitiveEffects().size();
}
}
diff --git a/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java
index 726536d..0a35db5 100644
--- a/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java
@@ -24,10 +24,6 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -48,6 +44,7 @@
import androidx.test.InstrumentationRegistry;
+import com.android.server.vibrator.FakeVibratorControllerProvider;
import com.android.server.vibrator.VibratorController;
import org.junit.After;
@@ -81,7 +78,7 @@
@Mock private PowerManagerInternal mPowerManagerInternalMock;
@Mock private PowerSaveState mPowerSaveStateMock;
- private final Map<Integer, VibratorController.NativeWrapper> mNativeWrappers = new HashMap<>();
+ private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>();
private TestLooper mTestLooper;
@@ -117,8 +114,8 @@
@Override
VibratorController createVibratorController(int vibratorId,
VibratorController.OnVibrationCompleteListener listener) {
- return new VibratorController(
- vibratorId, listener, mNativeWrappers.get(vibratorId));
+ return mVibratorProviders.get(vibratorId)
+ .newVibratorController(vibratorId, listener);
}
});
service.systemReady();
@@ -126,9 +123,12 @@
}
@Test
- public void createService_initializesNativeService() {
+ public void createService_initializesNativeManagerServiceAndVibrators() {
+ mockVibrators(1, 2);
createService();
verify(mNativeWrapperMock).init();
+ assertTrue(mVibratorProviders.get(1).isInitialized());
+ assertTrue(mVibratorProviders.get(2).isInitialized());
}
@Test
@@ -139,28 +139,23 @@
@Test
public void getVibratorIds_withNonEmptyResultFromNative_returnsSameArray() {
- mNativeWrappers.put(1, mockVibrator(/* capabilities= */ 0));
- mNativeWrappers.put(2, mockVibrator(/* capabilities= */ 0));
- when(mNativeWrapperMock.getVibratorIds()).thenReturn(new int[]{2, 1});
+ mockVibrators(2, 1);
assertArrayEquals(new int[]{2, 1}, createService().getVibratorIds());
}
@Test
public void getVibratorInfo_withMissingVibratorId_returnsNull() {
- mockVibrators(mockVibrator(/* capabilities= */ 0));
+ mockVibrators(1);
assertNull(createService().getVibratorInfo(2));
}
@Test
public void getVibratorInfo_withExistingVibratorId_returnsHalInfoForVibrator() {
- VibratorController.NativeWrapper vibratorMock = mockVibrator(
- IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_AMPLITUDE_CONTROL);
- when(vibratorMock.getSupportedEffects()).thenReturn(
- new int[]{VibrationEffect.EFFECT_CLICK});
- when(vibratorMock.getSupportedPrimitives()).thenReturn(
- new int[]{VibrationEffect.Composition.PRIMITIVE_CLICK});
- mNativeWrappers.put(1, vibratorMock);
- when(mNativeWrapperMock.getVibratorIds()).thenReturn(new int[]{1});
+ mockVibrators(1);
+ FakeVibratorControllerProvider vibrator = mVibratorProviders.get(1);
+ vibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS, IVibrator.CAP_AMPLITUDE_CONTROL);
+ vibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+ vibrator.setSupportedPrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK);
VibratorInfo info = createService().getVibratorInfo(1);
assertNotNull(info);
@@ -178,105 +173,95 @@
@Test
public void setAlwaysOnEffect_withMono_enablesAlwaysOnEffectToAllVibratorsWithCapability() {
- VibratorController.NativeWrapper[] vibratorMocks = new VibratorController.NativeWrapper[]{
- mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL),
- mockVibrator(/* capabilities= */ 0),
- mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL),
- };
- mockVibrators(vibratorMocks);
+ mockVibrators(1, 2, 3);
+ mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
+ mVibratorProviders.get(3).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced(
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
assertTrue(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1, effect, ALARM_ATTRS));
- // Only vibrators 0 and 2 have always-on capabilities.
- verify(vibratorMocks[0]).alwaysOnEnable(
- eq(1L), eq((long) VibrationEffect.EFFECT_CLICK),
- eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG));
- verify(vibratorMocks[1], never()).alwaysOnEnable(anyLong(), anyLong(), anyLong());
- verify(vibratorMocks[2]).alwaysOnEnable(
- eq(1L), eq((long) VibrationEffect.EFFECT_CLICK),
- eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG));
+ VibrationEffect.Prebaked expectedEffect = new VibrationEffect.Prebaked(
+ VibrationEffect.EFFECT_CLICK, false, VibrationEffect.EFFECT_STRENGTH_STRONG);
+
+ // Only vibrators 1 and 3 have always-on capabilities.
+ assertEquals(mVibratorProviders.get(1).getAlwaysOnEffect(1), expectedEffect);
+ assertNull(mVibratorProviders.get(2).getAlwaysOnEffect(1));
+ assertEquals(mVibratorProviders.get(3).getAlwaysOnEffect(1), expectedEffect);
}
@Test
public void setAlwaysOnEffect_withStereo_enablesAlwaysOnEffectToAllVibratorsWithCapability() {
- VibratorController.NativeWrapper[] vibratorMocks = new VibratorController.NativeWrapper[] {
- mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL),
- mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL),
- mockVibrator(0),
- mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL),
- };
- mockVibrators(vibratorMocks);
+ mockVibrators(1, 2, 3, 4);
+ mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
+ mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
+ mVibratorProviders.get(4).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
- .addVibrator(0, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK))
- .addVibrator(1, VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK))
- .addVibrator(2, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK))
+ .addVibrator(1, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK))
+ .addVibrator(2, VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK))
+ .addVibrator(3, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK))
.combine();
assertTrue(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1, effect, ALARM_ATTRS));
- // Enables click on vibrator 0 and tick on vibrator 1 only.
- verify(vibratorMocks[0]).alwaysOnEnable(
- eq(1L), eq((long) VibrationEffect.EFFECT_CLICK),
- eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG));
- verify(vibratorMocks[1]).alwaysOnEnable(
- eq(1L), eq((long) VibrationEffect.EFFECT_TICK),
- eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG));
- verify(vibratorMocks[2], never()).alwaysOnEnable(anyLong(), anyLong(), anyLong());
- verify(vibratorMocks[3], never()).alwaysOnEnable(anyLong(), anyLong(), anyLong());
+ VibrationEffect.Prebaked expectedClick = new VibrationEffect.Prebaked(
+ VibrationEffect.EFFECT_CLICK, false, VibrationEffect.EFFECT_STRENGTH_STRONG);
+
+ VibrationEffect.Prebaked expectedTick = new VibrationEffect.Prebaked(
+ VibrationEffect.EFFECT_TICK, false, VibrationEffect.EFFECT_STRENGTH_STRONG);
+
+ // Enables click on vibrator 1 and tick on vibrator 2 only.
+ assertEquals(mVibratorProviders.get(1).getAlwaysOnEffect(1), expectedClick);
+ assertEquals(mVibratorProviders.get(2).getAlwaysOnEffect(1), expectedTick);
+ assertNull(mVibratorProviders.get(3).getAlwaysOnEffect(1));
+ assertNull(mVibratorProviders.get(4).getAlwaysOnEffect(1));
}
@Test
public void setAlwaysOnEffect_withNullEffect_disablesAlwaysOnEffects() {
- VibratorController.NativeWrapper[] vibratorMocks = new VibratorController.NativeWrapper[] {
- mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL),
- mockVibrator(0),
- mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL),
- };
- mockVibrators(vibratorMocks);
+ mockVibrators(1, 2, 3);
+ mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
+ mVibratorProviders.get(3).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
+
+ CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced(
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
+ assertTrue(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1, effect, ALARM_ATTRS));
assertTrue(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1, null, ALARM_ATTRS));
- // Disables only 0 and 2 that have capability.
- verify(vibratorMocks[0]).alwaysOnDisable(eq(1L));
- verify(vibratorMocks[1], never()).alwaysOnDisable(anyLong());
- verify(vibratorMocks[2]).alwaysOnDisable(eq(1L));
+ assertNull(mVibratorProviders.get(1).getAlwaysOnEffect(1));
+ assertNull(mVibratorProviders.get(2).getAlwaysOnEffect(1));
+ assertNull(mVibratorProviders.get(3).getAlwaysOnEffect(1));
}
@Test
public void setAlwaysOnEffect_withNonPrebakedEffect_ignoresEffect() {
- VibratorController.NativeWrapper vibratorMock =
- mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL);
- mockVibrators(vibratorMock);
+ mockVibrators(1);
+ mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced(
VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE));
assertFalse(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1, effect, ALARM_ATTRS));
- verify(vibratorMock, never()).alwaysOnEnable(anyLong(), anyLong(), anyLong());
- verify(vibratorMock, never()).alwaysOnDisable(anyLong());
+ assertNull(mVibratorProviders.get(1).getAlwaysOnEffect(1));
}
@Test
public void setAlwaysOnEffect_withNonSyncedEffect_ignoresEffect() {
- VibratorController.NativeWrapper vibratorMock =
- mockVibrator(IVibrator.CAP_ALWAYS_ON_CONTROL);
- mockVibrators(vibratorMock);
+ mockVibrators(1);
+ mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
CombinedVibrationEffect effect = CombinedVibrationEffect.startSequential()
.addNext(0, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
.combine();
assertFalse(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1, effect, ALARM_ATTRS));
- verify(vibratorMock, never()).alwaysOnEnable(anyLong(), anyLong(), anyLong());
- verify(vibratorMock, never()).alwaysOnDisable(anyLong());
+ assertNull(mVibratorProviders.get(1).getAlwaysOnEffect(1));
}
@Test
public void setAlwaysOnEffect_withNoVibratorWithCapability_ignoresEffect() {
- VibratorController.NativeWrapper vibratorMock = mockVibrator(0);
- mockVibrators(vibratorMock);
+ mockVibrators(1);
VibratorManagerService service = createService();
CombinedVibrationEffect mono = CombinedVibrationEffect.createSynced(
@@ -287,8 +272,7 @@
assertFalse(service.setAlwaysOnEffect(UID, PACKAGE_NAME, 1, mono, ALARM_ATTRS));
assertFalse(service.setAlwaysOnEffect(UID, PACKAGE_NAME, 2, stereo, ALARM_ATTRS));
- verify(vibratorMock, never()).alwaysOnEnable(anyLong(), anyLong(), anyLong());
- verify(vibratorMock, never()).alwaysOnDisable(anyLong());
+ assertNull(mVibratorProviders.get(1).getAlwaysOnEffect(1));
}
@Test
@@ -310,19 +294,12 @@
"Not implemented", () -> service.cancelVibrate(service));
}
- private VibratorController.NativeWrapper mockVibrator(int capabilities) {
- VibratorController.NativeWrapper wrapper = mock(VibratorController.NativeWrapper.class);
- when(wrapper.getCapabilities()).thenReturn((long) capabilities);
- return wrapper;
- }
-
- private void mockVibrators(VibratorController.NativeWrapper... wrappers) {
- int[] ids = new int[wrappers.length];
- for (int i = 0; i < wrappers.length; i++) {
- ids[i] = i;
- mNativeWrappers.put(i, wrappers[i]);
+ private void mockVibrators(int... vibratorIds) {
+ for (int vibratorId : vibratorIds) {
+ mVibratorProviders.put(vibratorId,
+ new FakeVibratorControllerProvider(mTestLooper.getLooper()));
}
- when(mNativeWrapperMock.getVibratorIds()).thenReturn(ids);
+ when(mNativeWrapperMock.getVibratorIds()).thenReturn(vibratorIds);
}
private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
diff --git a/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java b/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java
index 32ca7b5..92256e2 100644
--- a/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java
@@ -19,21 +19,17 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import static org.mockito.AdditionalMatchers.gt;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.intThat;
-import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.app.AppOpsManager;
@@ -54,8 +50,6 @@
import android.os.PowerManagerInternal;
import android.os.PowerSaveState;
import android.os.Process;
-import android.os.RemoteException;
-import android.os.SystemClock;
import android.os.UserHandle;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
@@ -70,16 +64,15 @@
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.internal.util.test.FakeSettingsProviderRule;
+import com.android.server.vibrator.FakeVibratorControllerProvider;
import com.android.server.vibrator.VibratorController;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
-import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
-import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -87,7 +80,7 @@
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
/**
* Tests for {@link VibratorService}.
@@ -99,6 +92,7 @@
public class VibratorServiceTest {
private static final int UID = Process.ROOT_UID;
+ private static final int VIBRATOR_ID = 1;
private static final String PACKAGE_NAME = "package";
private static final PowerSaveState NORMAL_POWER_STATE = new PowerSaveState.Builder().build();
private static final PowerSaveState LOW_POWER_STATE = new PowerSaveState.Builder()
@@ -123,7 +117,6 @@
// TODO(b/131311651): replace with a FakeVibrator instead.
@Mock private Vibrator mVibratorMock;
@Mock private AppOpsManager mAppOpsManagerMock;
- @Mock private VibratorController.NativeWrapper mNativeWrapperMock;
@Mock private IVibratorStateListener mVibratorStateListenerMock;
@Mock private IInputManager mIInputManagerMock;
@Mock private IBinder mVibratorStateListenerBinderMock;
@@ -131,10 +124,12 @@
private TestLooper mTestLooper;
private ContextWrapper mContextSpy;
private PowerManagerInternal.LowPowerModeListener mRegisteredPowerModeListener;
+ private FakeVibratorControllerProvider mVibratorProvider;
@Before
public void setUp() throws Exception {
mTestLooper = new TestLooper();
+ mVibratorProvider = new FakeVibratorControllerProvider(mTestLooper.getLooper());
mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
InputManager inputManager = InputManager.resetInstance(mIInputManagerMock);
@@ -183,7 +178,7 @@
@Override
VibratorController createVibratorController(
VibratorController.OnVibrationCompleteListener listener) {
- return new VibratorController(0, listener, mNativeWrapperMock);
+ return mVibratorProvider.newVibratorController(VIBRATOR_ID, listener);
}
@Override
@@ -203,25 +198,23 @@
@Test
public void createService_initializesNativeService() {
createService();
- verify(mNativeWrapperMock).init(eq(0), notNull());
- verify(mNativeWrapperMock, times(2)).off(); // Called from constructor and onSystemReady
+ assertTrue(mVibratorProvider.isInitialized());
}
@Test
public void hasVibrator_withVibratorHalPresent_returnsTrue() {
- when(mNativeWrapperMock.isAvailable()).thenReturn(true);
assertTrue(createService().hasVibrator());
}
@Test
public void hasVibrator_withNoVibratorHalPresent_returnsFalse() {
- when(mNativeWrapperMock.isAvailable()).thenReturn(false);
+ mVibratorProvider.disableVibrators();
assertFalse(createService().hasVibrator());
}
@Test
public void hasAmplitudeControl_withAmplitudeControlSupport_returnsTrue() {
- mockVibratorCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+ mVibratorProvider.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
assertTrue(createService().hasAmplitudeControl());
}
@@ -234,18 +227,17 @@
public void hasAmplitudeControl_withInputDevices_returnsTrue() throws Exception {
when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1});
when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1));
- mockVibratorCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+ mVibratorProvider.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1);
assertTrue(createService().hasAmplitudeControl());
}
@Test
public void getVibratorInfo_returnsSameInfoFromNative() {
- mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_AMPLITUDE_CONTROL);
- when(mNativeWrapperMock.getSupportedEffects())
- .thenReturn(new int[]{VibrationEffect.EFFECT_CLICK});
- when(mNativeWrapperMock.getSupportedPrimitives())
- .thenReturn(new int[]{VibrationEffect.Composition.PRIMITIVE_CLICK});
+ mVibratorProvider.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS,
+ IVibrator.CAP_AMPLITUDE_CONTROL);
+ mVibratorProvider.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+ mVibratorProvider.setSupportedPrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK);
VibratorInfo info = createService().getVibratorInfo();
assertTrue(info.hasAmplitudeControl());
@@ -258,7 +250,7 @@
}
@Test
- public void vibrate_withRingtone_usesRingtoneSettings() {
+ public void vibrate_withRingtone_usesRingtoneSettings() throws Exception {
setRingerMode(AudioManager.RINGER_MODE_NORMAL);
setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 0);
setGlobalSetting(Settings.Global.APPLY_RAMPING_RINGER, 0);
@@ -266,34 +258,34 @@
setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 0);
setGlobalSetting(Settings.Global.APPLY_RAMPING_RINGER, 1);
- vibrate(createService(), VibrationEffect.createOneShot(10, 10), RINGTONE_ATTRS);
+ vibrateAndWait(createService(), VibrationEffect.createOneShot(10, 10), RINGTONE_ATTRS);
setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 1);
setGlobalSetting(Settings.Global.APPLY_RAMPING_RINGER, 0);
- vibrate(createService(), VibrationEffect.createOneShot(100, 100), RINGTONE_ATTRS);
+ vibrateAndWait(createService(), VibrationEffect.createOneShot(100, 100), RINGTONE_ATTRS);
- InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
- inOrderVerifier.verify(mNativeWrapperMock, never()).on(eq(1L), anyLong());
- inOrderVerifier.verify(mNativeWrapperMock).on(eq(10L), anyLong());
- inOrderVerifier.verify(mNativeWrapperMock).on(eq(100L), anyLong());
+ List<VibrationEffect> effects = mVibratorProvider.getEffects();
+ assertEquals(2, effects.size());
+ assertEquals(10, effects.get(0).getDuration());
+ assertEquals(100, effects.get(1).getDuration());
}
@Test
- public void vibrate_withPowerModeChange_usesLowPowerModeState() {
+ public void vibrate_withPowerModeChange_usesLowPowerModeState() throws Exception {
VibratorService service = createService();
mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
vibrate(service, VibrationEffect.createOneShot(1, 1), HAPTIC_FEEDBACK_ATTRS);
- vibrate(service, VibrationEffect.createOneShot(2, 2), RINGTONE_ATTRS);
+ vibrateAndWait(service, VibrationEffect.createOneShot(2, 2), RINGTONE_ATTRS);
mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE);
- vibrate(service, VibrationEffect.createOneShot(3, 3), /* attributes= */ null);
- vibrate(service, VibrationEffect.createOneShot(4, 4), NOTIFICATION_ATTRS);
+ vibrateAndWait(service, VibrationEffect.createOneShot(3, 3), /* attributes= */ null);
+ vibrateAndWait(service, VibrationEffect.createOneShot(4, 4), NOTIFICATION_ATTRS);
- InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
- inOrderVerifier.verify(mNativeWrapperMock, never()).on(eq(1L), anyLong());
- inOrderVerifier.verify(mNativeWrapperMock).on(eq(2L), anyLong());
- inOrderVerifier.verify(mNativeWrapperMock).on(eq(3L), anyLong());
- inOrderVerifier.verify(mNativeWrapperMock).on(eq(4L), anyLong());
+ List<VibrationEffect> effects = mVibratorProvider.getEffects();
+ assertEquals(3, effects.size());
+ assertEquals(2, effects.get(0).getDuration());
+ assertEquals(3, effects.get(1).getDuration());
+ assertEquals(4, effects.get(2).getDuration());
}
@Test
@@ -349,55 +341,55 @@
when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1));
setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1);
VibratorService service = createService();
- Mockito.clearInvocations(mNativeWrapperMock);
VibrationEffect effect = VibrationEffect.createOneShot(100, 128);
- vibrate(service, effect);
- assertFalse(service.isVibrating());
-
+ vibrate(service, effect, ALARM_ATTRS);
verify(mIInputManagerMock).vibrate(eq(1), eq(effect), any());
- verify(mNativeWrapperMock, never()).on(anyLong(), anyLong());
+
+ // VibrationThread will start this vibration async, so wait before checking it never played.
+ Thread.sleep(10);
+ assertTrue(mVibratorProvider.getEffects().isEmpty());
}
@Test
- public void vibrate_withOneShotAndAmplitudeControl_turnsVibratorOnAndSetsAmplitude() {
- mockVibratorCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+ public void vibrate_withOneShotAndAmplitudeControl_turnsVibratorOnAndSetsAmplitude()
+ throws Exception {
+ mVibratorProvider.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
VibratorService service = createService();
- Mockito.clearInvocations(mNativeWrapperMock);
- vibrate(service, VibrationEffect.createOneShot(100, 128));
- assertTrue(service.isVibrating());
+ vibrateAndWait(service, VibrationEffect.createOneShot(100, 128), ALARM_ATTRS);
- verify(mNativeWrapperMock).off();
- verify(mNativeWrapperMock).on(eq(100L), gt(0L));
- verify(mNativeWrapperMock).setAmplitude(eq(128));
+ List<VibrationEffect> effects = mVibratorProvider.getEffects();
+ assertEquals(1, effects.size());
+ assertEquals(100, effects.get(0).getDuration());
+ assertEquals(Arrays.asList(128), mVibratorProvider.getAmplitudes());
}
@Test
- public void vibrate_withOneShotAndNoAmplitudeControl_turnsVibratorOnAndIgnoresAmplitude() {
+ public void vibrate_withOneShotAndNoAmplitudeControl_turnsVibratorOnAndIgnoresAmplitude()
+ throws Exception {
VibratorService service = createService();
- Mockito.clearInvocations(mNativeWrapperMock);
+ clearInvocations();
- vibrate(service, VibrationEffect.createOneShot(100, 128));
- assertTrue(service.isVibrating());
+ vibrateAndWait(service, VibrationEffect.createOneShot(100, 128), ALARM_ATTRS);
- verify(mNativeWrapperMock).off();
- verify(mNativeWrapperMock).on(eq(100L), gt(0L));
- verify(mNativeWrapperMock, never()).setAmplitude(anyInt());
+ List<VibrationEffect> effects = mVibratorProvider.getEffects();
+ assertEquals(1, effects.size());
+ assertEquals(100, effects.get(0).getDuration());
+ assertTrue(mVibratorProvider.getAmplitudes().isEmpty());
}
@Test
- public void vibrate_withPrebaked_performsEffect() {
- when(mNativeWrapperMock.getSupportedEffects())
- .thenReturn(new int[]{VibrationEffect.EFFECT_CLICK});
+ public void vibrate_withPrebaked_performsEffect() throws Exception {
+ mVibratorProvider.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
VibratorService service = createService();
- Mockito.clearInvocations(mNativeWrapperMock);
- vibrate(service, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
+ VibrationEffect effect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK);
+ vibrateAndWait(service, effect, ALARM_ATTRS);
- verify(mNativeWrapperMock).off();
- verify(mNativeWrapperMock).perform(eq((long) VibrationEffect.EFFECT_CLICK),
- eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG), gt(0L));
+ VibrationEffect.Prebaked expectedEffect = new VibrationEffect.Prebaked(
+ VibrationEffect.EFFECT_CLICK, false, VibrationEffect.EFFECT_STRENGTH_STRONG);
+ assertEquals(Arrays.asList(expectedEffect), mVibratorProvider.getEffects());
}
@Test
@@ -407,120 +399,62 @@
when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1));
setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1);
VibratorService service = createService();
- Mockito.clearInvocations(mNativeWrapperMock);
- vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
- assertFalse(service.isVibrating());
-
- // Wait for VibrateThread to turn input device vibrator ON.
- Thread.sleep(5);
+ vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), ALARM_ATTRS);
verify(mIInputManagerMock).vibrate(eq(1), any(), any());
- verify(mNativeWrapperMock, never()).on(anyLong(), anyLong());
- verify(mNativeWrapperMock, never()).perform(anyLong(), anyLong(), anyLong());
+
+ // VibrationThread will start this vibration async, so wait before checking it never played.
+ Thread.sleep(10);
+ assertTrue(mVibratorProvider.getEffects().isEmpty());
}
@Test
- public void vibrate_withComposed_performsEffect() {
- mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+ public void vibrate_withComposed_performsEffect() throws Exception {
+ mVibratorProvider.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
VibratorService service = createService();
- Mockito.clearInvocations(mNativeWrapperMock);
VibrationEffect effect = VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10)
.compose();
- vibrate(service, effect);
-
- ArgumentCaptor<VibrationEffect.Composition.PrimitiveEffect[]> primitivesCaptor =
- ArgumentCaptor.forClass(VibrationEffect.Composition.PrimitiveEffect[].class);
-
- verify(mNativeWrapperMock).off();
- verify(mNativeWrapperMock).compose(primitivesCaptor.capture(), gt(0L));
-
- // Check all primitive effect fields are passed down to the HAL.
- assertEquals(1, primitivesCaptor.getValue().length);
- VibrationEffect.Composition.PrimitiveEffect primitive = primitivesCaptor.getValue()[0];
- assertEquals(VibrationEffect.Composition.PRIMITIVE_CLICK, primitive.id);
- assertEquals(0.5f, primitive.scale, /* delta= */ 1e-2);
- assertEquals(10, primitive.delay);
+ vibrateAndWait(service, effect, ALARM_ATTRS);
+ assertEquals(Arrays.asList(effect), mVibratorProvider.getEffects());
}
@Test
- public void vibrate_withComposedAndInputDevices_vibratesInputDevices()
- throws Exception {
+ public void vibrate_withComposedAndInputDevices_vibratesInputDevices() throws Exception {
when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1, 2});
when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1));
when(mIInputManagerMock.getInputDevice(2)).thenReturn(createInputDeviceWithVibrator(2));
setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1);
VibratorService service = createService();
- Mockito.clearInvocations(mNativeWrapperMock);
VibrationEffect effect = VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10)
.compose();
- vibrate(service, effect);
- assertFalse(service.isVibrating());
+ vibrate(service, effect, ALARM_ATTRS);
+ InOrder inOrderVerifier = inOrder(mIInputManagerMock);
+ inOrderVerifier.verify(mIInputManagerMock).vibrate(eq(1), eq(effect), any());
+ inOrderVerifier.verify(mIInputManagerMock).vibrate(eq(2), eq(effect), any());
- verify(mIInputManagerMock).vibrate(eq(1), eq(effect), any());
- verify(mIInputManagerMock).vibrate(eq(2), eq(effect), any());
- verify(mNativeWrapperMock, never()).compose(any(), anyLong());
+ // VibrationThread will start this vibration async, so wait before checking it never played.
+ Thread.sleep(10);
+ assertTrue(mVibratorProvider.getEffects().isEmpty());
}
@Test
public void vibrate_withWaveform_controlsVibratorAmplitudeDuringTotalVibrationTime()
throws Exception {
- mockVibratorCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+ mVibratorProvider.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
VibratorService service = createService();
- Mockito.clearInvocations(mNativeWrapperMock);
VibrationEffect effect = VibrationEffect.createWaveform(
new long[]{10, 10, 10}, new int[]{100, 200, 50}, -1);
- vibrate(service, effect);
+ vibrateAndWait(service, effect, ALARM_ATTRS);
- // Wait for VibrateThread to finish: 10ms 100, 10ms 200, 10ms 50.
- Thread.sleep(40);
- InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
- inOrderVerifier.verify(mNativeWrapperMock).off();
- inOrderVerifier.verify(mNativeWrapperMock).on(eq(30L), anyLong());
- inOrderVerifier.verify(mNativeWrapperMock).setAmplitude(eq(100));
- inOrderVerifier.verify(mNativeWrapperMock).setAmplitude(eq(200));
- inOrderVerifier.verify(mNativeWrapperMock).setAmplitude(eq(50));
- inOrderVerifier.verify(mNativeWrapperMock).off();
- }
-
- @Test
- public void vibrate_withWaveform_totalVibrationTimeRespected() throws Exception {
- int totalDuration = 10_000; // 10s
- int stepDuration = 25; // 25ms
-
- // 25% of the first waveform step will be spent on the native on() call.
- mockVibratorCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
- doAnswer(invocation -> {
- Thread.currentThread().sleep(stepDuration / 4);
- return null;
- }).when(mNativeWrapperMock).on(anyLong(), anyLong());
- // 25% of each waveform step will be spent on the native setAmplitude() call..
- doAnswer(invocation -> {
- Thread.currentThread().sleep(stepDuration / 4);
- return null;
- }).when(mNativeWrapperMock).setAmplitude(anyInt());
-
- VibratorService service = createService();
-
- int stepCount = totalDuration / stepDuration;
- long[] timings = new long[stepCount];
- int[] amplitudes = new int[stepCount];
- Arrays.fill(timings, stepDuration);
- Arrays.fill(amplitudes, VibrationEffect.DEFAULT_AMPLITUDE);
- VibrationEffect effect = VibrationEffect.createWaveform(timings, amplitudes, -1);
-
- int perceivedDuration = vibrateAndMeasure(service, effect, /* timeoutSecs= */ 15);
- int delay = Math.abs(perceivedDuration - totalDuration);
-
- // Allow some delay for thread scheduling and callback triggering.
- int maxDelay = (int) (0.05 * totalDuration); // < 5% of total duration
- assertTrue("Waveform with perceived delay of " + delay + "ms,"
- + " expected less than " + maxDelay + "ms",
- delay < maxDelay);
+ assertEquals(Arrays.asList(100, 200, 50), mVibratorProvider.getAmplitudes());
+ assertEquals(
+ Arrays.asList(VibrationEffect.createOneShot(30, VibrationEffect.DEFAULT_AMPLITUDE)),
+ mVibratorProvider.getEffects());
}
@Test
@@ -529,123 +463,52 @@
when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1));
setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1);
VibratorService service = createService();
- Mockito.clearInvocations(mNativeWrapperMock);
VibrationEffect effect = VibrationEffect.createWaveform(
new long[]{10, 10, 10}, new int[]{100, 200, 50}, -1);
- vibrate(service, effect);
- assertFalse(service.isVibrating());
-
- // Wait for VibrateThread to turn input device vibrator ON.
- Thread.sleep(5);
+ vibrate(service, effect, ALARM_ATTRS);
verify(mIInputManagerMock).vibrate(eq(1), eq(effect), any());
- verify(mNativeWrapperMock, never()).on(anyLong(), anyLong());
+
+ // VibrationThread will start this vibration async, so wait before checking it never played.
+ Thread.sleep(10);
+ assertTrue(mVibratorProvider.getEffects().isEmpty());
}
@Test
- public void vibrate_withOneShotAndNativeCallbackTriggered_finishesVibration() {
+ public void vibrate_withNativeCallbackTriggered_finishesVibration() throws Exception {
+ mVibratorProvider.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
VibratorService service = createService();
- doAnswer(invocation -> {
- service.onVibrationComplete(invocation.getArgument(1));
- return null;
- }).when(mNativeWrapperMock).on(anyLong(), anyLong());
- Mockito.clearInvocations(mNativeWrapperMock);
- vibrate(service, VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE));
+ vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), ALARM_ATTRS);
- InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
- inOrderVerifier.verify(mNativeWrapperMock).off();
- inOrderVerifier.verify(mNativeWrapperMock).on(eq(100L), gt(0L));
- inOrderVerifier.verify(mNativeWrapperMock).off();
- }
-
- @Test
- public void vibrate_withPrebakedAndNativeCallbackTriggered_finishesVibration() {
- when(mNativeWrapperMock.getSupportedEffects())
- .thenReturn(new int[]{VibrationEffect.EFFECT_CLICK});
- VibratorService service = createService();
- doAnswer(invocation -> {
- service.onVibrationComplete(invocation.getArgument(2));
- return 10_000L; // 10s
- }).when(mNativeWrapperMock).perform(anyLong(), anyLong(), anyLong());
- Mockito.clearInvocations(mNativeWrapperMock);
-
- vibrate(service, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
-
- InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
- inOrderVerifier.verify(mNativeWrapperMock).off();
- inOrderVerifier.verify(mNativeWrapperMock).perform(
- eq((long) VibrationEffect.EFFECT_CLICK),
- eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG),
- gt(0L));
- inOrderVerifier.verify(mNativeWrapperMock).off();
- }
-
- @Test
- public void vibrate_withWaveformAndNativeCallback_callbackIgnoredAndWaveformPlaysCompletely()
- throws Exception {
- VibratorService service = createService();
- doAnswer(invocation -> {
- service.onVibrationComplete(invocation.getArgument(1));
- return null;
- }).when(mNativeWrapperMock).on(anyLong(), anyLong());
- Mockito.clearInvocations(mNativeWrapperMock);
-
- VibrationEffect effect = VibrationEffect.createWaveform(new long[]{1, 3, 1, 2}, -1);
- vibrate(service, effect);
-
- // Wait for VibrateThread to finish: 1ms OFF, 3ms ON, 1ms OFF, 2ms ON.
- Thread.sleep(15);
- InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
- inOrderVerifier.verify(mNativeWrapperMock, times(2)).off();
- inOrderVerifier.verify(mNativeWrapperMock).on(eq(3L), anyLong());
- inOrderVerifier.verify(mNativeWrapperMock).off();
- inOrderVerifier.verify(mNativeWrapperMock).on(eq(2L), anyLong());
- inOrderVerifier.verify(mNativeWrapperMock).off();
- }
-
- @Test
- public void vibrate_withComposedAndNativeCallbackTriggered_finishesVibration() {
- mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
- VibratorService service = createService();
- doAnswer(invocation -> {
- service.onVibrationComplete(invocation.getArgument(1));
- return null;
- }).when(mNativeWrapperMock).compose(any(), anyLong());
- Mockito.clearInvocations(mNativeWrapperMock);
-
- VibrationEffect effect = VibrationEffect.startComposition()
- .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 10)
- .compose();
- vibrate(service, effect);
-
- InOrder inOrderVerifier = inOrder(mNativeWrapperMock);
- inOrderVerifier.verify(mNativeWrapperMock).off();
- inOrderVerifier.verify(mNativeWrapperMock).compose(
- any(VibrationEffect.Composition.PrimitiveEffect[].class), gt(0L));
- inOrderVerifier.verify(mNativeWrapperMock).off();
- }
-
- @Test
- public void cancelVibrate_withDeviceVibrating_callsoff() {
- VibratorService service = createService();
- vibrate(service, VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE));
+ // VibrationThread will start this vibration async, so wait before triggering callbacks.
+ Thread.sleep(10);
assertTrue(service.isVibrating());
- Mockito.clearInvocations(mNativeWrapperMock);
- service.cancelVibrate(service);
+ // Trigger callbacks from controller.
+ mTestLooper.moveTimeForward(50);
+ mTestLooper.dispatchAll();
+
+ // VibrationThread needs some time to react to native callbacks and stop the vibrator.
+ Thread.sleep(10);
assertFalse(service.isVibrating());
- verify(mNativeWrapperMock).off();
}
@Test
- public void cancelVibrate_withDeviceNotVibrating_ignoresCall() {
+ public void cancelVibrate_withDeviceVibrating_callsOff() throws Exception {
VibratorService service = createService();
- Mockito.clearInvocations(mNativeWrapperMock);
+
+ vibrate(service, VibrationEffect.createOneShot(100, 100), ALARM_ATTRS);
+
+ // VibrationThread will start this vibration async, so wait before checking.
+ Thread.sleep(10);
+ assertTrue(service.isVibrating());
service.cancelVibrate(service);
+
+ // VibrationThread will stop this vibration async, so wait before checking.
+ Thread.sleep(10);
assertFalse(service.isVibrating());
- verify(mNativeWrapperMock, never()).off();
}
@Test
@@ -653,12 +516,11 @@
VibratorService service = createService();
service.registerVibratorStateListener(mVibratorStateListenerMock);
- vibrate(service, VibrationEffect.createOneShot(10, VibrationEffect.DEFAULT_AMPLITUDE));
- service.cancelVibrate(service);
+ vibrateAndWait(service, VibrationEffect.createOneShot(100, 100), ALARM_ATTRS);
InOrder inOrderVerifier = inOrder(mVibratorStateListenerMock);
// First notification done when listener is registered.
- inOrderVerifier.verify(mVibratorStateListenerMock).onVibrating(false);
+ inOrderVerifier.verify(mVibratorStateListenerMock).onVibrating(eq(false));
inOrderVerifier.verify(mVibratorStateListenerMock).onVibrating(eq(true));
inOrderVerifier.verify(mVibratorStateListenerMock).onVibrating(eq(false));
inOrderVerifier.verifyNoMoreInteractions();
@@ -671,18 +533,25 @@
service.registerVibratorStateListener(mVibratorStateListenerMock);
verify(mVibratorStateListenerMock).onVibrating(false);
- vibrate(service, VibrationEffect.createOneShot(5, VibrationEffect.DEFAULT_AMPLITUDE));
- verify(mVibratorStateListenerMock).onVibrating(true);
+ vibrate(service, VibrationEffect.createOneShot(100, 100), ALARM_ATTRS);
+ // VibrationThread will start this vibration async, so wait before triggering callbacks.
+ Thread.sleep(10);
service.unregisterVibratorStateListener(mVibratorStateListenerMock);
- Mockito.clearInvocations(mVibratorStateListenerMock);
+ // Trigger callbacks from controller.
+ mTestLooper.moveTimeForward(150);
+ mTestLooper.dispatchAll();
- vibrate(service, VibrationEffect.createOneShot(10, VibrationEffect.DEFAULT_AMPLITUDE));
- verifyNoMoreInteractions(mVibratorStateListenerMock);
+ InOrder inOrderVerifier = inOrder(mVibratorStateListenerMock);
+ // First notification done when listener is registered.
+ inOrderVerifier.verify(mVibratorStateListenerMock).onVibrating(eq(false));
+ inOrderVerifier.verify(mVibratorStateListenerMock).onVibrating(eq(true));
+ inOrderVerifier.verify(mVibratorStateListenerMock, atLeastOnce()).asBinder(); // unregister
+ inOrderVerifier.verifyNoMoreInteractions();
}
@Test
- public void scale_withPrebaked_userIntensitySettingAsEffectStrength() {
+ public void scale_withPrebaked_userIntensitySettingAsEffectStrength() throws Exception {
// Alarm vibration is always VIBRATION_INTENSITY_HIGH.
setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY,
Vibrator.VIBRATION_INTENSITY_MEDIUM);
@@ -690,28 +559,29 @@
Vibrator.VIBRATION_INTENSITY_LOW);
setUserSetting(Settings.System.RING_VIBRATION_INTENSITY,
Vibrator.VIBRATION_INTENSITY_OFF);
+ mVibratorProvider.setSupportedEffects(
+ VibrationEffect.EFFECT_CLICK,
+ VibrationEffect.EFFECT_TICK,
+ VibrationEffect.EFFECT_DOUBLE_CLICK,
+ VibrationEffect.EFFECT_HEAVY_CLICK);
VibratorService service = createService();
- vibrate(service, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK),
- ALARM_ATTRS);
- vibrate(service, VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK),
+ vibrateAndWait(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), ALARM_ATTRS);
+ vibrateAndWait(service, VibrationEffect.get(VibrationEffect.EFFECT_TICK),
NOTIFICATION_ATTRS);
- vibrate(service, VibrationEffect.createPredefined(VibrationEffect.EFFECT_DOUBLE_CLICK),
+ vibrateAndWait(service, VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK),
HAPTIC_FEEDBACK_ATTRS);
- vibrate(service, VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK),
- RINGTONE_ATTRS);
+ vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK), RINGTONE_ATTRS);
- verify(mNativeWrapperMock).perform(
- eq((long) VibrationEffect.EFFECT_CLICK),
- eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG), anyLong());
- verify(mNativeWrapperMock).perform(
- eq((long) VibrationEffect.EFFECT_TICK),
- eq((long) VibrationEffect.EFFECT_STRENGTH_MEDIUM), anyLong());
- verify(mNativeWrapperMock).perform(
- eq((long) VibrationEffect.EFFECT_DOUBLE_CLICK),
- eq((long) VibrationEffect.EFFECT_STRENGTH_LIGHT), anyLong());
- verify(mNativeWrapperMock, never()).perform(
- eq((long) VibrationEffect.EFFECT_HEAVY_CLICK), anyLong(), anyLong());
+ List<Integer> playedStrengths = mVibratorProvider.getEffects().stream()
+ .map(VibrationEffect.Prebaked.class::cast)
+ .map(VibrationEffect.Prebaked::getEffectStrength)
+ .collect(Collectors.toList());
+ assertEquals(Arrays.asList(
+ VibrationEffect.EFFECT_STRENGTH_STRONG,
+ VibrationEffect.EFFECT_STRENGTH_MEDIUM,
+ VibrationEffect.EFFECT_STRENGTH_LIGHT),
+ playedStrengths);
}
@Test
@@ -725,31 +595,28 @@
setUserSetting(Settings.System.RING_VIBRATION_INTENSITY,
Vibrator.VIBRATION_INTENSITY_OFF);
- mockVibratorCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+ mVibratorProvider.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
VibratorService service = createService();
- vibrate(service, VibrationEffect.createOneShot(20, 100), ALARM_ATTRS);
- vibrate(service, VibrationEffect.createOneShot(20, 100), NOTIFICATION_ATTRS);
- vibrate(service, VibrationEffect.createOneShot(20, 255), RINGTONE_ATTRS);
- vibrate(service, VibrationEffect.createWaveform(new long[] { 10 }, new int[] { 100 }, -1),
+ vibrateAndWait(service, VibrationEffect.createOneShot(20, 100), ALARM_ATTRS);
+ vibrateAndWait(service, VibrationEffect.createOneShot(20, 100), NOTIFICATION_ATTRS);
+ vibrateAndWait(service,
+ VibrationEffect.createWaveform(new long[]{10}, new int[]{100}, -1),
HAPTIC_FEEDBACK_ATTRS);
+ vibrate(service, VibrationEffect.createOneShot(20, 255), RINGTONE_ATTRS);
- // Waveform effect runs on a separate thread.
- Thread.sleep(15);
-
+ List<Integer> amplitudes = mVibratorProvider.getAmplitudes();
+ assertEquals(3, amplitudes.size());
// Alarm vibration is never scaled.
- verify(mNativeWrapperMock).setAmplitude(eq(100));
+ assertEquals(100, amplitudes.get(0).intValue());
// Notification vibrations will be scaled with SCALE_VERY_HIGH.
- verify(mNativeWrapperMock).setAmplitude(intThat(amplitude -> amplitude > 150));
+ assertTrue(amplitudes.get(1) > 150);
// Haptic feedback vibrations will be scaled with SCALE_LOW.
- verify(mNativeWrapperMock).setAmplitude(
- intThat(amplitude -> amplitude < 100 && amplitude > 50));
- // Ringtone vibration is off.
- verify(mNativeWrapperMock, never()).setAmplitude(eq(255));
+ assertTrue(amplitudes.get(2) < 100 && amplitudes.get(2) > 50);
}
@Test
- public void scale_withComposed_usesScaleLevelOnPrimitiveScaleValues() {
+ public void scale_withComposed_usesScaleLevelOnPrimitiveScaleValues() throws Exception {
when(mVibratorMock.getDefaultNotificationVibrationIntensity())
.thenReturn(Vibrator.VIBRATION_INTENSITY_LOW);
setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY,
@@ -759,82 +626,72 @@
setUserSetting(Settings.System.RING_VIBRATION_INTENSITY,
Vibrator.VIBRATION_INTENSITY_OFF);
- mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+ mVibratorProvider.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
VibratorService service = createService();
VibrationEffect effect = VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
.compose();
- ArgumentCaptor<VibrationEffect.Composition.PrimitiveEffect[]> primitivesCaptor =
- ArgumentCaptor.forClass(VibrationEffect.Composition.PrimitiveEffect[].class);
- vibrate(service, effect, ALARM_ATTRS);
- vibrate(service, effect, NOTIFICATION_ATTRS);
- vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS);
+ vibrateAndWait(service, effect, ALARM_ATTRS);
+ vibrateAndWait(service, effect, NOTIFICATION_ATTRS);
+ vibrateAndWait(service, effect, HAPTIC_FEEDBACK_ATTRS);
vibrate(service, effect, RINGTONE_ATTRS);
- // Ringtone vibration is off, so only the other 3 are propagated to native.
- verify(mNativeWrapperMock, times(3)).compose(
- primitivesCaptor.capture(), anyLong());
+ List<VibrationEffect.Composition.PrimitiveEffect> primitives =
+ mVibratorProvider.getEffects().stream()
+ .map(VibrationEffect.Composed.class::cast)
+ .map(VibrationEffect.Composed::getPrimitiveEffects)
+ .flatMap(List::stream)
+ .collect(Collectors.toList());
- List<VibrationEffect.Composition.PrimitiveEffect[]> values =
- primitivesCaptor.getAllValues();
+ // Ringtone vibration is off, so only the other 3 are propagated to native.
+ assertEquals(6, primitives.size());
// Alarm vibration is never scaled.
- assertEquals(1f, values.get(0)[0].scale, /* delta= */ 1e-2);
- assertEquals(0.5f, values.get(0)[1].scale, /* delta= */ 1e-2);
+ assertEquals(1f, primitives.get(0).scale, /* delta= */ 1e-2);
+ assertEquals(0.5f, primitives.get(1).scale, /* delta= */ 1e-2);
// Notification vibrations will be scaled with SCALE_VERY_HIGH.
- assertEquals(1f, values.get(1)[0].scale, /* delta= */ 1e-2);
- assertTrue(0.7 < values.get(1)[1].scale);
+ assertEquals(1f, primitives.get(2).scale, /* delta= */ 1e-2);
+ assertTrue(0.7 < primitives.get(3).scale);
// Haptic feedback vibrations will be scaled with SCALE_LOW.
- assertTrue(0.5 < values.get(2)[0].scale);
- assertTrue(0.5 > values.get(2)[1].scale);
- }
-
- private void vibrate(VibratorService service, VibrationEffect effect) {
- vibrate(service, effect, ALARM_ATTRS);
+ assertTrue(0.5 < primitives.get(4).scale);
+ assertTrue(0.5 > primitives.get(5).scale);
}
private void vibrate(VibratorService service, VibrationEffect effect,
- VibrationAttributes attributes) {
- service.vibrate(UID, PACKAGE_NAME, effect, attributes, "some reason", service);
+ VibrationAttributes attrs) {
+ service.vibrate(UID, PACKAGE_NAME, effect, attrs, "some reason", service);
}
- private int vibrateAndMeasure(
- VibratorService service, VibrationEffect effect, long timeoutSecs) throws Exception {
- AtomicLong startTime = new AtomicLong(0);
- AtomicLong endTime = new AtomicLong(0);
+ private void vibrateAndWait(VibratorService service, VibrationEffect effect,
+ VibrationAttributes attrs) throws Exception {
CountDownLatch startedCount = new CountDownLatch(1);
CountDownLatch finishedCount = new CountDownLatch(1);
service.registerVibratorStateListener(new IVibratorStateListener() {
@Override
- public void onVibrating(boolean vibrating) throws RemoteException {
+ public void onVibrating(boolean vibrating) {
if (vibrating) {
- startTime.set(SystemClock.uptimeMillis());
startedCount.countDown();
} else if (startedCount.getCount() == 0) {
- endTime.set(SystemClock.uptimeMillis());
finishedCount.countDown();
}
}
@Override
public IBinder asBinder() {
- return mVibratorStateListenerBinderMock;
+ return mock(IBinder.class);
}
});
- vibrate(service, effect);
-
- assertTrue(finishedCount.await(timeoutSecs, TimeUnit.SECONDS));
- return (int) (endTime.get() - startTime.get());
- }
-
- private void mockVibratorCapabilities(int capabilities) {
- when(mNativeWrapperMock.getCapabilities()).thenReturn((long) capabilities);
+ mTestLooper.startAutoDispatch();
+ service.vibrate(UID, PACKAGE_NAME, effect, attrs, "some reason", service);
+ assertTrue(startedCount.await(1, TimeUnit.SECONDS));
+ assertTrue(finishedCount.await(1, TimeUnit.SECONDS));
+ mTestLooper.stopAutoDispatchAndIgnoreExceptions();
}
private InputDevice createInputDeviceWithVibrator(int id) {
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
new file mode 100644
index 0000000..f562c16
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.VibrationEffect;
+
+import com.android.server.vibrator.VibratorController.OnVibrationCompleteListener;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Provides {@link VibratorController} with controlled vibrator hardware capabilities and
+ * interactions.
+ */
+public final class FakeVibratorControllerProvider {
+
+ private static final int EFFECT_DURATION = 20;
+
+ private final Map<Long, VibrationEffect.Prebaked> mEnabledAlwaysOnEffects = new HashMap<>();
+ private final List<VibrationEffect> mEffects = new ArrayList<>();
+ private final List<Integer> mAmplitudes = new ArrayList<>();
+ private final Handler mHandler;
+ private final FakeNativeWrapper mNativeWrapper;
+
+ private boolean mIsAvailable = true;
+ private long mLatency;
+
+ private int mCapabilities;
+ private int[] mSupportedEffects;
+ private int[] mSupportedPrimitives;
+
+ private final class FakeNativeWrapper extends VibratorController.NativeWrapper {
+ public int vibratorId;
+ public OnVibrationCompleteListener listener;
+ public boolean isInitialized;
+
+ public void init(int vibratorId, OnVibrationCompleteListener listener) {
+ isInitialized = true;
+ this.vibratorId = vibratorId;
+ this.listener = listener;
+ }
+
+ public boolean isAvailable() {
+ return mIsAvailable;
+ }
+
+ public void on(long milliseconds, long vibrationId) {
+ VibrationEffect effect = VibrationEffect.createOneShot(
+ milliseconds, VibrationEffect.DEFAULT_AMPLITUDE);
+ mEffects.add(effect);
+ applyLatency();
+ scheduleListener(milliseconds, vibrationId);
+ }
+
+ public void off() {
+ }
+
+ public void setAmplitude(int amplitude) {
+ mAmplitudes.add(amplitude);
+ applyLatency();
+ }
+
+ public int[] getSupportedEffects() {
+ return mSupportedEffects;
+ }
+
+ public int[] getSupportedPrimitives() {
+ return mSupportedPrimitives;
+ }
+
+ public long perform(long effect, long strength, long vibrationId) {
+ if (mSupportedEffects == null
+ || Arrays.binarySearch(mSupportedEffects, (int) effect) < 0) {
+ return 0;
+ }
+ mEffects.add(new VibrationEffect.Prebaked((int) effect, false, (int) strength));
+ applyLatency();
+ scheduleListener(EFFECT_DURATION, vibrationId);
+ return EFFECT_DURATION;
+ }
+
+ public void compose(VibrationEffect.Composition.PrimitiveEffect[] effect,
+ long vibrationId) {
+ VibrationEffect.Composed composed = new VibrationEffect.Composed(Arrays.asList(effect));
+ mEffects.add(composed);
+ applyLatency();
+ long duration = EFFECT_DURATION * effect.length;
+ for (VibrationEffect.Composition.PrimitiveEffect e : effect) {
+ duration += e.delay;
+ }
+ scheduleListener(duration, vibrationId);
+ }
+
+ public void setExternalControl(boolean enabled) {
+ }
+
+ public long getCapabilities() {
+ return mCapabilities;
+ }
+
+ public void alwaysOnEnable(long id, long effect, long strength) {
+ VibrationEffect.Prebaked prebaked = new VibrationEffect.Prebaked((int) effect, false,
+ (int) strength);
+ mEnabledAlwaysOnEffects.put(id, prebaked);
+ }
+
+ public void alwaysOnDisable(long id) {
+ mEnabledAlwaysOnEffects.remove(id);
+ }
+
+ private void applyLatency() {
+ try {
+ if (mLatency > 0) {
+ Thread.sleep(mLatency);
+ }
+ } catch (InterruptedException e) {
+ }
+ }
+
+ private void scheduleListener(long vibrationDuration, long vibrationId) {
+ mHandler.postDelayed(() -> listener.onComplete(vibratorId, vibrationId),
+ vibrationDuration);
+ }
+ }
+
+ public FakeVibratorControllerProvider(Looper looper) {
+ mHandler = new Handler(looper);
+ mNativeWrapper = new FakeNativeWrapper();
+ }
+
+ public VibratorController newVibratorController(
+ int vibratorId, OnVibrationCompleteListener listener) {
+ return new VibratorController(vibratorId, listener, mNativeWrapper);
+ }
+
+ /** Return {@code true} if this controller was initialized. */
+ public boolean isInitialized() {
+ return mNativeWrapper.isInitialized;
+ }
+
+ /**
+ * Disable fake vibrator hardware, mocking a state where the underlying service is unavailable.
+ */
+ public void disableVibrators() {
+ mIsAvailable = false;
+ }
+
+ /**
+ * Sets the latency this controller should fake for turning the vibrator hardware on or setting
+ * it's vibration amplitude.
+ */
+ public void setLatency(long millis) {
+ mLatency = millis;
+ }
+
+ /** Set the capabilities of the fake vibrator hardware. */
+ public void setCapabilities(int... capabilities) {
+ mCapabilities = Arrays.stream(capabilities).reduce(0, (a, b) -> a | b);
+ }
+
+ /** Set the effects supported by the fake vibrator hardware. */
+ public void setSupportedEffects(int... effects) {
+ if (effects != null) {
+ effects = Arrays.copyOf(effects, effects.length);
+ Arrays.sort(effects);
+ }
+ mSupportedEffects = effects;
+ }
+
+ /** Set the primitives supported by the fake vibrator hardware. */
+ public void setSupportedPrimitives(int... primitives) {
+ if (primitives != null) {
+ primitives = Arrays.copyOf(primitives, primitives.length);
+ Arrays.sort(primitives);
+ }
+ mSupportedPrimitives = primitives;
+ }
+
+ /**
+ * Return the amplitudes set by this controller, including zeroes for each time the vibrator was
+ * turned off.
+ */
+ public List<Integer> getAmplitudes() {
+ return new ArrayList<>(mAmplitudes);
+ }
+
+ /** Return list of {@link VibrationEffect} played by this controller, in order. */
+ public List<VibrationEffect> getEffects() {
+ return new ArrayList<>(mEffects);
+ }
+
+ /**
+ * Return the {@link VibrationEffect.Prebaked} effect enabled with given id, or {@code null} if
+ * missing or disabled.
+ */
+ @Nullable
+ public VibrationEffect.Prebaked getAlwaysOnEffect(int id) {
+ return mEnabledAlwaysOnEffects.get((long) id);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java b/services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java
index ac93ff6..28d313b 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java
@@ -34,6 +34,7 @@
import android.hardware.input.IInputDevicesChangedListener;
import android.hardware.input.IInputManager;
import android.hardware.input.InputManager;
+import android.os.CombinedVibrationEffect;
import android.os.Handler;
import android.os.Process;
import android.os.VibrationAttributes;
@@ -66,6 +67,9 @@
private static final String REASON = "some reason";
private static final VibrationAttributes VIBRATION_ATTRIBUTES =
new VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_ALARM).build();
+ private static final VibrationEffect EFFECT = VibrationEffect.createOneShot(100, 255);
+ private static final CombinedVibrationEffect SYNCED_EFFECT =
+ CombinedVibrationEffect.createSynced(EFFECT);
@Rule public MockitoRule rule = MockitoJUnit.rule();
@@ -227,10 +231,9 @@
@Test
public void vibrateIfAvailable_withNoInputDevice_returnsFalse() {
- VibrationEffect effect = VibrationEffect.createOneShot(100, 255);
assertFalse(mInputDeviceDelegate.isAvailable());
assertFalse(mInputDeviceDelegate.vibrateIfAvailable(
- UID, PACKAGE_NAME, effect, REASON, VIBRATION_ATTRIBUTES));
+ UID, PACKAGE_NAME, SYNCED_EFFECT, REASON, VIBRATION_ATTRIBUTES));
}
@Test
@@ -241,11 +244,10 @@
when(mIInputManagerMock.getInputDevice(eq(2))).thenReturn(createInputDeviceWithVibrator(2));
mInputDeviceDelegate.updateInputDeviceVibrators(/* vibrateInputDevices= */ true);
- VibrationEffect effect = VibrationEffect.createOneShot(100, 255);
assertTrue(mInputDeviceDelegate.vibrateIfAvailable(
- UID, PACKAGE_NAME, effect, REASON, VIBRATION_ATTRIBUTES));
- verify(mIInputManagerMock).vibrate(eq(1), same(effect), any());
- verify(mIInputManagerMock).vibrate(eq(2), same(effect), any());
+ UID, PACKAGE_NAME, SYNCED_EFFECT, REASON, VIBRATION_ATTRIBUTES));
+ verify(mIInputManagerMock).vibrate(eq(1), same(EFFECT), any());
+ verify(mIInputManagerMock).vibrate(eq(2), same(EFFECT), any());
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationScalerTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationScalerTest.java
index 1a4ac07..82a6937 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationScalerTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationScalerTest.java
@@ -26,6 +26,7 @@
import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
+import android.os.CombinedVibrationEffect;
import android.os.Handler;
import android.os.IExternalVibratorService;
import android.os.PowerManagerInternal;
@@ -131,6 +132,45 @@
}
@Test
+ public void scale_withCombined_resolvesAndScalesRecursively() {
+ setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY,
+ Vibrator.VIBRATION_INTENSITY_HIGH);
+ VibrationEffect prebaked = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK);
+ VibrationEffect oneShot = VibrationEffect.createOneShot(10, 10);
+
+ CombinedVibrationEffect.Mono monoScaled = mVibrationScaler.scale(
+ CombinedVibrationEffect.createSynced(prebaked),
+ VibrationAttributes.USAGE_NOTIFICATION);
+ VibrationEffect.Prebaked prebakedScaled = (VibrationEffect.Prebaked) monoScaled.getEffect();
+ assertEquals(prebakedScaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_STRONG);
+
+ CombinedVibrationEffect.Stereo stereoScaled = mVibrationScaler.scale(
+ CombinedVibrationEffect.startSynced()
+ .addVibrator(1, prebaked)
+ .addVibrator(2, oneShot)
+ .combine(),
+ VibrationAttributes.USAGE_NOTIFICATION);
+ prebakedScaled = (VibrationEffect.Prebaked) stereoScaled.getEffects().get(1);
+ assertEquals(prebakedScaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_STRONG);
+ VibrationEffect.OneShot oneshotScaled =
+ (VibrationEffect.OneShot) stereoScaled.getEffects().get(2);
+ assertTrue(oneshotScaled.getAmplitude() > 0);
+
+ CombinedVibrationEffect.Sequential sequentialScaled = mVibrationScaler.scale(
+ CombinedVibrationEffect.startSequential()
+ .addNext(CombinedVibrationEffect.createSynced(prebaked))
+ .addNext(CombinedVibrationEffect.createSynced(oneShot))
+ .combine(),
+ VibrationAttributes.USAGE_NOTIFICATION);
+ monoScaled = (CombinedVibrationEffect.Mono) sequentialScaled.getEffects().get(0);
+ prebakedScaled = (VibrationEffect.Prebaked) monoScaled.getEffect();
+ assertEquals(prebakedScaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_STRONG);
+ monoScaled = (CombinedVibrationEffect.Mono) sequentialScaled.getEffects().get(1);
+ oneshotScaled = (VibrationEffect.OneShot) monoScaled.getEffect();
+ assertTrue(oneshotScaled.getAmplitude() > 0);
+ }
+
+ @Test
public void scale_withPrebaked_setsEffectStrengthBasedOnSettings() {
setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY,
Vibrator.VIBRATION_INTENSITY_HIGH);
@@ -158,6 +198,28 @@
}
@Test
+ public void scale_withPrebakedAndFallback_resolvesAndScalesRecursively() {
+ setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY,
+ Vibrator.VIBRATION_INTENSITY_HIGH);
+ VibrationEffect.OneShot fallback2 = (VibrationEffect.OneShot) VibrationEffect.createOneShot(
+ 10, VibrationEffect.DEFAULT_AMPLITUDE);
+ VibrationEffect.Prebaked fallback1 = new VibrationEffect.Prebaked(
+ VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_STRENGTH_MEDIUM, fallback2);
+ VibrationEffect.Prebaked effect = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+ VibrationEffect.EFFECT_STRENGTH_MEDIUM, fallback1);
+
+ VibrationEffect.Prebaked scaled = mVibrationScaler.scale(
+ effect, VibrationAttributes.USAGE_NOTIFICATION);
+ VibrationEffect.Prebaked scaledFallback1 =
+ (VibrationEffect.Prebaked) scaled.getFallbackEffect();
+ VibrationEffect.OneShot scaledFallback2 =
+ (VibrationEffect.OneShot) scaledFallback1.getFallbackEffect();
+ assertEquals(scaled.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_STRONG);
+ assertEquals(scaledFallback1.getEffectStrength(), VibrationEffect.EFFECT_STRENGTH_STRONG);
+ assertTrue(scaledFallback2.getAmplitude() > 0);
+ }
+
+ @Test
public void scale_withOneShotAndWaveform_resolvesAmplitude() {
// No scale, default amplitude still resolved
when(mVibratorMock.getDefaultRingVibrationIntensity())
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
new file mode 100644
index 0000000..bee7392
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -0,0 +1,658 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.vibrator.IVibrator;
+import android.os.CombinedVibrationEffect;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.SystemClock;
+import android.os.VibrationAttributes;
+import android.os.VibrationEffect;
+import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
+import android.util.SparseArray;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.app.IBatteryStats;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Tests for {@link VibrationThread}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:VibrationThreadTest
+ */
+@Presubmit
+public class VibrationThreadTest {
+
+ private static final int TEST_TIMEOUT_MILLIS = 1_000;
+ private static final int UID = Process.ROOT_UID;
+ private static final int VIBRATOR_ID = 1;
+ private static final String PACKAGE_NAME = "package";
+ private static final VibrationAttributes ATTRS = new VibrationAttributes.Builder().build();
+
+ @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ @Mock private VibrationThread.VibrationCallbacks mThreadCallbacks;
+ @Mock private VibratorController.OnVibrationCompleteListener mControllerCallbacks;
+ @Mock private IBinder mVibrationToken;
+ @Mock private IBatteryStats mIBatteryStatsMock;
+
+ private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>();
+ private PowerManager.WakeLock mWakeLock;
+ private TestLooper mTestLooper;
+
+ @Before
+ public void setUp() throws Exception {
+ mTestLooper = new TestLooper();
+ mWakeLock = InstrumentationRegistry.getContext().getSystemService(
+ PowerManager.class).newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*vibrator*");
+
+ mockVibrators(VIBRATOR_ID);
+ }
+
+ @Test
+ public void vibrate_noVibrator_ignoresVibration() {
+ mVibratorProviders.clear();
+ long vibrationId = 1;
+ CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced(
+ VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+ waitForCompletion(thread);
+
+ verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibrationId));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.IGNORED));
+ }
+
+ @Test
+ public void vibrate_missingVibrators_ignoresVibration() {
+ long vibrationId = 1;
+ CombinedVibrationEffect effect = CombinedVibrationEffect.startSequential()
+ .addNext(2, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
+ .addNext(3, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
+ .combine();
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+ waitForCompletion(thread);
+
+ verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibrationId));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.IGNORED));
+ }
+
+ @Test
+ public void vibrate_singleVibratorOneShot_runsVibrationAndSetsAmplitude() throws Exception {
+ mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+ long vibrationId = 1;
+ VibrationEffect effect = VibrationEffect.createOneShot(10, 100);
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+ waitForCompletion(thread);
+
+ verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
+ verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+ assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+ assertEquals(Arrays.asList(expectedOneShot(10)),
+ mVibratorProviders.get(VIBRATOR_ID).getEffects());
+ assertEquals(Arrays.asList(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
+ }
+
+ @Test
+ public void vibrate_oneShotWithoutAmplitudeControl_runsVibrationWithDefaultAmplitude()
+ throws Exception {
+ long vibrationId = 1;
+ VibrationEffect effect = VibrationEffect.createOneShot(10, 100);
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+ waitForCompletion(thread);
+
+ verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
+ verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+ assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+ assertEquals(Arrays.asList(expectedOneShot(10)),
+ mVibratorProviders.get(VIBRATOR_ID).getEffects());
+ assertTrue(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes().isEmpty());
+ }
+
+ @Test
+ public void vibrate_singleVibratorWaveform_runsVibrationAndChangesAmplitudes()
+ throws Exception {
+ mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+ long vibrationId = 1;
+ VibrationEffect effect = VibrationEffect.createWaveform(
+ new long[]{5, 5, 5}, new int[]{1, 2, 3}, -1);
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+ waitForCompletion(thread);
+
+ verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(15L));
+ verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+ assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+ assertEquals(Arrays.asList(expectedOneShot(15)),
+ mVibratorProviders.get(VIBRATOR_ID).getEffects());
+ assertEquals(Arrays.asList(1, 2, 3), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
+ }
+
+ @Test
+ public void vibrate_singleVibratorRepeatingWaveform_runsVibrationUntilThreadCancelled()
+ throws Exception {
+ mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+ long vibrationId = 1;
+ int[] amplitudes = new int[]{1, 2, 3};
+ VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5, 5, 5}, amplitudes, 0);
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+
+ Thread.sleep(35);
+ // Vibration still running after 2 cycles.
+ assertTrue(thread.isAlive());
+ assertTrue(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+ thread.cancel();
+ waitForCompletion(thread);
+
+ verify(mIBatteryStatsMock, never()).noteVibratorOn(eq(UID), anyLong());
+ verify(mIBatteryStatsMock, never()).noteVibratorOff(eq(UID));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED));
+ assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+ List<Integer> playedAmplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes();
+ assertFalse(mVibratorProviders.get(VIBRATOR_ID).getEffects().isEmpty());
+ assertFalse(playedAmplitudes.isEmpty());
+
+ for (int i = 0; i < playedAmplitudes.size(); i++) {
+ assertEquals(amplitudes[i % amplitudes.length], playedAmplitudes.get(i).intValue());
+ }
+ }
+
+ @Test
+ public void vibrate_singleVibratorPrebaked_runsVibration() throws Exception {
+ mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_THUD);
+
+ long vibrationId = 1;
+ VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_THUD);
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+ waitForCompletion(thread);
+
+ verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
+ verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+ assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+ assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_THUD)),
+ mVibratorProviders.get(VIBRATOR_ID).getEffects());
+ }
+
+ @Test
+ public void vibrate_singleVibratorPrebakedAndUnsupportedEffectWithFallback_runsFallback()
+ throws Exception {
+ mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+ long vibrationId = 1;
+ VibrationEffect fallback = VibrationEffect.createOneShot(10, 100);
+ VibrationEffect.Prebaked effect = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK,
+ VibrationEffect.EFFECT_STRENGTH_STRONG, fallback);
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+ waitForCompletion(thread);
+
+ verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
+ verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+ assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+ assertEquals(Arrays.asList(expectedOneShot(10)),
+ mVibratorProviders.get(VIBRATOR_ID).getEffects());
+ assertEquals(Arrays.asList(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
+ }
+
+ @Test
+ public void vibrate_singleVibratorPrebakedAndUnsupportedEffect_ignoresVibration()
+ throws Exception {
+ long vibrationId = 1;
+ VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+ waitForCompletion(thread);
+
+ verify(mIBatteryStatsMock, never()).noteVibratorOn(eq(UID), anyLong());
+ verify(mIBatteryStatsMock, never()).noteVibratorOff(eq(UID));
+ verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId),
+ eq(Vibration.Status.IGNORED_UNSUPPORTED));
+ assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffects().isEmpty());
+ }
+
+ @Test
+ public void vibrate_singleVibratorComposed_runsVibration() throws Exception {
+ mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+
+ long vibrationId = 1;
+ VibrationEffect effect = VibrationEffect.startComposition()
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
+ .compose();
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+ waitForCompletion(thread);
+
+ verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(40L));
+ verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+ assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+ assertEquals(Arrays.asList(effect), mVibratorProviders.get(VIBRATOR_ID).getEffects());
+ }
+
+ @Test
+ public void vibrate_singleVibratorComposedAndNoCapability_ignoresVibration() throws Exception {
+ long vibrationId = 1;
+ VibrationEffect effect = VibrationEffect.startComposition()
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
+ .compose();
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+ waitForCompletion(thread);
+
+ verify(mIBatteryStatsMock, never()).noteVibratorOn(eq(UID), anyLong());
+ verify(mIBatteryStatsMock, never()).noteVibratorOff(eq(UID));
+ verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId),
+ eq(Vibration.Status.IGNORED_UNSUPPORTED));
+ assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffects().isEmpty());
+ }
+
+ @Test
+ public void vibrate_singleVibratorCancelled_vibratorStopped() throws Exception {
+ long vibrationId = 1;
+ VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5}, new int[]{100}, 0);
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+
+ Thread.sleep(15);
+ // Vibration still running after 2 cycles.
+ assertTrue(thread.isAlive());
+ assertTrue(thread.getVibrators().get(1).isVibrating());
+
+ thread.binderDied();
+ waitForCompletion(thread);
+ assertFalse(thread.getVibrators().get(1).isVibrating());
+
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED));
+ }
+
+ @Test
+ public void vibrate_multipleExistingAndMissingVibrators_vibratesOnlyExistingOnes()
+ throws Exception {
+ mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_TICK);
+
+ long vibrationId = 1;
+ CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
+ .addVibrator(VIBRATOR_ID, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
+ .addVibrator(2, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
+ .combine();
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+ waitForCompletion(thread);
+
+ verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
+ verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
+ verify(mControllerCallbacks, never()).onComplete(eq(2), eq(vibrationId));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+ assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
+
+ assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_TICK)),
+ mVibratorProviders.get(VIBRATOR_ID).getEffects());
+ }
+
+ @Test
+ public void vibrate_multipleMono_runsSameEffectInAllVibrators() throws Exception {
+ mockVibrators(1, 2, 3);
+ mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+ mVibratorProviders.get(2).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+ mVibratorProviders.get(3).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+
+ long vibrationId = 1;
+ CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced(
+ VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+ waitForCompletion(thread);
+
+ verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
+ verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+ verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
+ verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
+ verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+ assertFalse(thread.getVibrators().get(1).isVibrating());
+ assertFalse(thread.getVibrators().get(2).isVibrating());
+ assertFalse(thread.getVibrators().get(3).isVibrating());
+
+ VibrationEffect expected = expectedPrebaked(VibrationEffect.EFFECT_CLICK);
+ assertEquals(Arrays.asList(expected), mVibratorProviders.get(1).getEffects());
+ assertEquals(Arrays.asList(expected), mVibratorProviders.get(2).getEffects());
+ assertEquals(Arrays.asList(expected), mVibratorProviders.get(3).getEffects());
+ }
+
+ @Test
+ public void vibrate_multipleStereo_runsVibrationOnRightVibrators() throws Exception {
+ mockVibrators(1, 2, 3, 4);
+ mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+ mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+ mVibratorProviders.get(3).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+ mVibratorProviders.get(4).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+
+ long vibrationId = 1;
+ VibrationEffect composed = VibrationEffect.startComposition()
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+ .compose();
+ CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
+ .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
+ .addVibrator(2, VibrationEffect.createOneShot(10, 100))
+ .addVibrator(3, VibrationEffect.createWaveform(
+ new long[]{10, 10}, new int[]{1, 2}, -1))
+ .addVibrator(4, composed)
+ .combine();
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+ waitForCompletion(thread);
+
+ verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
+ verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+ verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
+ verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
+ verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
+ verify(mControllerCallbacks).onComplete(eq(4), eq(vibrationId));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+ assertFalse(thread.getVibrators().get(1).isVibrating());
+ assertFalse(thread.getVibrators().get(2).isVibrating());
+ assertFalse(thread.getVibrators().get(3).isVibrating());
+ assertFalse(thread.getVibrators().get(4).isVibrating());
+
+ assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
+ mVibratorProviders.get(1).getEffects());
+ assertEquals(Arrays.asList(expectedOneShot(10)), mVibratorProviders.get(2).getEffects());
+ assertEquals(Arrays.asList(100), mVibratorProviders.get(2).getAmplitudes());
+ assertEquals(Arrays.asList(expectedOneShot(20)), mVibratorProviders.get(3).getEffects());
+ assertEquals(Arrays.asList(1, 2), mVibratorProviders.get(3).getAmplitudes());
+ assertEquals(Arrays.asList(composed), mVibratorProviders.get(4).getEffects());
+ }
+
+ @Test
+ public void vibrate_multipleSequential_runsVibrationInOrderWithDelays()
+ throws Exception {
+ mockVibrators(1, 2, 3);
+ mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+ mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+ mVibratorProviders.get(3).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+
+ long vibrationId = 1;
+ VibrationEffect composed = VibrationEffect.startComposition()
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+ .compose();
+ CombinedVibrationEffect effect = CombinedVibrationEffect.startSequential()
+ .addNext(3, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), /* delay= */ 50)
+ .addNext(1, VibrationEffect.createOneShot(10, 100), /* delay= */ 50)
+ .addNext(2, composed, /* delay= */ 50)
+ .combine();
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+
+ waitForCompletion(thread);
+ InOrder controllerVerifier = inOrder(mControllerCallbacks);
+ controllerVerifier.verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
+ controllerVerifier.verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
+ controllerVerifier.verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
+
+ InOrder batterVerifier = inOrder(mIBatteryStatsMock);
+ batterVerifier.verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
+ batterVerifier.verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+ batterVerifier.verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
+ batterVerifier.verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+ batterVerifier.verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
+ batterVerifier.verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+ assertFalse(thread.getVibrators().get(1).isVibrating());
+ assertFalse(thread.getVibrators().get(2).isVibrating());
+ assertFalse(thread.getVibrators().get(3).isVibrating());
+
+ assertEquals(Arrays.asList(expectedOneShot(10)), mVibratorProviders.get(1).getEffects());
+ assertEquals(Arrays.asList(100), mVibratorProviders.get(1).getAmplitudes());
+ assertEquals(Arrays.asList(composed), mVibratorProviders.get(2).getEffects());
+ assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
+ mVibratorProviders.get(3).getEffects());
+ }
+
+ @Test
+ public void vibrate_multipleWaveforms_playsWaveformsInParallel() throws Exception {
+ mockVibrators(1, 2, 3);
+ mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+ mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+ mVibratorProviders.get(3).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+ long vibrationId = 1;
+ CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
+ .addVibrator(1, VibrationEffect.createWaveform(
+ new long[]{5, 10, 10}, new int[]{1, 2, 3}, -1))
+ .addVibrator(2, VibrationEffect.createWaveform(
+ new long[]{20, 60}, new int[]{4, 5}, -1))
+ .addVibrator(3, VibrationEffect.createWaveform(
+ new long[]{60}, new int[]{6}, -1))
+ .combine();
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+
+ Thread.sleep(40);
+ // First waveform has finished.
+ verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
+ assertEquals(Arrays.asList(1, 2, 3), mVibratorProviders.get(1).getAmplitudes());
+ // Second waveform is halfway through.
+ assertEquals(Arrays.asList(4, 5), mVibratorProviders.get(2).getAmplitudes());
+ // Third waveform is almost ending.
+ assertEquals(Arrays.asList(6), mVibratorProviders.get(3).getAmplitudes());
+
+ waitForCompletion(thread);
+
+ verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(80L));
+ verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+ verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
+ verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
+ assertFalse(thread.getVibrators().get(1).isVibrating());
+ assertFalse(thread.getVibrators().get(2).isVibrating());
+ assertFalse(thread.getVibrators().get(3).isVibrating());
+
+ assertEquals(Arrays.asList(expectedOneShot(25)), mVibratorProviders.get(1).getEffects());
+ assertEquals(Arrays.asList(expectedOneShot(80)), mVibratorProviders.get(2).getEffects());
+ assertEquals(Arrays.asList(expectedOneShot(60)), mVibratorProviders.get(3).getEffects());
+ assertEquals(Arrays.asList(1, 2, 3), mVibratorProviders.get(1).getAmplitudes());
+ assertEquals(Arrays.asList(4, 5), mVibratorProviders.get(2).getAmplitudes());
+ assertEquals(Arrays.asList(6), mVibratorProviders.get(3).getAmplitudes());
+ }
+
+ @Test
+ public void vibrate_withWaveform_totalVibrationTimeRespected() {
+ int totalDuration = 10_000; // 10s
+ int stepDuration = 25; // 25ms
+
+ // 25% of the first waveform step will be spent on the native on() call.
+ // 25% of each waveform step will be spent on the native setAmplitude() call..
+ mVibratorProviders.get(VIBRATOR_ID).setLatency(stepDuration / 4);
+ mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+ int stepCount = totalDuration / stepDuration;
+ long[] timings = new long[stepCount];
+ int[] amplitudes = new int[stepCount];
+ Arrays.fill(timings, stepDuration);
+ Arrays.fill(amplitudes, VibrationEffect.DEFAULT_AMPLITUDE);
+ VibrationEffect effect = VibrationEffect.createWaveform(timings, amplitudes, -1);
+
+ long vibrationId = 1;
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+ long startTime = SystemClock.elapsedRealtime();
+
+ waitForCompletion(thread, totalDuration + TEST_TIMEOUT_MILLIS);
+ long delay = Math.abs(SystemClock.elapsedRealtime() - startTime - totalDuration);
+
+ // Allow some delay for thread scheduling and callback triggering.
+ int maxDelay = (int) (0.05 * totalDuration); // < 5% of total duration
+ assertTrue("Waveform with perceived delay of " + delay + "ms,"
+ + " expected less than " + maxDelay + "ms",
+ delay < maxDelay);
+ }
+
+ @Test
+ public void vibrate_multipleCancelled_allVibratorsStopped() throws Exception {
+ mockVibrators(1, 2, 3);
+ mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+ mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+ mVibratorProviders.get(3).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+ long vibrationId = 1;
+ CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced()
+ .addVibrator(1, VibrationEffect.createWaveform(
+ new long[]{5, 10}, new int[]{1, 2}, 0))
+ .addVibrator(2, VibrationEffect.createWaveform(
+ new long[]{20, 30}, new int[]{3, 4}, 0))
+ .addVibrator(3, VibrationEffect.createWaveform(
+ new long[]{10, 40}, new int[]{5, 6}, 0))
+ .combine();
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+
+ Thread.sleep(15);
+ assertTrue(thread.isAlive());
+ assertTrue(thread.getVibrators().get(1).isVibrating());
+ assertTrue(thread.getVibrators().get(2).isVibrating());
+ assertTrue(thread.getVibrators().get(3).isVibrating());
+
+ thread.cancel();
+ waitForCompletion(thread);
+ assertFalse(thread.getVibrators().get(1).isVibrating());
+ assertFalse(thread.getVibrators().get(2).isVibrating());
+ assertFalse(thread.getVibrators().get(3).isVibrating());
+
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED));
+ }
+
+ @Test
+ public void vibrate_binderDied_cancelsVibration() throws Exception {
+ long vibrationId = 1;
+ VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5}, new int[]{100}, 0);
+ VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
+
+ Thread.sleep(15);
+ // Vibration still running after 2 cycles.
+ assertTrue(thread.isAlive());
+ assertTrue(thread.getVibrators().get(1).isVibrating());
+
+ thread.binderDied();
+ waitForCompletion(thread);
+
+ verify(mVibrationToken).linkToDeath(same(thread), eq(0));
+ verify(mVibrationToken).unlinkToDeath(same(thread), eq(0));
+ verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED));
+ assertFalse(mVibratorProviders.get(VIBRATOR_ID).getEffects().isEmpty());
+ assertFalse(thread.getVibrators().get(1).isVibrating());
+ }
+
+ private void mockVibrators(int... vibratorIds) {
+ for (int vibratorId : vibratorIds) {
+ mVibratorProviders.put(vibratorId,
+ new FakeVibratorControllerProvider(mTestLooper.getLooper()));
+ }
+ }
+
+ private VibrationThread startThreadAndDispatcher(long vibrationId, VibrationEffect effect) {
+ return startThreadAndDispatcher(vibrationId, CombinedVibrationEffect.createSynced(effect));
+ }
+
+ private VibrationThread startThreadAndDispatcher(long vibrationId,
+ CombinedVibrationEffect effect) {
+ VibrationThread thread = new VibrationThread(createVibration(vibrationId, effect),
+ createVibratorControllers(), mWakeLock, mIBatteryStatsMock, mThreadCallbacks);
+ doAnswer(answer -> {
+ thread.vibratorComplete(answer.getArgument(0));
+ return null;
+ }).when(mControllerCallbacks).onComplete(anyInt(), eq(vibrationId));
+ mTestLooper.startAutoDispatch();
+ thread.start();
+ return thread;
+ }
+
+ private void waitForCompletion(VibrationThread thread) {
+ waitForCompletion(thread, TEST_TIMEOUT_MILLIS);
+ }
+
+ private void waitForCompletion(VibrationThread thread, long timeout) {
+ try {
+ thread.join(timeout);
+ } catch (InterruptedException e) {
+ }
+ assertFalse(thread.isAlive());
+ mTestLooper.dispatchAll();
+ }
+
+ private Vibration createVibration(long id, CombinedVibrationEffect effect) {
+ return new Vibration(mVibrationToken, (int) id, effect, ATTRS, UID, PACKAGE_NAME, "reason");
+ }
+
+ private SparseArray<VibratorController> createVibratorControllers() {
+ SparseArray<VibratorController> array = new SparseArray<>();
+ for (Map.Entry<Integer, FakeVibratorControllerProvider> e : mVibratorProviders.entrySet()) {
+ int id = e.getKey();
+ array.put(id, e.getValue().newVibratorController(id, mControllerCallbacks));
+ }
+ return array;
+ }
+
+ private VibrationEffect expectedOneShot(long millis) {
+ return VibrationEffect.createOneShot(millis, VibrationEffect.DEFAULT_AMPLITUDE);
+ }
+
+ private VibrationEffect expectedPrebaked(int effectId) {
+ return new VibrationEffect.Prebaked(effectId, false,
+ VibrationEffect.EFFECT_STRENGTH_MEDIUM);
+ }
+}