summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Lais Andrade <lsandrade@google.com> 2021-02-02 17:15:44 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2021-02-02 17:15:44 +0000
commit14edd62216032f8756e7d7327328bdb96ed18b5b (patch)
tree3a40852d043cb803ff76c67779be4a947ca88e70
parent277cfb354dc76da33cb7c2cfaa914cedf73e130c (diff)
parent502e1ae7b76c942c1071450299c9e2f28e747d70 (diff)
Merge "Implement vibrate and cancelVibrate on VibratorManagerService" into sc-dev
-rw-r--r--core/java/android/os/CombinedVibrationEffect.java29
-rw-r--r--core/java/android/os/IVibratorManagerService.aidl4
-rw-r--r--core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java55
-rw-r--r--services/core/java/com/android/server/VibratorManagerService.java482
-rw-r--r--services/core/java/com/android/server/vibrator/Vibration.java5
-rw-r--r--services/core/java/com/android/server/vibrator/VibrationThread.java8
-rw-r--r--services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java468
-rw-r--r--services/tests/servicestests/src/com/android/server/VibratorServiceTest.java9
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.