Merge "Create single thread to play any Vibration"
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 08b73278..e885fd3 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1366,11 +1366,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 c0b2ada..df3beb2 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 3e0d040..23c70ee 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -1856,6 +1856,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() };
@@ -1882,8 +1889,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);
+ }
+}