diff options
17 files changed, 2352 insertions, 876 deletions
diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 91212beeb06c..f873ce7e25ff 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1329,11 +1329,10 @@ package android.os { 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 7ec7fffd3588..869a72717f9f 100644 --- a/core/java/android/os/CombinedVibrationEffect.java +++ b/core/java/android/os/CombinedVibrationEffect.java @@ -87,8 +87,14 @@ public abstract class CombinedVibrationEffect implements Parcelable { } /** @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 @@ public abstract class CombinedVibrationEffect implements Parcelable { return mEffect; } + @Override + public long getDuration() { + return mEffect.getDuration(); + } + /** @hide */ @Override public void validate() { @@ -272,12 +283,17 @@ public abstract class CombinedVibrationEffect implements Parcelable { } @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 @@ public abstract class CombinedVibrationEffect implements Parcelable { 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 @@ public abstract class CombinedVibrationEffect implements Parcelable { } @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 @@ public abstract class CombinedVibrationEffect implements Parcelable { 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 @@ public abstract class CombinedVibrationEffect implements Parcelable { "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 @@ public abstract class CombinedVibrationEffect implements Parcelable { } @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 b57418d751bc..b9b7a6e41937 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -317,7 +317,7 @@ public abstract class VibrationEffect implements Parcelable { */ @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 abstract class VibrationEffect implements Parcelable { 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); + public Prebaked(int effectId, boolean fallback, int effectStrength) { + mEffectId = effectId; + mFallback = fallback; + mEffectStrength = effectStrength; + mFallbackEffect = null; } /** @hide */ - public Prebaked(int effectId, boolean fallback, int effectStrength) { + public Prebaked(int effectId, int effectStrength, @NonNull VibrationEffect fallbackEffect) { mEffectId = effectId; - mFallback = fallback; + mFallback = true; mEffectStrength = effectStrength; + mFallbackEffect = fallbackEffect; } public int getId() { @@ -829,33 +837,44 @@ public abstract class VibrationEffect implements Parcelable { /** @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. + if (mFallbackEffect != null) { + VibrationEffect scaledFallback = mFallbackEffect.scale(scaleFactor); + if (!mFallbackEffect.equals(scaledFallback)) { + return new Prebaked(mEffectId, mEffectStrength, scaledFallback); + } + } + // Prebaked effect strength cannot be scaled with this method. return this; } /** - * Set the effect strength of the prebaked effect. + * Set the effect strength. */ - public void setEffectStrength(int strength) { - if (!isValidEffectStrength(strength)) { - throw new IllegalArgumentException("Invalid effect strength: " + strength); - } - mEffectStrength = strength; + public int getEffectStrength() { + return mEffectStrength; } /** - * Set the effect strength. + * Return the fallback effect, if set. + * + * @hide */ - public int getEffectStrength() { - return mEffectStrength; + @Nullable + public VibrationEffect getFallbackEffect() { + return mFallbackEffect; } private static boolean isValidEffectStrength(int strength) { @@ -901,15 +920,13 @@ public abstract class VibrationEffect implements Parcelable { 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 @@ public abstract class VibrationEffect implements Parcelable { return "Prebaked{mEffectId=" + mEffectId + ", mEffectStrength=" + mEffectStrength + ", mFallback=" + mFallback + + ", mFallbackEffect=" + mFallbackEffect + "}"; } @@ -927,6 +945,7 @@ public abstract class VibrationEffect implements Parcelable { 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 @@ public abstract class VibrationEffect implements Parcelable { // 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 @@ public abstract class VibrationEffect implements Parcelable { /** @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 c357414c8913..1d56e179317f 100644 --- a/core/tests/coretests/src/android/os/VibrationEffectTest.java +++ b/core/tests/coretests/src/android/os/VibrationEffectTest.java @@ -39,6 +39,7 @@ import android.platform.test.annotations.Presubmit; 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 @@ public class VibrationEffectTest { } @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 @@ public class VibrationEffectTest { 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 6a9715e49b99..2c83da55f275 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.content.pm.PackageManagerInternal; 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.IVibratorStateListener; 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.GuardedBy; 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 @@ public class VibratorService extends IVibratorService.Stub { 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,61 +102,55 @@ public class VibratorService extends IVibratorService.Stub { 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 OnVibrationCompleteListener} with a weak reference to this service. + * Implementation of {@link VibrationThread.VibrationCallbacks} that reports finished + * vibrations. */ - private static final class VibrationCompleteListener implements OnVibrationCompleteListener { - private WeakReference<VibratorService> mServiceRef; - - VibrationCompleteListener(VibratorService service) { - mServiceRef = new WeakReference<>(service); - } + private final class VibrationCallbacks implements VibrationThread.VibrationCallbacks { @Override - public void onComplete(int vibratorId, long vibrationId) { - VibratorService service = mServiceRef.get(); - if (service != null) { - service.onVibrationComplete(vibrationId); - } + public void prepareSyncedVibration(int requiredCapabilities, int[] vibratorIds) { } - } - /** 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 triggerSyncedVibration(long vibrationId) { } @Override - public void binderDied() { + public void onVibrationEnded(long vibrationId, Vibration.Status status) { + if (DEBUG) { + Slog.d(TAG, "Vibration thread finished with status " + status); + } synchronized (mLock) { - if (mVibration == mCurrentVibration) { - if (DEBUG) { - Slog.d(TAG, "Vibration finished because binder died, cleaning up"); - } - doCancelVibrateLocked(Vibration.Status.CANCELLED); - } + mThread = null; + reportFinishVibrationLocked(status); } } + } - private void linkToDeath() throws RemoteException { - mVibration.token.linkToDeath(this, 0); + /** + * Implementation of {@link OnVibrationCompleteListener} with a weak reference to this service. + */ + private static final class VibrationCompleteListener implements OnVibrationCompleteListener { + private WeakReference<VibratorService> mServiceRef; + + VibrationCompleteListener(VibratorService service) { + mServiceRef = new WeakReference<>(service); } - private void unlinkToDeath() { - mVibration.token.unlinkToDeath(this, 0); + @Override + public void onComplete(int vibratorId, long vibrationId) { + VibratorService service = mServiceRef.get(); + if (service != null) { + service.onVibrationComplete(vibratorId, vibrationId); + } } } @@ -262,13 +254,20 @@ public class VibratorService extends IVibratorService.Stub { /** 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"); + } + 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); } - doCancelVibrateLocked(Vibration.Status.FINISHED); } } } @@ -354,6 +353,17 @@ public class VibratorService extends IVibratorService.Stub { 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 @@ public class VibratorService extends IVibratorService.Stub { 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 @@ public class VibratorService extends IVibratorService.Stub { endVibrationLocked(vib, Vibration.Status.IGNORED_BACKGROUND); return; } - linkVibrationLocked(vib); final long ident = Binder.clearCallingIdentity(); try { doCancelVibrateLocked(Vibration.Status.CANCELLED); @@ -474,6 +483,10 @@ public class VibratorService extends IVibratorService.Stub { 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 @@ public class VibratorService extends IVibratorService.Stub { @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 @@ public class VibratorService extends IVibratorService.Stub { } } - // 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 @@ public class VibratorService extends IVibratorService.Stub { 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 @@ public class VibratorService extends IVibratorService.Stub { @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 @@ public class VibratorService extends IVibratorService.Stub { } } - @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 @@ public class VibratorService extends IVibratorService.Stub { } } - 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 @@ public class VibratorService extends IVibratorService.Stub { } } - @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 @@ public class VibratorService extends IVibratorService.Stub { 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 @@ public class VibratorService extends IVibratorService.Stub { 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 71fcd1de3b8f..950c225f0894 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -1866,6 +1866,13 @@ public class InputManagerService extends IInputManager.Stub } VibrationInfo(VibrationEffect effect) { + // First replace prebaked effects with its fallback, if any available. + if (effect instanceof VibrationEffect.Prebaked) { + VibrationEffect fallback = ((VibrationEffect.Prebaked) effect).getFallbackEffect(); + if (fallback != null) { + effect = fallback; + } + } if (effect instanceof VibrationEffect.OneShot) { VibrationEffect.OneShot oneShot = (VibrationEffect.OneShot) effect; mPattern = new long[] { 0, oneShot.getDuration() }; @@ -1892,8 +1899,7 @@ public class InputManagerService extends IInputManager.Stub 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 edbc05802697..39687231c249 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 @@ package com.android.server.vibrator; 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 @@ public final class InputDeviceDelegate implements InputManager.InputDeviceListen * * @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 b0266d025c08..fe3b03abc79b 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 @@ package com.android.server.vibrator; 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 @@ public class Vibration { /** 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 @@ public class Vibration { 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 @@ public class Vibration { * 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 @@ public class Vibration { /** Return the effect that should be played by this vibration. */ @Nullable - public VibrationEffect getEffect() { + public CombinedVibrationEffect getEffect() { return mEffect; } @@ -154,8 +155,8 @@ public class Vibration { 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 @@ public class Vibration { 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 @@ public class Vibration { 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 5f7e47d6ca29..0fa4fe16e1ba 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 @@ package com.android.server.vibrator; 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 @@ public final class VibrationScaler { } /** + * 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 @@ public final class VibrationScaler { int intensity = mSettingsController.getCurrentIntensity(usageHint); int newStrength = intensityToEffectStrength(intensity); VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) effect; - - if (prebaked.getEffectStrength() == newStrength) { + int strength = prebaked.getEffectStrength(); + VibrationEffect fallback = prebaked.getFallbackEffect(); + + 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 @@ public final class VibrationScaler { 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 000000000000..a4d888b3f9cf --- /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 311c73bcb19f..53f52e286fbd 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 @@ public final class VibratorController { } } + @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 @@ public final class VibratorController { * {@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 @@ public final class VibratorController { * {@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 726536db859e..0a35db56f35c 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.assertFalse; 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 android.platform.test.annotations.Presubmit; import androidx.test.InstrumentationRegistry; +import com.android.server.vibrator.FakeVibratorControllerProvider; import com.android.server.vibrator.VibratorController; import org.junit.After; @@ -81,7 +78,7 @@ public class VibratorManagerServiceTest { @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 @@ public class VibratorManagerServiceTest { @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 @@ public class VibratorManagerServiceTest { } @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 @@ public class VibratorManagerServiceTest { @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 @@ public class VibratorManagerServiceTest { @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 @@ public class VibratorManagerServiceTest { 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 @@ public class VibratorManagerServiceTest { "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 32ca7b58c48c..92256e24d7b3 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 @@ package com.android.server; 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.Looper; 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 androidx.test.InstrumentationRegistry; 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.Arrays; 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 @@ import java.util.concurrent.atomic.AtomicLong; 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 @@ public class VibratorServiceTest { // 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 @@ public class VibratorServiceTest { 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 @@ public class VibratorServiceTest { @Override VibratorController createVibratorController( VibratorController.OnVibrationCompleteListener listener) { - return new VibratorController(0, listener, mNativeWrapperMock); + return mVibratorProvider.newVibratorController(VIBRATOR_ID, listener); } @Override @@ -203,25 +198,23 @@ public class VibratorServiceTest { @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 class VibratorServiceTest { 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 @@ public class VibratorServiceTest { } @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 @@ public class VibratorServiceTest { 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 @@ public class VibratorServiceTest { 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 @@ public class VibratorServiceTest { 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); - - // 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()); + vibrateAndWait(service, effect, ALARM_ATTRS); - 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 @@ public class VibratorServiceTest { 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()); - } - @Test - public void vibrate_withOneShotAndNativeCallbackTriggered_finishesVibration() { - 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)); - - InOrder inOrderVerifier = inOrder(mNativeWrapperMock); - inOrderVerifier.verify(mNativeWrapperMock).off(); - inOrderVerifier.verify(mNativeWrapperMock).on(eq(100L), gt(0L)); - inOrderVerifier.verify(mNativeWrapperMock).off(); + // VibrationThread will start this vibration async, so wait before checking it never played. + Thread.sleep(10); + assertTrue(mVibratorProvider.getEffects().isEmpty()); } @Test - public void vibrate_withPrebakedAndNativeCallbackTriggered_finishesVibration() { - when(mNativeWrapperMock.getSupportedEffects()) - .thenReturn(new int[]{VibrationEffect.EFFECT_CLICK}); + public void vibrate_withNativeCallbackTriggered_finishesVibration() throws Exception { + mVibratorProvider.setSupportedEffects(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(); - } + vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), ALARM_ATTRS); - @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); + // VibrationThread will start this vibration async, so wait before triggering callbacks. + Thread.sleep(10); + assertTrue(service.isVibrating()); - VibrationEffect effect = VibrationEffect.createWaveform(new long[]{1, 3, 1, 2}, -1); - vibrate(service, effect); + // Trigger callbacks from controller. + mTestLooper.moveTimeForward(50); + mTestLooper.dispatchAll(); - // 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(); + // VibrationThread needs some time to react to native callbacks and stop the vibrator. + Thread.sleep(10); + assertFalse(service.isVibrating()); } @Test - public void vibrate_withComposedAndNativeCallbackTriggered_finishesVibration() { - mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + public void cancelVibrate_withDeviceVibrating_callsOff() throws Exception { 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(); - } + vibrate(service, VibrationEffect.createOneShot(100, 100), ALARM_ATTRS); - @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 checking. + Thread.sleep(10); assertTrue(service.isVibrating()); - Mockito.clearInvocations(mNativeWrapperMock); service.cancelVibrate(service); - assertFalse(service.isVibrating()); - verify(mNativeWrapperMock).off(); - } - - @Test - public void cancelVibrate_withDeviceNotVibrating_ignoresCall() { - VibratorService service = createService(); - Mockito.clearInvocations(mNativeWrapperMock); - 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 @@ public class VibratorServiceTest { 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 @@ public class VibratorServiceTest { 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 @@ public class VibratorServiceTest { 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); - - 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()); + vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK), RINGTONE_ATTRS); + + 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 @@ public class VibratorServiceTest { 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 @@ public class VibratorServiceTest { 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 000000000000..f562c1613413 --- /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 ac93ff691925..28d313b4d4b5 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.content.ContextWrapper; 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 @@ public class InputDeviceDelegateTest { 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 @@ public class InputDeviceDelegateTest { @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 @@ public class InputDeviceDelegateTest { 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 1a4ac0777ba5..82a693730dc2 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 static org.mockito.Mockito.when; 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 @@ public class VibrationScalerTest { } @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 @@ public class VibrationScalerTest { } @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 000000000000..bee739231d3f --- /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); + } +} |