diff options
| author | 2021-02-02 17:15:44 +0000 | |
|---|---|---|
| committer | 2021-02-02 17:15:44 +0000 | |
| commit | 14edd62216032f8756e7d7327328bdb96ed18b5b (patch) | |
| tree | 3a40852d043cb803ff76c67779be4a947ca88e70 | |
| parent | 277cfb354dc76da33cb7c2cfaa914cedf73e130c (diff) | |
| parent | 502e1ae7b76c942c1071450299c9e2f28e747d70 (diff) | |
Merge "Implement vibrate and cancelVibrate on VibratorManagerService" into sc-dev
8 files changed, 1014 insertions, 46 deletions
diff --git a/core/java/android/os/CombinedVibrationEffect.java b/core/java/android/os/CombinedVibrationEffect.java index 869a72717f9f..cb4e9cba0977 100644 --- a/core/java/android/os/CombinedVibrationEffect.java +++ b/core/java/android/os/CombinedVibrationEffect.java @@ -364,8 +364,22 @@ public abstract class CombinedVibrationEffect implements Parcelable { @Override public long getDuration() { long maxDuration = Long.MIN_VALUE; + boolean hasUnknownStep = false; for (int i = 0; i < mEffects.size(); i++) { - maxDuration = Math.max(maxDuration, mEffects.valueAt(i).getDuration()); + long duration = mEffects.valueAt(i).getDuration(); + if (duration == Long.MAX_VALUE) { + // If any duration is repeating, this combination duration is also repeating. + return duration; + } + maxDuration = Math.max(maxDuration, duration); + // If any step is unknown, this combination duration will also be unknown, unless + // any step is repeating. Repeating vibrations take precedence over non-repeating + // ones in the service, so continue looping to check for repeating steps. + hasUnknownStep |= duration < 0; + } + if (hasUnknownStep) { + // If any step is unknown, this combination duration is also unknown. + return -1; } return maxDuration; } @@ -477,16 +491,25 @@ public abstract class CombinedVibrationEffect implements Parcelable { @Override public long getDuration() { + boolean hasUnknownStep = false; 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. + if (duration == Long.MAX_VALUE) { + // If any duration is repeating, this combination duration is also repeating. return duration; } durations += duration; + // If any step is unknown, this combination duration will also be unknown, unless + // any step is repeating. Repeating vibrations take precedence over non-repeating + // ones in the service, so continue looping to check for repeating steps. + hasUnknownStep |= duration < 0; + } + if (hasUnknownStep) { + // If any step is unknown, this combination duration is also unknown. + return -1; } long delays = 0; for (int i = 0; i < effectCount; i++) { diff --git a/core/java/android/os/IVibratorManagerService.aidl b/core/java/android/os/IVibratorManagerService.aidl index 804dc102c3f6..f9e294791cca 100644 --- a/core/java/android/os/IVibratorManagerService.aidl +++ b/core/java/android/os/IVibratorManagerService.aidl @@ -17,6 +17,7 @@ package android.os; import android.os.CombinedVibrationEffect; +import android.os.IVibratorStateListener; import android.os.VibrationAttributes; import android.os.VibratorInfo; @@ -24,6 +25,9 @@ import android.os.VibratorInfo; interface IVibratorManagerService { int[] getVibratorIds(); VibratorInfo getVibratorInfo(int vibratorId); + boolean isVibrating(int vibratorId); + boolean registerVibratorStateListener(int vibratorId, in IVibratorStateListener listener); + boolean unregisterVibratorStateListener(int vibratorId, in IVibratorStateListener listener); boolean setAlwaysOnEffect(int uid, String opPkg, int alwaysOnId, in CombinedVibrationEffect effect, in VibrationAttributes attributes); void vibrate(int uid, String opPkg, in CombinedVibrationEffect effect, diff --git a/core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java b/core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java index 6955ca84103e..564103efef65 100644 --- a/core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java +++ b/core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java @@ -117,6 +117,61 @@ public class CombinedVibrationEffectTest { } @Test + public void testDurationMono() { + assertEquals(1, CombinedVibrationEffect.createSynced( + VibrationEffect.createOneShot(1, 1)).getDuration()); + assertEquals(-1, CombinedVibrationEffect.createSynced( + VibrationEffect.get(VibrationEffect.EFFECT_CLICK)).getDuration()); + assertEquals(Long.MAX_VALUE, CombinedVibrationEffect.createSynced( + VibrationEffect.createWaveform( + new long[]{1, 2, 3}, new int[]{1, 2, 3}, 0)).getDuration()); + } + + @Test + public void testDurationStereo() { + assertEquals(6, CombinedVibrationEffect.startSynced() + .addVibrator(1, VibrationEffect.createOneShot(1, 1)) + .addVibrator(2, + VibrationEffect.createWaveform(new long[]{1, 2, 3}, new int[]{1, 2, 3}, -1)) + .combine() + .getDuration()); + assertEquals(-1, CombinedVibrationEffect.startSynced() + .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) + .addVibrator(2, + VibrationEffect.createWaveform(new long[]{1, 2, 3}, new int[]{1, 2, 3}, -1)) + .combine() + .getDuration()); + assertEquals(Long.MAX_VALUE, CombinedVibrationEffect.startSynced() + .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) + .addVibrator(2, + VibrationEffect.createWaveform(new long[]{1, 2, 3}, new int[]{1, 2, 3}, 0)) + .combine() + .getDuration()); + } + + @Test + public void testDurationSequential() { + assertEquals(26, CombinedVibrationEffect.startSequential() + .addNext(1, VibrationEffect.createOneShot(10, 10), 10) + .addNext(2, + VibrationEffect.createWaveform(new long[]{1, 2, 3}, new int[]{1, 2, 3}, -1)) + .combine() + .getDuration()); + assertEquals(-1, CombinedVibrationEffect.startSequential() + .addNext(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) + .addNext(2, + VibrationEffect.createWaveform(new long[]{1, 2, 3}, new int[]{1, 2, 3}, -1)) + .combine() + .getDuration()); + assertEquals(Long.MAX_VALUE, CombinedVibrationEffect.startSequential() + .addNext(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) + .addNext(2, + VibrationEffect.createWaveform(new long[]{1, 2, 3}, new int[]{1, 2, 3}, 0)) + .combine() + .getDuration()); + } + + @Test public void testSerializationMono() { CombinedVibrationEffect original = CombinedVibrationEffect.createSynced(VALID_EFFECT); diff --git a/services/core/java/com/android/server/VibratorManagerService.java b/services/core/java/com/android/server/VibratorManagerService.java index eca1dfaed810..1738c971afa2 100644 --- a/services/core/java/com/android/server/VibratorManagerService.java +++ b/services/core/java/com/android/server/VibratorManagerService.java @@ -22,12 +22,20 @@ import android.app.AppOpsManager; import android.content.Context; import android.content.pm.PackageManager; 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; +import android.os.IExternalVibratorService; import android.os.IVibratorManagerService; +import android.os.IVibratorStateListener; import android.os.Looper; +import android.os.PowerManager; +import android.os.Process; import android.os.ResultReceiver; +import android.os.ServiceManager; import android.os.ShellCallback; import android.os.ShellCommand; import android.os.Trace; @@ -40,9 +48,12 @@ 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.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 libcore.util.NativeAllocationRegistry; @@ -51,6 +62,8 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; @@ -83,18 +96,31 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + // Used to generate globally unique vibration ids. + private final AtomicInteger mNextVibrationId = new AtomicInteger(1); // 0 = no callback + private final Object mLock = new Object(); private final Context mContext; + private final PowerManager.WakeLock mWakeLock; + private final IBatteryStats mBatteryStatsService; private final Handler mHandler; private final AppOpsManager mAppOps; private final NativeWrapper mNativeWrapper; private final int[] mVibratorIds; private final SparseArray<VibratorController> mVibrators; + private final VibrationCallbacks mVibrationCallbacks = new VibrationCallbacks(); @GuardedBy("mLock") private final SparseArray<AlwaysOnVibration> mAlwaysOnEffects = new SparseArray<>(); + @GuardedBy("mLock") + private VibrationThread mCurrentVibration; + @GuardedBy("mLock") + private VibrationThread mNextVibration; + @GuardedBy("mLock") + private ExternalVibrationHolder mCurrentExternalVibration; private VibrationSettings mVibrationSettings; private VibrationScaler mVibrationScaler; + private InputDeviceDelegate mInputDeviceDelegate; static native long nativeInit(); @@ -109,8 +135,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mNativeWrapper = injector.getNativeWrapper(); mNativeWrapper.init(); + mBatteryStatsService = IBatteryStats.Stub.asInterface(ServiceManager.getService( + BatteryStats.SERVICE_NAME)); + mAppOps = mContext.getSystemService(AppOpsManager.class); + PowerManager pm = context.getSystemService(PowerManager.class); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*vibrator*"); + mWakeLock.setReferenceCounted(true); + int[] vibratorIds = mNativeWrapper.getVibratorIds(); if (vibratorIds == null) { mVibratorIds = new int[0]; @@ -134,6 +167,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { try { mVibrationSettings = new VibrationSettings(mContext, mHandler); mVibrationScaler = new VibrationScaler(mContext, mVibrationSettings); + mInputDeviceDelegate = new InputDeviceDelegate(mContext, mHandler); mVibrationSettings.addListener(this::updateServiceState); @@ -145,6 +179,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @Override // Binder call + public int[] getVibratorIds() { + return Arrays.copyOf(mVibratorIds, mVibratorIds.length); + } + + @Override // Binder call @Nullable public VibratorInfo getVibratorInfo(int vibratorId) { VibratorController controller = mVibrators.get(vibratorId); @@ -152,8 +191,37 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @Override // Binder call - public int[] getVibratorIds() { - return Arrays.copyOf(mVibratorIds, mVibratorIds.length); + public boolean isVibrating(int vibratorId) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.ACCESS_VIBRATOR_STATE, + "isVibrating"); + VibratorController controller = mVibrators.get(vibratorId); + return controller != null && controller.isVibrating(); + } + + @Override // Binder call + public boolean registerVibratorStateListener(int vibratorId, IVibratorStateListener listener) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.ACCESS_VIBRATOR_STATE, + "registerVibratorStateListener"); + VibratorController controller = mVibrators.get(vibratorId); + if (controller == null) { + return false; + } + return controller.registerVibratorStateListener(listener); + } + + @Override // Binder call + public boolean unregisterVibratorStateListener(int vibratorId, + IVibratorStateListener listener) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.ACCESS_VIBRATOR_STATE, + "unregisterVibratorStateListener"); + VibratorController controller = mVibrators.get(vibratorId); + if (controller == null) { + return false; + } + return controller.unregisterVibratorStateListener(listener); } @Override // Binder call @@ -161,9 +229,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @Nullable CombinedVibrationEffect effect, @Nullable VibrationAttributes attrs) { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "setAlwaysOnEffect"); try { - if (!hasPermission(android.Manifest.permission.VIBRATE_ALWAYS_ON)) { - throw new SecurityException("Requires VIBRATE_ALWAYS_ON permission"); - } + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.VIBRATE_ALWAYS_ON, + "setAlwaysOnEffect"); + if (effect == null) { synchronized (mLock) { mAlwaysOnEffects.delete(alwaysOnId); @@ -200,12 +269,89 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @Override // Binder call public void vibrate(int uid, String opPkg, @NonNull CombinedVibrationEffect effect, @Nullable VibrationAttributes attrs, String reason, IBinder token) { - throw new UnsupportedOperationException("Not implemented"); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate, reason = " + reason); + try { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.VIBRATE, "vibrate"); + + if (token == null) { + Slog.e(TAG, "token must not be null"); + return; + } + enforceUpdateAppOpsStatsPermission(uid); + if (!isEffectValid(effect)) { + return; + } + effect = fixupVibrationEffect(effect); + attrs = fixupVibrationAttributes(attrs); + Vibration vib = new Vibration(token, mNextVibrationId.getAndIncrement(), effect, attrs, + uid, opPkg, reason); + + synchronized (mLock) { + Vibration.Status ignoreStatus = shouldIgnoreVibrationLocked(vib); + if (ignoreStatus != null) { + endVibrationLocked(vib, ignoreStatus); + return; + } + + VibrationThread vibThread = new VibrationThread(vib, mVibrators, mWakeLock, + mBatteryStatsService, mVibrationCallbacks); + + ignoreStatus = shouldIgnoreVibrationForCurrentLocked(vibThread); + if (ignoreStatus != null) { + endVibrationLocked(vib, ignoreStatus); + return; + } + + final long ident = Binder.clearCallingIdentity(); + try { + if (mCurrentVibration != null) { + mCurrentVibration.cancel(); + } + Vibration.Status status = startVibrationLocked(vibThread); + if (status != Vibration.Status.RUNNING) { + endVibrationLocked(vib, status); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } } @Override // Binder call public void cancelVibrate(IBinder token) { - throw new UnsupportedOperationException("Not implemented"); + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "cancelVibrate"); + try { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.VIBRATE, + "cancelVibrate"); + + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "Canceling vibration."); + } + final long ident = Binder.clearCallingIdentity(); + try { + mNextVibration = null; + if (mCurrentVibration != null + && mCurrentVibration.getVibration().token == token) { + mCurrentVibration.cancel(); + } + if (mCurrentExternalVibration != null) { + // TODO(b/167946816): end vibration and add to list to be dumped for debug + mCurrentExternalVibration.externalVibration.mute(); + mCurrentExternalVibration = null; + // TODO(b/167946816): set external control to false + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } } @Override @@ -214,11 +360,24 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { new VibratorManagerShellCommand(this).exec(this, in, out, err, args, cb, resultReceiver); } - private void updateServiceState() { + @VisibleForTesting + void updateServiceState() { synchronized (mLock) { + boolean inputDevicesChanged = mInputDeviceDelegate.updateInputDeviceVibrators( + mVibrationSettings.shouldVibrateInputDevices()); + for (int i = 0; i < mAlwaysOnEffects.size(); i++) { updateAlwaysOnLocked(mAlwaysOnEffects.valueAt(i)); } + + if (mCurrentVibration == null) { + return; + } + + if (inputDevicesChanged || !mVibrationSettings.shouldVibrateForPowerMode( + mCurrentVibration.getVibration().attrs.getUsage())) { + mCurrentVibration.cancel(); + } } } @@ -230,9 +389,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (vibrator == null) { continue; } - Vibration.Status ignoredStatus = shouldIgnoreVibrationLocked( + Vibration.Status ignoreStatus = shouldIgnoreVibrationLocked( vib.uid, vib.opPkg, vib.attrs); - if (ignoredStatus == null) { + if (ignoreStatus == null) { effect = mVibrationScaler.scale(effect, vib.attrs.getUsage()); } else { // Vibration should not run, use null effect to remove registered effect. @@ -242,6 +401,134 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + @GuardedBy("mLock") + private Vibration.Status startVibrationLocked(VibrationThread vibThread) { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "startVibrationLocked"); + try { + Vibration vib = vibThread.getVibration(); + vib.updateEffect(mVibrationScaler.scale(vib.getEffect(), vib.attrs.getUsage())); + + boolean inputDevicesAvailable = mInputDeviceDelegate.vibrateIfAvailable( + vib.uid, vib.opPkg, vib.getEffect(), vib.reason, vib.attrs); + + if (inputDevicesAvailable) { + return Vibration.Status.FORWARDED_TO_INPUT_DEVICES; + } + + if (mCurrentVibration == null) { + return startVibrationThreadLocked(vibThread); + } + + mNextVibration = vibThread; + return Vibration.Status.RUNNING; + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } + + @GuardedBy("mLock") + private Vibration.Status startVibrationThreadLocked(VibrationThread vibThread) { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "startVibrationThreadLocked"); + try { + Vibration vib = vibThread.getVibration(); + int mode = startAppOpModeLocked(vib.uid, vib.opPkg, vib.attrs); + switch (mode) { + case AppOpsManager.MODE_ALLOWED: + Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0); + mCurrentVibration = vibThread; + mCurrentVibration.start(); + return Vibration.Status.RUNNING; + case AppOpsManager.MODE_ERRORED: + Slog.w(TAG, "Start AppOpsManager operation errored for uid " + vib.uid); + return Vibration.Status.IGNORED_ERROR_APP_OPS; + default: + return Vibration.Status.IGNORED_APP_OPS; + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } + + @GuardedBy("mLock") + private void endVibrationLocked(Vibration vib, Vibration.Status status) { + // TODO(b/167946816): end vibration and add to list to be dumped for debug + } + + @GuardedBy("mLock") + private void reportFinishedVibrationLocked(Vibration.Status status) { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "reportFinishVibrationLocked"); + Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0); + try { + Vibration vib = mCurrentVibration.getVibration(); + mCurrentVibration = null; + endVibrationLocked(vib, status); + finishAppOpModeLocked(vib.uid, vib.opPkg); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } + + private void onVibrationComplete(int vibratorId, long vibrationId) { + synchronized (mLock) { + if (mCurrentVibration != null && mCurrentVibration.getVibration().id == vibrationId) { + if (DEBUG) { + Slog.d(TAG, "Vibration " + vibrationId + " on vibrator " + vibratorId + + " complete, notifying thread"); + } + mCurrentVibration.vibratorComplete(vibratorId); + } + } + } + + /** + * Check if given vibration should be ignored in favour of one of the vibrations currently + * running on the same vibrators. + * + * @return One of Vibration.Status.IGNORED_* values if the vibration should be ignored. + */ + @GuardedBy("mLock") + @Nullable + private Vibration.Status shouldIgnoreVibrationForCurrentLocked(VibrationThread vibThread) { + if (vibThread.getVibration().isRepeating()) { + // Repeating vibrations always take precedence. + return null; + } + if (mCurrentVibration != null && mCurrentVibration.getVibration().isRepeating()) { + if (DEBUG) { + Slog.d(TAG, "Ignoring incoming vibration in favor of previous alarm vibration"); + } + return Vibration.Status.IGNORED_FOR_ALARM; + } + return null; + } + + /** + * Check if given vibration should be ignored by this service. + * + * @return One of Vibration.Status.IGNORED_* values if the vibration should be ignored. + * @see #shouldIgnoreVibrationLocked(int, String, VibrationAttributes) + */ + @GuardedBy("mLock") + @Nullable + private Vibration.Status shouldIgnoreVibrationLocked(Vibration vib) { + // If something has external control of the vibrator, assume that it's more important. + if (mCurrentExternalVibration != null) { + if (DEBUG) { + Slog.d(TAG, "Ignoring incoming vibration for current external vibration"); + } + return Vibration.Status.IGNORED_FOR_EXTERNAL; + } + + if (!mVibrationSettings.shouldVibrateForUid(vib.uid, vib.attrs.getUsage())) { + Slog.e(TAG, "Ignoring incoming vibration as process with" + + " uid= " + vib.uid + " is background," + + " attrs= " + vib.attrs); + return Vibration.Status.IGNORED_BACKGROUND; + } + + return shouldIgnoreVibrationLocked(vib.uid, vib.opPkg, vib.attrs); + } + /** * Check if a vibration with given {@code uid}, {@code opPkg} and {@code attrs} should be * ignored by this service. @@ -271,7 +558,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { return Vibration.Status.IGNORED_RINGTONE; } - int mode = getAppOpMode(uid, opPkg, attrs); + int mode = checkAppOpModeLocked(uid, opPkg, attrs); if (mode != AppOpsManager.MODE_ALLOWED) { if (mode == AppOpsManager.MODE_ERRORED) { // We might be getting calls from within system_server, so we don't actually @@ -290,21 +577,49 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { * Check which mode should be set for a vibration with given {@code uid}, {@code opPkg} and * {@code attrs}. This will return one of the AppOpsManager.MODE_*. */ - private int getAppOpMode(int uid, String opPkg, VibrationAttributes attrs) { + @GuardedBy("mLock") + private int checkAppOpModeLocked(int uid, String opPkg, VibrationAttributes attrs) { int mode = mAppOps.checkAudioOpNoThrow(AppOpsManager.OP_VIBRATE, attrs.getAudioUsage(), uid, opPkg); - if (mode == AppOpsManager.MODE_ALLOWED) { - mode = mAppOps.startOpNoThrow(AppOpsManager.OP_VIBRATE, uid, opPkg); - } - if (mode == AppOpsManager.MODE_IGNORED - && attrs.isFlagSet(VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY)) { + int fixedMode = fixupAppOpModeLocked(mode, attrs); + if (mode != fixedMode && fixedMode == AppOpsManager.MODE_ALLOWED) { // If we're just ignoring the vibration op then this is set by DND and we should ignore // if we're asked to bypass. AppOps won't be able to record this operation, so make // sure we at least note it in the logs for debugging. Slog.d(TAG, "Bypassing DND for vibrate from uid " + uid); - mode = AppOpsManager.MODE_ALLOWED; } - return mode; + return fixedMode; + } + + /** Start an operation in {@link AppOpsManager}, if allowed. */ + @GuardedBy("mLock") + private int startAppOpModeLocked(int uid, String opPkg, VibrationAttributes attrs) { + return fixupAppOpModeLocked( + mAppOps.startOpNoThrow(AppOpsManager.OP_VIBRATE, uid, opPkg), attrs); + } + + /** + * Finish a previously started operation in {@link AppOpsManager}. This will be a noop if no + * operation with same uid was previously started. + */ + @GuardedBy("mLock") + private void finishAppOpModeLocked(int uid, String opPkg) { + mAppOps.finishOp(AppOpsManager.OP_VIBRATE, uid, opPkg); + } + + /** + * Enforces {@link android.Manifest.permission#UPDATE_APP_OPS_STATS} to incoming UID if it's + * different from the calling UID. + */ + private void enforceUpdateAppOpsStatsPermission(int uid) { + if (uid == Binder.getCallingUid()) { + return; + } + if (Binder.getCallingPid() == Process.myPid()) { + return; + } + mContext.enforcePermission(android.Manifest.permission.UPDATE_APP_OPS_STATS, + Binder.getCallingPid(), Binder.getCallingUid(), null); } /** @@ -330,6 +645,49 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } /** + * Sets fallback effects to all prebaked ones in given combination of effects, based on {@link + * VibrationSettings#getFallbackEffect}. + */ + private CombinedVibrationEffect fixupVibrationEffect(CombinedVibrationEffect effect) { + if (effect instanceof CombinedVibrationEffect.Mono) { + return CombinedVibrationEffect.createSynced( + fixupVibrationEffect(((CombinedVibrationEffect.Mono) effect).getEffect())); + } else if (effect instanceof CombinedVibrationEffect.Stereo) { + CombinedVibrationEffect.SyncedCombination combination = + CombinedVibrationEffect.startSynced(); + SparseArray<VibrationEffect> effects = + ((CombinedVibrationEffect.Stereo) effect).getEffects(); + for (int i = 0; i < effects.size(); i++) { + combination.addVibrator(effects.keyAt(i), fixupVibrationEffect(effects.valueAt(i))); + } + return combination.combine(); + } else if (effect instanceof CombinedVibrationEffect.Sequential) { + CombinedVibrationEffect.SequentialCombination combination = + CombinedVibrationEffect.startSequential(); + List<CombinedVibrationEffect> effects = + ((CombinedVibrationEffect.Sequential) effect).getEffects(); + for (CombinedVibrationEffect e : effects) { + combination.addNext(fixupVibrationEffect(e)); + } + return combination.combine(); + } + return effect; + } + + 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()); + if (fallback != null) { + return new VibrationEffect.Prebaked(prebaked.getId(), prebaked.getEffectStrength(), + fallback); + } + } + return effect; + } + + /** * Return new {@link VibrationAttributes} that only applies flags that this user has permissions * to use. */ @@ -388,6 +746,19 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + /** + * Check given mode, one of the AppOpsManager.MODE_*, against {@link VibrationAttributes} to + * allow bypassing {@link AppOpsManager} checks. + */ + @GuardedBy("mLock") + private int fixupAppOpModeLocked(int mode, VibrationAttributes attrs) { + if (mode == AppOpsManager.MODE_IGNORED + && attrs.isFlagSet(VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY)) { + return AppOpsManager.MODE_ALLOWED; + } + return mode; + } + private boolean hasPermission(String permission) { return mContext.checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED; @@ -428,6 +799,42 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } /** + * Implementation of {@link VibrationThread.VibrationCallbacks} that controls synced vibrations + * and reports them when finished. + */ + private final class VibrationCallbacks implements VibrationThread.VibrationCallbacks { + + @Override + public void prepareSyncedVibration(int requiredCapabilities, int[] vibratorIds) { + // TODO(b/167946816): call IVibratorManager to prepare + } + + @Override + public void triggerSyncedVibration(long vibrationId) { + // TODO(b/167946816): call IVibratorManager to trigger + } + + @Override + public void onVibrationEnded(long vibrationId, Vibration.Status status) { + if (DEBUG) { + Slog.d(TAG, "Vibration " + vibrationId + " thread finished with status " + status); + } + synchronized (mLock) { + if (mCurrentVibration != null + && mCurrentVibration.getVibration().id == vibrationId) { + reportFinishedVibrationLocked(status); + + if (mNextVibration != null) { + VibrationThread vibThread = mNextVibration; + mNextVibration = null; + startVibrationThreadLocked(vibThread); + } + } + } + } + } + + /** * Implementation of {@link VibratorController.OnVibrationCompleteListener} with a weak * reference to this service. */ @@ -443,7 +850,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { public void onComplete(int vibratorId, long vibrationId) { VibratorManagerService service = mServiceRef.get(); if (service != null) { - // TODO(b/159207608): finish vibration if all vibrators finished for this vibration + service.onVibrationComplete(vibratorId, vibrationId); } } } @@ -469,6 +876,41 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + /** Holder for a {@link ExternalVibration}. */ + private final class ExternalVibrationHolder { + + public final ExternalVibration externalVibration; + public int scale; + + private final long mStartTimeDebug; + private long mEndTimeDebug; + private Vibration.Status mStatus; + + private ExternalVibrationHolder(ExternalVibration externalVibration) { + this.externalVibration = externalVibration; + this.scale = IExternalVibratorService.SCALE_NONE; + mStartTimeDebug = System.currentTimeMillis(); + mStatus = Vibration.Status.RUNNING; + } + + public void end(Vibration.Status status) { + if (mStatus != Vibration.Status.RUNNING) { + // Vibration already ended, keep first ending status set and ignore this one. + return; + } + mStatus = status; + mEndTimeDebug = System.currentTimeMillis(); + } + + public Vibration.DebugInfo getDebugInfo() { + return new Vibration.DebugInfo( + mStartTimeDebug, mEndTimeDebug, /* effect= */ null, /* originalEffect= */ null, + scale, externalVibration.getVibrationAttributes(), + externalVibration.getUid(), externalVibration.getPackage(), + /* reason= */ null, mStatus); + } + } + /** Wrapper around the static-native methods of {@link VibratorManagerService} for tests. */ @VisibleForTesting public static class NativeWrapper { diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java index fe3b03abc79b..e0f5408a1537 100644 --- a/services/core/java/com/android/server/vibrator/Vibration.java +++ b/services/core/java/com/android/server/vibrator/Vibration.java @@ -138,6 +138,11 @@ public class Vibration { return mStatus != Status.RUNNING; } + /** Return true is effect is a repeating vibration. */ + public boolean isRepeating() { + return mEffect.getDuration() == Long.MAX_VALUE; + } + /** Return the effect that should be played by this vibration. */ @Nullable public CombinedVibrationEffect getEffect() { diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java index 53552526c936..6f391f3bbc92 100644 --- a/services/core/java/com/android/server/vibrator/VibrationThread.java +++ b/services/core/java/com/android/server/vibrator/VibrationThread.java @@ -114,10 +114,6 @@ public final class VibrationThread extends Thread implements IBinder.DeathRecipi } } - public Vibration getVibration() { - return mVibration; - } - @Override public void binderDied() { cancel(); @@ -156,6 +152,10 @@ public final class VibrationThread extends Thread implements IBinder.DeathRecipi } } + public Vibration getVibration() { + return mVibration; + } + @VisibleForTesting SparseArray<VibratorController> getVibrators() { return mVibrators; diff --git a/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java index 0a35db56f35c..f7b24920f903 100644 --- a/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java @@ -16,34 +16,61 @@ package com.android.server; -import static com.android.server.testutils.TestUtils.assertExpectException; - import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; 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.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.AppOpsManager; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.PackageManagerInternal; +import android.hardware.input.IInputManager; +import android.hardware.input.InputManager; import android.hardware.vibrator.IVibrator; +import android.media.AudioAttributes; +import android.media.AudioManager; import android.os.CombinedVibrationEffect; import android.os.Handler; +import android.os.IBinder; +import android.os.IVibratorStateListener; import android.os.Looper; import android.os.PowerManager; import android.os.PowerManagerInternal; import android.os.PowerSaveState; import android.os.Process; +import android.os.SystemClock; +import android.os.UserHandle; import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; import android.os.VibratorInfo; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; +import android.provider.Settings; +import android.view.InputDevice; import androidx.test.InstrumentationRegistry; +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.internal.util.test.FakeSettingsProviderRule; +import com.android.server.vibrator.FakeVibrator; import com.android.server.vibrator.FakeVibratorControllerProvider; import com.android.server.vibrator.VibratorController; @@ -51,12 +78,16 @@ 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.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.function.Predicate; /** * Tests for {@link VibratorManagerService}. @@ -67,39 +98,86 @@ import java.util.Map; @Presubmit public class VibratorManagerServiceTest { + private static final int TEST_TIMEOUT_MILLIS = 1_000; private static final int UID = Process.ROOT_UID; 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() + .setBatterySaverEnabled(true).build(); private static final VibrationAttributes ALARM_ATTRS = new VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_ALARM).build(); + private static final VibrationAttributes HAPTIC_FEEDBACK_ATTRS = + new VibrationAttributes.Builder().setUsage( + VibrationAttributes.USAGE_TOUCH).build(); + private static final VibrationAttributes NOTIFICATION_ATTRS = + new VibrationAttributes.Builder().setUsage( + VibrationAttributes.USAGE_NOTIFICATION).build(); + private static final VibrationAttributes RINGTONE_ATTRS = + new VibrationAttributes.Builder().setUsage( + VibrationAttributes.USAGE_RINGTONE).build(); @Rule public MockitoRule rule = MockitoJUnit.rule(); + @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); @Mock private VibratorManagerService.NativeWrapper mNativeWrapperMock; + @Mock private PackageManagerInternal mPackageManagerInternalMock; @Mock private PowerManagerInternal mPowerManagerInternalMock; @Mock private PowerSaveState mPowerSaveStateMock; + @Mock private AppOpsManager mAppOpsManagerMock; + @Mock private IInputManager mIInputManagerMock; private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>(); + private Context mContextSpy; private TestLooper mTestLooper; + private FakeVibrator mVibrator; + private PowerManagerInternal.LowPowerModeListener mRegisteredPowerModeListener; @Before public void setUp() throws Exception { mTestLooper = new TestLooper(); - + mVibrator = new FakeVibrator(); + mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext())); + InputManager inputManager = InputManager.resetInstance(mIInputManagerMock); + + ContentResolver contentResolver = mSettingsProviderRule.mockContentResolver(mContextSpy); + when(mContextSpy.getContentResolver()).thenReturn(contentResolver); + when(mContextSpy.getSystemService(eq(Context.VIBRATOR_SERVICE))).thenReturn(mVibrator); + when(mContextSpy.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager); + when(mContextSpy.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOpsManagerMock); + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[0]); + when(mPackageManagerInternalMock.getSystemUiServiceComponent()) + .thenReturn(new ComponentName("", "")); when(mPowerManagerInternalMock.getLowPowerState(PowerManager.ServiceType.VIBRATION)) .thenReturn(mPowerSaveStateMock); - + doAnswer(invocation -> { + mRegisteredPowerModeListener = invocation.getArgument(0); + return null; + }).when(mPowerManagerInternalMock).registerLowPowerModeObserver(any()); + + setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 1); + setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, + Vibrator.VIBRATION_INTENSITY_MEDIUM); + setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, + Vibrator.VIBRATION_INTENSITY_MEDIUM); + setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, + Vibrator.VIBRATION_INTENSITY_MEDIUM); + + addLocalServiceMock(PackageManagerInternal.class, mPackageManagerInternalMock); addLocalServiceMock(PowerManagerInternal.class, mPowerManagerInternalMock); + + mTestLooper.startAutoDispatch(); } @After public void tearDown() throws Exception { + LocalServices.removeServiceForTest(PackageManagerInternal.class); LocalServices.removeServiceForTest(PowerManagerInternal.class); } private VibratorManagerService createService() { VibratorManagerService service = new VibratorManagerService( - InstrumentationRegistry.getContext(), + mContextSpy, new VibratorManagerService.Injector() { @Override VibratorManagerService.NativeWrapper getNativeWrapper() { @@ -172,6 +250,74 @@ public class VibratorManagerServiceTest { } @Test + public void registerVibratorStateListener_callbacksAreTriggered() throws Exception { + mockVibrators(1); + VibratorManagerService service = createService(); + IVibratorStateListener listenerMock = mockVibratorStateListener(); + service.registerVibratorStateListener(1, listenerMock); + + vibrate(service, VibrationEffect.createOneShot(40, 100), ALARM_ATTRS); + // Wait until service knows vibrator is on. + assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + // Wait until effect ends. + assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + + InOrder inOrderVerifier = inOrder(listenerMock); + // First notification done when listener is registered. + inOrderVerifier.verify(listenerMock).onVibrating(eq(false)); + inOrderVerifier.verify(listenerMock).onVibrating(eq(true)); + inOrderVerifier.verify(listenerMock).onVibrating(eq(false)); + inOrderVerifier.verifyNoMoreInteractions(); + } + + @Test + public void unregisterVibratorStateListener_callbackNotTriggeredAfter() throws Exception { + mockVibrators(1); + VibratorManagerService service = createService(); + IVibratorStateListener listenerMock = mockVibratorStateListener(); + service.registerVibratorStateListener(1, listenerMock); + + vibrate(service, VibrationEffect.createOneShot(40, 100), ALARM_ATTRS); + + // Wait until service knows vibrator is on. + assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + + service.unregisterVibratorStateListener(1, listenerMock); + + // Wait until vibrator is off. + assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + + InOrder inOrderVerifier = inOrder(listenerMock); + // First notification done when listener is registered. + inOrderVerifier.verify(listenerMock).onVibrating(eq(false)); + inOrderVerifier.verify(listenerMock).onVibrating(eq(true)); + inOrderVerifier.verify(listenerMock, atLeastOnce()).asBinder(); // unregister + inOrderVerifier.verifyNoMoreInteractions(); + } + + @Test + public void registerVibratorStateListener_multipleVibratorsAreTriggered() throws Exception { + mockVibrators(0, 1, 2); + VibratorManagerService service = createService(); + IVibratorStateListener[] listeners = new IVibratorStateListener[3]; + for (int i = 0; i < 3; i++) { + listeners[i] = mockVibratorStateListener(); + service.registerVibratorStateListener(i, listeners[i]); + } + + vibrate(service, CombinedVibrationEffect.startSynced() + .addVibrator(0, VibrationEffect.createOneShot(40, 100)) + .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) + .combine(), ALARM_ATTRS); + // Wait until service knows vibrator is on. + assertTrue(waitUntil(s -> s.isVibrating(0), service, TEST_TIMEOUT_MILLIS)); + + verify(listeners[0]).onVibrating(eq(true)); + verify(listeners[1]).onVibrating(eq(true)); + verify(listeners[2], never()).onVibrating(eq(true)); + } + + @Test public void setAlwaysOnEffect_withMono_enablesAlwaysOnEffectToAllVibratorsWithCapability() { mockVibrators(1, 2, 3); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL); @@ -276,22 +422,264 @@ public class VibratorManagerServiceTest { } @Test - public void vibrate_isUnsupported() { + public void vibrate_withRingtone_usesRingtoneSettings() throws Exception { + mockVibrators(1); + mVibrator.setDefaultRingVibrationIntensity(Vibrator.VIBRATION_INTENSITY_MEDIUM); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + + setRingerMode(AudioManager.RINGER_MODE_NORMAL); + setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 0); + setGlobalSetting(Settings.Global.APPLY_RAMPING_RINGER, 0); + VibratorManagerService service = createService(); + vibrate(service, VibrationEffect.createOneShot(40, 1), RINGTONE_ATTRS); + + setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 0); + setGlobalSetting(Settings.Global.APPLY_RAMPING_RINGER, 1); + service = createService(); + vibrate(service, VibrationEffect.createOneShot(40, 10), RINGTONE_ATTRS); + assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + + setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 1); + setGlobalSetting(Settings.Global.APPLY_RAMPING_RINGER, 0); + service = createService(); + vibrate(service, VibrationEffect.createOneShot(40, 100), RINGTONE_ATTRS); + assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + + assertEquals(2, mVibratorProviders.get(1).getEffects().size()); + assertEquals(Arrays.asList(10, 100), mVibratorProviders.get(1).getAmplitudes()); + } + + @Test + public void vibrate_withPowerMode_usesPowerModeState() throws Exception { + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); VibratorManagerService service = createService(); - CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced( - VibrationEffect.get(VibrationEffect.EFFECT_CLICK)); - assertExpectException(UnsupportedOperationException.class, - "Not implemented", - () -> service.vibrate(UID, PACKAGE_NAME, effect, ALARM_ATTRS, "reason", service)); + mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE); + vibrate(service, VibrationEffect.createOneShot(1, 1), HAPTIC_FEEDBACK_ATTRS); + vibrate(service, VibrationEffect.createOneShot(2, 2), RINGTONE_ATTRS); + assertTrue(waitUntil(s -> fakeVibrator.getEffects().size() == 1, + service, TEST_TIMEOUT_MILLIS)); + + mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE); + vibrate(service, VibrationEffect.createOneShot(3, 3), /* attributes= */ null); + assertTrue(waitUntil(s -> fakeVibrator.getEffects().size() == 2, + service, TEST_TIMEOUT_MILLIS)); + + vibrate(service, VibrationEffect.createOneShot(4, 4), NOTIFICATION_ATTRS); + assertTrue(waitUntil(s -> fakeVibrator.getEffects().size() == 3, + service, TEST_TIMEOUT_MILLIS)); + + assertEquals(Arrays.asList(2, 3, 4), fakeVibrator.getAmplitudes()); } @Test - public void cancelVibrate_isUnsupported() { + public void vibrate_withAudioAttributes_usesOriginalAudioUsageInAppOpsManager() { VibratorManagerService service = createService(); - CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced( - VibrationEffect.get(VibrationEffect.EFFECT_CLICK)); - assertExpectException(UnsupportedOperationException.class, - "Not implemented", () -> service.cancelVibrate(service)); + + VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); + AudioAttributes audioAttributes = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY).build(); + VibrationAttributes vibrationAttributes = new VibrationAttributes.Builder( + audioAttributes, effect).build(); + + vibrate(service, effect, vibrationAttributes); + + verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE), + eq(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY), anyInt(), anyString()); + } + + @Test + public void vibrate_withVibrationAttributes_usesCorrespondingAudioUsageInAppOpsManager() { + VibratorManagerService service = createService(); + + vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), ALARM_ATTRS); + vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_TICK), NOTIFICATION_ATTRS); + vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), RINGTONE_ATTRS); + vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_TICK), HAPTIC_FEEDBACK_ATTRS); + vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), + new VibrationAttributes.Builder().setUsage( + VibrationAttributes.USAGE_COMMUNICATION_REQUEST).build()); + vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_TICK), + new VibrationAttributes.Builder().setUsage( + VibrationAttributes.USAGE_UNKNOWN).build()); + + InOrder inOrderVerifier = inOrder(mAppOpsManagerMock); + inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE), + eq(AudioAttributes.USAGE_ALARM), anyInt(), anyString()); + inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE), + eq(AudioAttributes.USAGE_NOTIFICATION), anyInt(), anyString()); + inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE), + eq(AudioAttributes.USAGE_NOTIFICATION_RINGTONE), anyInt(), anyString()); + inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE), + eq(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION), anyInt(), anyString()); + inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE), + eq(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST), + anyInt(), anyString()); + inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE), + eq(AudioAttributes.USAGE_UNKNOWN), anyInt(), anyString()); + } + + @Test + public void vibrate_withInputDevices_vibratesInputDevices() throws Exception { + mockVibrators(1); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK); + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1}); + when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1)); + setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1); + VibratorManagerService service = createService(); + + // Prebaked vibration will play fallback waveform on input device. + ArgumentCaptor<VibrationEffect> captor = ArgumentCaptor.forClass(VibrationEffect.class); + vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), ALARM_ATTRS); + verify(mIInputManagerMock).vibrate(eq(1), captor.capture(), any()); + assertTrue(captor.getValue() instanceof VibrationEffect.Waveform); + + VibrationEffect[] effects = new VibrationEffect[]{ + VibrationEffect.createOneShot(100, 128), + VibrationEffect.createWaveform(new long[]{10}, new int[]{100}, -1), + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .compose(), + }; + + for (VibrationEffect effect : effects) { + vibrate(service, effect, ALARM_ATTRS); + verify(mIInputManagerMock).vibrate(eq(1), eq(effect), any()); + } + + // VibrationThread will start this vibration async, so wait before checking it never played. + assertFalse(waitUntil(s -> !mVibratorProviders.get(1).getEffects().isEmpty(), + service, /* timeout= */ 50)); + } + + @Test + public void vibrate_withNativeCallbackTriggered_finishesVibration() throws Exception { + mockVibrators(1); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS, + IVibrator.CAP_AMPLITUDE_CONTROL); + mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK); + VibratorManagerService service = createService(); + // The native callback will be dispatched manually in this test. + mTestLooper.stopAutoDispatchAndIgnoreExceptions(); + + vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), ALARM_ATTRS); + + // VibrationThread will start this vibration async, so wait before triggering callbacks. + assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + + // Trigger callbacks from controller. + mTestLooper.moveTimeForward(50); + mTestLooper.dispatchAll(); + + // VibrationThread needs some time to react to native callbacks and stop the vibrator. + assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + } + + + @Test + public void vibrate_withIntensitySettings_appliesSettingsToScaleVibrations() throws Exception { + mVibrator.setDefaultNotificationVibrationIntensity(Vibrator.VIBRATION_INTENSITY_LOW); + setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, + Vibrator.VIBRATION_INTENSITY_HIGH); + setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, + Vibrator.VIBRATION_INTENSITY_LOW); + setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, + Vibrator.VIBRATION_INTENSITY_OFF); + + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL, + IVibrator.CAP_COMPOSE_EFFECTS); + fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); + VibratorManagerService service = createService(); + + vibrate(service, CombinedVibrationEffect.startSynced() + .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) + .combine(), ALARM_ATTRS); + assertTrue(waitUntil(s -> fakeVibrator.getEffects().size() == 1, + service, TEST_TIMEOUT_MILLIS)); + + vibrate(service, CombinedVibrationEffect.startSequential() + .addNext(1, VibrationEffect.createOneShot(20, 100)) + .combine(), NOTIFICATION_ATTRS); + assertTrue(waitUntil(s -> fakeVibrator.getEffects().size() == 2, + service, TEST_TIMEOUT_MILLIS)); + + vibrate(service, VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f) + .compose(), HAPTIC_FEEDBACK_ATTRS); + assertTrue(waitUntil(s -> fakeVibrator.getEffects().size() == 3, + service, TEST_TIMEOUT_MILLIS)); + + vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), RINGTONE_ATTRS); + + assertEquals(3, fakeVibrator.getEffects().size()); + assertEquals(1, fakeVibrator.getAmplitudes().size()); + + // Alarm vibration is always VIBRATION_INTENSITY_HIGH. + VibrationEffect expected = new VibrationEffect.Prebaked(VibrationEffect.EFFECT_CLICK, false, + VibrationEffect.EFFECT_STRENGTH_STRONG); + assertEquals(expected, fakeVibrator.getEffects().get(0)); + + // Notification vibrations will be scaled with SCALE_VERY_HIGH. + assertTrue(150 < fakeVibrator.getAmplitudes().get(0)); + + // Haptic feedback vibrations will be scaled with SCALE_LOW. + VibrationEffect.Composed played = + (VibrationEffect.Composed) fakeVibrator.getEffects().get(2); + assertTrue(0.5 < played.getPrimitiveEffects().get(0).scale); + assertTrue(0.5 > played.getPrimitiveEffects().get(1).scale); + + // Ring vibrations have intensity OFF and are not played. + } + + @Test + public void vibrate_withPowerModeChange_cancelVibrationIfNotAllowed() throws Exception { + mockVibrators(1, 2); + VibratorManagerService service = createService(); + vibrate(service, + CombinedVibrationEffect.startSynced() + .addVibrator(1, VibrationEffect.createOneShot(1000, 100)) + .combine(), + HAPTIC_FEEDBACK_ATTRS); + + assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + + mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE); + + // Haptic feedback cancelled on low power mode. + assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + } + + @Test + public void vibrate_withSettingsChange_doNotCancelVibration() throws Exception { + mockVibrators(1); + VibratorManagerService service = createService(); + + vibrate(service, VibrationEffect.createOneShot(1000, 100), HAPTIC_FEEDBACK_ATTRS); + assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + + service.updateServiceState(); + // Vibration is not stopped nearly after updating service. + assertFalse(waitUntil(s -> !s.isVibrating(1), service, 50)); + } + + @Test + public void cancelVibrate_stopsVibrating() throws Exception { + mockVibrators(1); + VibratorManagerService service = createService(); + + service.cancelVibrate(service); + assertFalse(service.isVibrating(1)); + + vibrate(service, VibrationEffect.createOneShot(10_000, 100), ALARM_ATTRS); + assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + + service.cancelVibrate(service); + assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); } private void mockVibrators(int... vibratorIds) { @@ -302,8 +690,56 @@ public class VibratorManagerServiceTest { when(mNativeWrapperMock.getVibratorIds()).thenReturn(vibratorIds); } + private IVibratorStateListener mockVibratorStateListener() { + IVibratorStateListener listenerMock = mock(IVibratorStateListener.class); + IBinder binderMock = mock(IBinder.class); + when(listenerMock.asBinder()).thenReturn(binderMock); + return listenerMock; + } + + private InputDevice createInputDeviceWithVibrator(int id) { + return new InputDevice(id, 0, 0, "name", 0, 0, "description", false, 0, 0, + null, /* hasVibrator= */ true, false, false, false, false); + } + private static <T> void addLocalServiceMock(Class<T> clazz, T mock) { LocalServices.removeServiceForTest(clazz); LocalServices.addService(clazz, mock); } + + private void setRingerMode(int ringerMode) { + AudioManager audioManager = mContextSpy.getSystemService(AudioManager.class); + audioManager.setRingerModeInternal(ringerMode); + assertEquals(ringerMode, audioManager.getRingerModeInternal()); + } + + private void setUserSetting(String settingName, int value) { + Settings.System.putIntForUser( + mContextSpy.getContentResolver(), settingName, value, UserHandle.USER_CURRENT); + } + + private void setGlobalSetting(String settingName, int value) { + Settings.Global.putInt(mContextSpy.getContentResolver(), settingName, value); + } + + private void vibrate(VibratorManagerService service, VibrationEffect effect, + VibrationAttributes attrs) { + vibrate(service, CombinedVibrationEffect.createSynced(effect), attrs); + } + + private void vibrate(VibratorManagerService service, CombinedVibrationEffect effect, + VibrationAttributes attrs) { + service.vibrate(UID, PACKAGE_NAME, effect, attrs, "some reason", service); + } + + private boolean waitUntil(Predicate<VibratorManagerService> predicate, + VibratorManagerService service, long timeout) throws InterruptedException { + long timeoutTimestamp = SystemClock.uptimeMillis() + timeout; + boolean predicateResult = false; + while (!predicateResult && SystemClock.uptimeMillis() < timeoutTimestamp) { + Thread.sleep(10); + predicateResult = predicate.test(service); + } + return predicateResult; + } } diff --git a/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java b/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java index 2932926b0b05..2a7905a451b9 100644 --- a/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java @@ -582,16 +582,19 @@ public class VibratorServiceTest { VibratorService service = createService(); service.registerVibratorStateListener(mVibratorStateListenerMock); - verify(mVibratorStateListenerMock).onVibrating(false); - vibrate(service, VibrationEffect.createOneShot(100, 100), ALARM_ATTRS); + vibrate(service, VibrationEffect.createOneShot(30, 100), ALARM_ATTRS); // VibrationThread will start this vibration async, so wait before triggering callbacks. Thread.sleep(10); + assertTrue(service.isVibrating()); + service.unregisterVibratorStateListener(mVibratorStateListenerMock); // Trigger callbacks from controller. - mTestLooper.moveTimeForward(150); + mTestLooper.moveTimeForward(50); mTestLooper.dispatchAll(); + Thread.sleep(20); + assertFalse(service.isVibrating()); InOrder inOrderVerifier = inOrder(mVibratorStateListenerMock); // First notification done when listener is registered. |