diff options
| author | 2020-11-25 18:32:37 +0000 | |
|---|---|---|
| committer | 2021-01-19 11:48:34 +0000 | |
| commit | d6fae9a1e8ce2195ad26204daeb71222b87283e6 (patch) | |
| tree | 1c0f49e3d902ba7b2364058cf590a8a42e7b23d2 | |
| parent | 13658939c59824ecd724886d95189e7a888e5269 (diff) | |
Create single thread to play any Vibration
This thread takes in a CombinedVibrationEffect and plays all vibrations.
The Prebaked effect now also holds a fallback VibrationEffect, which is
resolved and scaled together with the original effect and is passed on
to the VibrateThread. All class attributes are not final, making this
effect immutable as all the others.
The Vibration now takes in a CombinedVibrationEffect, in preparation to
be used by the VibratorManagerService in multiple vibrators.
The new thread is replacing all vibration from VibratorService, which
means they are all triggering the HAL asynchronously.
Bug: 167946816
Bug: 131311651
Test: VibrationThreadTest, VibratorServiceTest, VibrationEffectTest, VibrationScalerTest
Change-Id: Ic27b35e63ca35ad47083f94da9ca7bd75b683d43
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); + } +} |