Create a hidden Vibrator API for haptic feedback.
Haptic feedbacks are played on a View (via the performHapticFeedback API
in the View class). This API used to go through the window manager code
space to derive the vibration effect/attribute for a given haptic
feedback ID, and use the vibrator to play the corresponding vibration.
To remove dependency on window manager, we are creating a hidden API in
Vibrator that will handle performing haptic feedback. View can pass a
haptic feedback request directly to this new Vibrator API (after making
sure that the View is attached to a window), and this API will handle
creating the vibration effect/attribute for the haptic feedback and
playing it, all without requiring the VIBRATE permission.
We are guarding View's direct call to the Vibrator API with a new
vibrator flag. Once this implementation has been seen to be stable, we
will remove the haptic feedback code in the window manager code space.
Test: atest VibratorManagerServiceTest
Test: atest HapticFeedbackVibrationProviderTest
Test: sample test app without VIBRATE permission plays haptic feedback
Bug: 295459081
Change-Id: Ic4d226f724bf0591bad9f465a4872a41ddca2c60
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 5cfdeb9..e9fcad8 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -19,6 +19,7 @@
// Add java_aconfig_libraries to here to add them to the core framework
srcs: [
":android.os.flags-aconfig-java{.generated_srcjars}",
+ ":android.os.vibrator.flags-aconfig-java{.generated_srcjars}",
":android.security.flags-aconfig-java{.generated_srcjars}",
":camera_platform_flags_core_java_lib{.generated_srcjars}",
":com.android.window.flags.window-aconfig-java{.generated_srcjars}",
@@ -138,3 +139,16 @@
aconfig_declarations: "android.view.inputmethod.flags-aconfig",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+
+// Vibrator
+aconfig_declarations {
+ name: "android.os.vibrator.flags-aconfig",
+ package: "android.os.vibrator",
+ srcs: ["core/java/android/os/vibrator/*.aconfig"],
+}
+
+java_aconfig_library {
+ name: "android.os.vibrator.flags-aconfig-java",
+ aconfig_declarations: "android.os.vibrator.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/core/java/android/os/IVibratorManagerService.aidl b/core/java/android/os/IVibratorManagerService.aidl
index 6275352..f30dd20 100644
--- a/core/java/android/os/IVibratorManagerService.aidl
+++ b/core/java/android/os/IVibratorManagerService.aidl
@@ -36,4 +36,10 @@
void vibrate(int uid, int displayId, String opPkg, in CombinedVibration vibration,
in VibrationAttributes attributes, String reason, IBinder token);
void cancelVibrate(int usageFilter, IBinder token);
+
+ // Async oneway APIs.
+ // There is no order guarantee with respect to the two-way APIs above like
+ // vibrate/isVibrating/cancel.
+ oneway void performHapticFeedback(int uid, int displayId, String opPkg, int constant,
+ boolean always, String reason, IBinder token);
}
diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java
index 1cd0f3b..04c257b 100644
--- a/core/java/android/os/SystemVibrator.java
+++ b/core/java/android/os/SystemVibrator.java
@@ -206,6 +206,15 @@
}
@Override
+ public void performHapticFeedback(int constant, boolean always, String reason) {
+ if (mVibratorManager == null) {
+ Log.w(TAG, "Failed to perform haptic feedback; no vibrator manager.");
+ return;
+ }
+ mVibratorManager.performHapticFeedback(constant, always, reason);
+ }
+
+ @Override
public void cancel() {
if (mVibratorManager == null) {
Log.w(TAG, "Failed to cancel vibrate; no vibrator manager.");
diff --git a/core/java/android/os/SystemVibratorManager.java b/core/java/android/os/SystemVibratorManager.java
index 284b246..ee90834 100644
--- a/core/java/android/os/SystemVibratorManager.java
+++ b/core/java/android/os/SystemVibratorManager.java
@@ -145,6 +145,21 @@
}
@Override
+ public void performHapticFeedback(int constant, boolean always, String reason) {
+ if (mService == null) {
+ Log.w(TAG, "Failed to perform haptic feedback; no vibrator manager service.");
+ return;
+ }
+ try {
+ mService.performHapticFeedback(
+ Process.myUid(), mContext.getAssociatedDisplayId(), mPackageName, constant,
+ always, reason, mToken);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to perform haptic feedback.", e);
+ }
+ }
+
+ @Override
public void cancel() {
cancelVibration(VibrationAttributes.USAGE_FILTER_MATCH_ALL);
}
@@ -228,6 +243,11 @@
}
@Override
+ public void performHapticFeedback(int effectId, boolean always, String reason) {
+ SystemVibratorManager.this.performHapticFeedback(effectId, always, reason);
+ }
+
+ @Override
public void cancel() {
SystemVibratorManager.this.cancel();
}
diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java
index aafa501..99c9925 100644
--- a/core/java/android/os/Vibrator.java
+++ b/core/java/android/os/Vibrator.java
@@ -510,6 +510,28 @@
String reason, @NonNull VibrationAttributes attributes);
/**
+ * Performs a haptic feedback.
+ *
+ * <p>A haptic feedback is a short vibration feedback. The type of feedback is identified via
+ * the {@code constant}, which should be one of the effect constants provided in
+ * {@link HapticFeedbackConstants}. The haptic feedback provided for a given effect ID is
+ * consistent across all usages on the same device.
+ *
+ * @param constant the ID for the haptic feedback. This should be one of the constants defined
+ * in {@link HapticFeedbackConstants}.
+ * @param always {@code true} if the haptic feedback should be played regardless of the user
+ * vibration intensity settings applicable to the corresponding vibration.
+ * {@code false} if the vibration for the haptic feedback should respect the applicable
+ * vibration intensity settings.
+ * @param reason the reason for this haptic feedback.
+ *
+ * @hide
+ */
+ public void performHapticFeedback(int constant, boolean always, String reason) {
+ Log.w(TAG, "performHapticFeedback is not supported");
+ }
+
+ /**
* Query whether the vibrator natively supports the given effects.
*
* <p>If an effect is not supported, the system may still automatically fall back to playing
diff --git a/core/java/android/os/VibratorManager.java b/core/java/android/os/VibratorManager.java
index f506ef8..e0b6a9f 100644
--- a/core/java/android/os/VibratorManager.java
+++ b/core/java/android/os/VibratorManager.java
@@ -35,7 +35,8 @@
public abstract class VibratorManager {
private static final String TAG = "VibratorManager";
- private final String mPackageName;
+ /** @hide */
+ protected final String mPackageName;
/**
* @hide to prevent subclassing from outside of the framework
@@ -137,6 +138,21 @@
String reason, @Nullable VibrationAttributes attributes);
/**
+ * Performs a haptic feedback.
+ *
+ * @param constant the ID of the requested haptic feedback. Should be one of the constants
+ * defined in {@link HapticFeedbackConstants}.
+ * @param always {@code true} if the haptic feedback should be played regardless of the user
+ * vibration intensity settings applicable to the corresponding vibration.
+ * {@code false} otherwise.
+ * @param reason the reason for this haptic feedback.
+ * @hide
+ */
+ public void performHapticFeedback(int constant, boolean always, String reason) {
+ Log.w(TAG, "performHapticFeedback is not supported");
+ }
+
+ /**
* Turn all the vibrators off.
*/
@RequiresPermission(android.Manifest.permission.VIBRATE)
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
new file mode 100644
index 0000000..361e244
--- /dev/null
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.os.vibrator"
+
+flag {
+ namespace: "vibrator"
+ name: "use_vibrator_haptic_feedback"
+ description: "Enables performHapticFeedback to directly use the vibrator service instead of going through the window session"
+ bug: "295459081"
+}
\ No newline at end of file
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 9a4cb72..30fd2cf 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -105,6 +105,8 @@
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.Trace;
+import android.os.Vibrator;
+import android.os.vibrator.Flags;
import android.sysprop.DisplayProperties;
import android.text.InputType;
import android.text.TextUtils;
@@ -5411,6 +5413,9 @@
*/
private PointerIcon mMousePointerIcon;
+ /** Vibrator for haptic feedback. */
+ private Vibrator mVibrator;
+
/**
* @hide
*/
@@ -27758,8 +27763,24 @@
&& !isHapticFeedbackEnabled()) {
return false;
}
- return mAttachInfo.mRootCallbacks.performHapticFeedback(feedbackConstant,
- (flags & HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0);
+
+ final boolean always = (flags & HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0;
+ if (Flags.useVibratorHapticFeedback()) {
+ if (!mAttachInfo.canPerformHapticFeedback()) {
+ return false;
+ }
+ getSystemVibrator().performHapticFeedback(
+ feedbackConstant, always, "View#performHapticFeedback");
+ return true;
+ }
+ return mAttachInfo.mRootCallbacks.performHapticFeedback(feedbackConstant, always);
+ }
+
+ private Vibrator getSystemVibrator() {
+ if (mVibrator != null) {
+ return mVibrator;
+ }
+ return mVibrator = mContext.getSystemService(Vibrator.class);
}
/**
@@ -31239,6 +31260,11 @@
return events;
}
+ private boolean canPerformHapticFeedback() {
+ return mSession != null
+ && (mDisplay.getFlags() & Display.FLAG_TOUCH_FEEDBACK_DISABLED) == 0;
+ }
+
@Nullable
ScrollCaptureInternal getScrollCaptureInternal() {
if (mScrollCaptureInternal != null) {
diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java b/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java
index 3fb845f..e4f9607 100644
--- a/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java
+++ b/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java
@@ -19,7 +19,7 @@
import android.annotation.Nullable;
import android.content.res.Resources;
import android.os.VibrationEffect;
-import android.os.Vibrator;
+import android.os.VibratorInfo;
import android.os.vibrator.persistence.ParsedVibration;
import android.os.vibrator.persistence.VibrationXmlParser;
import android.text.TextUtils;
@@ -107,10 +107,10 @@
* @hide
*/
@Nullable
- static SparseArray<VibrationEffect> loadVibrations(Resources res, Vibrator vibrator)
+ static SparseArray<VibrationEffect> loadVibrations(Resources res, VibratorInfo vibratorInfo)
throws CustomizationParserException, IOException {
try {
- return loadVibrationsInternal(res, vibrator);
+ return loadVibrationsInternal(res, vibratorInfo);
} catch (VibrationXmlParser.VibrationXmlParserException
| XmlParserException
| XmlPullParserException e) {
@@ -121,7 +121,7 @@
@Nullable
private static SparseArray<VibrationEffect> loadVibrationsInternal(
- Resources res, Vibrator vibrator) throws
+ Resources res, VibratorInfo vibratorInfo) throws
CustomizationParserException,
IOException,
VibrationXmlParser.VibrationXmlParserException,
@@ -175,7 +175,7 @@
throw new CustomizationParserException(
"Unable to parse vibration element for effect " + effectId);
}
- VibrationEffect effect = parsedVibration.resolve(vibrator);
+ VibrationEffect effect = parsedVibration.resolve(vibratorInfo);
if (effect != null) {
if (effect.getDuration() == Long.MAX_VALUE) {
throw new CustomizationParserException(String.format(
diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
index 7c99543..3d89afa 100644
--- a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
+++ b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
@@ -21,6 +21,7 @@
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
+import android.os.VibratorInfo;
import android.util.Slog;
import android.util.SparseArray;
import android.view.HapticFeedbackConstants;
@@ -29,8 +30,6 @@
import java.io.IOException;
import java.io.PrintWriter;
-import java.util.HashSet;
-import java.util.Set;
/**
* Provides the {@link VibrationEffect} and {@link VibrationAttributes} for haptic feedback.
@@ -47,7 +46,7 @@
private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
- private final Vibrator mVibrator;
+ private final VibratorInfo mVibratorInfo;
private final boolean mHapticTextHandleEnabled;
// Vibrator effect for haptic feedback during boot when safe mode is enabled.
private final VibrationEffect mSafeModeEnabledVibrationEffect;
@@ -58,25 +57,25 @@
/** @hide */
public HapticFeedbackVibrationProvider(Resources res, Vibrator vibrator) {
- this(res, vibrator, loadHapticCustomizations(res, vibrator));
+ this(res, vibrator.getInfo());
+ }
+
+ /** @hide */
+ public HapticFeedbackVibrationProvider(Resources res, VibratorInfo vibratorInfo) {
+ this(res, vibratorInfo, loadHapticCustomizations(res, vibratorInfo));
}
/** @hide */
@VisibleForTesting HapticFeedbackVibrationProvider(
Resources res,
- Vibrator vibrator,
+ VibratorInfo vibratorInfo,
@Nullable SparseArray<VibrationEffect> hapticCustomizations) {
- mVibrator = vibrator;
+ mVibratorInfo = vibratorInfo;
mHapticTextHandleEnabled = res.getBoolean(
com.android.internal.R.bool.config_enableHapticTextHandle);
- if (hapticCustomizations != null) {
- // Clean up the customizations to remove vibrations which may not ever be used due to
- // Vibrator properties or other device configurations.
- removeUnsupportedVibrations(hapticCustomizations, vibrator);
- if (hapticCustomizations.size() == 0) {
- hapticCustomizations = null;
- }
+ if (hapticCustomizations != null && hapticCustomizations.size() == 0) {
+ hapticCustomizations = null;
}
mHapticCustomizations = hapticCustomizations;
@@ -257,7 +256,7 @@
if (effectHasCustomization(hapticFeedbackId)) {
return mHapticCustomizations.get(hapticFeedbackId);
}
- if (mVibrator.areAllPrimitivesSupported(primitiveId)) {
+ if (mVibratorInfo.isPrimitiveSupported(primitiveId)) {
return VibrationEffect.startComposition()
.addPrimitive(primitiveId, primitiveScale)
.compose();
@@ -270,9 +269,8 @@
if (effectHasCustomization(HapticFeedbackConstants.ASSISTANT_BUTTON)) {
return mHapticCustomizations.get(HapticFeedbackConstants.ASSISTANT_BUTTON);
}
- if (mVibrator.areAllPrimitivesSupported(
- VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
- VibrationEffect.Composition.PRIMITIVE_TICK)) {
+ if (mVibratorInfo.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE)
+ && mVibratorInfo.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_TICK)) {
// quiet ramp, short pause, then sharp tick
return VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.25f)
@@ -289,27 +287,12 @@
@Nullable
private static SparseArray<VibrationEffect> loadHapticCustomizations(
- Resources res, Vibrator vibrator) {
+ Resources res, VibratorInfo vibratorInfo) {
try {
- return HapticFeedbackCustomization.loadVibrations(res, vibrator);
+ return HapticFeedbackCustomization.loadVibrations(res, vibratorInfo);
} catch (IOException | HapticFeedbackCustomization.CustomizationParserException e) {
Slog.e(TAG, "Unable to load haptic customizations.", e);
return null;
}
}
-
- private static void removeUnsupportedVibrations(
- SparseArray<VibrationEffect> customizations, Vibrator vibrator) {
- Set<Integer> keysToRemove = new HashSet<>();
- for (int i = 0; i < customizations.size(); i++) {
- int key = customizations.keyAt(i);
- if (!vibrator.areVibrationFeaturesSupported(customizations.get(key))) {
- keysToRemove.add(key);
- }
- }
-
- for (int key : keysToRemove) {
- customizations.remove(key);
- }
- }
}
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index e296c7b..92bfe29 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -28,6 +28,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
+import android.content.res.Resources;
import android.hardware.vibrator.IVibrator;
import android.os.BatteryStats;
import android.os.Binder;
@@ -131,6 +132,7 @@
private final Object mLock = new Object();
private final Context mContext;
+ private final Injector mInjector;
private final PowerManager.WakeLock mWakeLock;
private final IBatteryStats mBatteryStatsService;
private final VibratorFrameworkStatsLogger mFrameworkStatsLogger;
@@ -162,6 +164,8 @@
@GuardedBy("mLock")
@Nullable private VibratorInfo mCombinedVibratorInfo;
+ @GuardedBy("mLock")
+ @Nullable private HapticFeedbackVibrationProvider mHapticFeedbackVibrationProvider;
private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
@@ -201,6 +205,7 @@
@VisibleForTesting
VibratorManagerService(Context context, Injector injector) {
mContext = context;
+ mInjector = injector;
mHandler = injector.createHandler(Looper.myLooper());
mVibrationSettings = new VibrationSettings(mContext, mHandler);
@@ -393,7 +398,42 @@
@Override // Binder call
public void vibrate(int uid, int displayId, String opPkg, @NonNull CombinedVibration effect,
@Nullable VibrationAttributes attrs, String reason, IBinder token) {
- vibrateInternal(uid, displayId, opPkg, effect, attrs, reason, token);
+ vibrateWithPermissionCheck(uid, displayId, opPkg, effect, attrs, reason, token);
+ }
+
+ @Override // Binder call
+ public void performHapticFeedback(
+ int uid, int displayId, String opPkg, int constant, boolean always, String reason,
+ IBinder token) {
+ performHapticFeedbackInternal(uid, displayId, opPkg, constant, always, reason, token);
+ }
+
+ /**
+ * An internal-only version of performHapticFeedback that allows the caller access to the
+ * {@link HalVibration}.
+ * The Vibration is only returned if it is ongoing after this method returns.
+ */
+ @VisibleForTesting
+ @Nullable
+ HalVibration performHapticFeedbackInternal(
+ int uid, int displayId, String opPkg, int constant, boolean always, String reason,
+ IBinder token) {
+ HapticFeedbackVibrationProvider hapticVibrationProvider = getHapticVibrationProvider();
+ if (hapticVibrationProvider == null) {
+ Slog.w(TAG, "performHapticFeedback; haptic vibration provider not ready.");
+ return null;
+ }
+ VibrationEffect effect = hapticVibrationProvider.getVibrationForHapticFeedback(constant);
+ if (effect == null) {
+ Slog.w(TAG, "performHapticFeedback; vibration absent for effect " + constant);
+ return null;
+ }
+ CombinedVibration combinedVibration = CombinedVibration.createParallel(effect);
+ VibrationAttributes attrs =
+ hapticVibrationProvider.getVibrationAttributesForHapticFeedback(
+ constant, /* bypassVibrationIntensitySetting= */ always);
+ return vibrateWithoutPermissionCheck(uid, displayId, opPkg, combinedVibration, attrs,
+ "performHapticFeedback: " + reason, token);
}
/**
@@ -403,93 +443,110 @@
*/
@VisibleForTesting
@Nullable
- HalVibration vibrateInternal(int uid, int displayId, String opPkg,
+ HalVibration vibrateWithPermissionCheck(int uid, int displayId, String opPkg,
@NonNull CombinedVibration effect, @Nullable VibrationAttributes attrs,
String reason, IBinder token) {
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 null;
- }
- enforceUpdateAppOpsStatsPermission(uid);
- if (!isEffectValid(effect)) {
- return null;
- }
- attrs = fixupVibrationAttributes(attrs, effect);
- // Create Vibration.Stats as close to the received request as possible, for tracking.
- HalVibration vib = new HalVibration(token, effect,
- new Vibration.CallerInfo(attrs, uid, displayId, opPkg, reason));
- fillVibrationFallbacks(vib, effect);
-
- if (attrs.isFlagSet(VibrationAttributes.FLAG_INVALIDATE_SETTINGS_CACHE)) {
- // Force update of user settings before checking if this vibration effect should
- // be ignored or scaled.
- mVibrationSettings.update();
- }
-
- synchronized (mLock) {
- if (DEBUG) {
- Slog.d(TAG, "Starting vibrate for vibration " + vib.id);
- }
-
- // Check if user settings or DnD is set to ignore this vibration.
- Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationLocked(vib.callerInfo);
-
- // Check if ongoing vibration is more important than this vibration.
- if (vibrationEndInfo == null) {
- vibrationEndInfo = shouldIgnoreVibrationForOngoingLocked(vib);
- }
-
- // If not ignored so far then try to start this vibration.
- if (vibrationEndInfo == null) {
- final long ident = Binder.clearCallingIdentity();
- try {
- if (mCurrentExternalVibration != null) {
- mCurrentExternalVibration.mute();
- vib.stats.reportInterruptedAnotherVibration(
- mCurrentExternalVibration.callerInfo);
- endExternalVibrateLocked(
- new Vibration.EndInfo(Vibration.Status.CANCELLED_SUPERSEDED,
- vib.callerInfo),
- /* continueExternalControl= */ false);
- } else if (mCurrentVibration != null) {
- if (mCurrentVibration.getVibration().canPipelineWith(vib)) {
- // Don't cancel the current vibration if it's pipeline-able.
- // Note that if there is a pending next vibration that can't be
- // pipelined, it will have already cancelled the current one, so we
- // don't need to consider it here as well.
- if (DEBUG) {
- Slog.d(TAG, "Pipelining vibration " + vib.id);
- }
- } else {
- vib.stats.reportInterruptedAnotherVibration(
- mCurrentVibration.getVibration().callerInfo);
- mCurrentVibration.notifyCancelled(
- new Vibration.EndInfo(Vibration.Status.CANCELLED_SUPERSEDED,
- vib.callerInfo),
- /* immediate= */ false);
- }
- }
- vibrationEndInfo = startVibrationLocked(vib);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- }
-
- // Ignored or failed to start the vibration, end it and report metrics right away.
- if (vibrationEndInfo != null) {
- endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ true);
- }
- return vib;
- }
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.VIBRATE, "vibrate");
+ return vibrateInternal(uid, displayId, opPkg, effect, attrs, reason, token);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
+ HalVibration vibrateWithoutPermissionCheck(int uid, int displayId, String opPkg,
+ @NonNull CombinedVibration effect, @Nullable VibrationAttributes attrs,
+ String reason, IBinder token) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate no perm check, reason = " + reason);
+ try {
+ return vibrateInternal(uid, displayId, opPkg, effect, attrs, reason, token);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ }
+ }
+
+ private HalVibration vibrateInternal(int uid, int displayId, String opPkg,
+ @NonNull CombinedVibration effect, @Nullable VibrationAttributes attrs,
+ String reason, IBinder token) {
+ if (token == null) {
+ Slog.e(TAG, "token must not be null");
+ return null;
+ }
+ enforceUpdateAppOpsStatsPermission(uid);
+ if (!isEffectValid(effect)) {
+ return null;
+ }
+ attrs = fixupVibrationAttributes(attrs, effect);
+ // Create Vibration.Stats as close to the received request as possible, for tracking.
+ HalVibration vib = new HalVibration(token, effect,
+ new Vibration.CallerInfo(attrs, uid, displayId, opPkg, reason));
+ fillVibrationFallbacks(vib, effect);
+
+ if (attrs.isFlagSet(VibrationAttributes.FLAG_INVALIDATE_SETTINGS_CACHE)) {
+ // Force update of user settings before checking if this vibration effect should
+ // be ignored or scaled.
+ mVibrationSettings.update();
+ }
+
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slog.d(TAG, "Starting vibrate for vibration " + vib.id);
+ }
+
+ // Check if user settings or DnD is set to ignore this vibration.
+ Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationLocked(vib.callerInfo);
+
+ // Check if ongoing vibration is more important than this vibration.
+ if (vibrationEndInfo == null) {
+ vibrationEndInfo = shouldIgnoreVibrationForOngoingLocked(vib);
+ }
+
+ // If not ignored so far then try to start this vibration.
+ if (vibrationEndInfo == null) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ if (mCurrentExternalVibration != null) {
+ mCurrentExternalVibration.mute();
+ vib.stats.reportInterruptedAnotherVibration(
+ mCurrentExternalVibration.callerInfo);
+ endExternalVibrateLocked(
+ new Vibration.EndInfo(Vibration.Status.CANCELLED_SUPERSEDED,
+ vib.callerInfo),
+ /* continueExternalControl= */ false);
+ } else if (mCurrentVibration != null) {
+ if (mCurrentVibration.getVibration().canPipelineWith(vib)) {
+ // Don't cancel the current vibration if it's pipeline-able.
+ // Note that if there is a pending next vibration that can't be
+ // pipelined, it will have already cancelled the current one, so we
+ // don't need to consider it here as well.
+ if (DEBUG) {
+ Slog.d(TAG, "Pipelining vibration " + vib.id);
+ }
+ } else {
+ vib.stats.reportInterruptedAnotherVibration(
+ mCurrentVibration.getVibration().callerInfo);
+ mCurrentVibration.notifyCancelled(
+ new Vibration.EndInfo(Vibration.Status.CANCELLED_SUPERSEDED,
+ vib.callerInfo),
+ /* immediate= */ false);
+ }
+ }
+ vibrationEndInfo = startVibrationLocked(vib);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ // Ignored or failed to start the vibration, end it and report metrics right away.
+ if (vibrationEndInfo != null) {
+ endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ true);
+ }
+ return vib;
+ }
+ }
+
@Override // Binder call
public void cancelVibrate(int usageFilter, IBinder token) {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "cancelVibrate");
@@ -1315,6 +1372,11 @@
return new VibratorController(vibratorId, listener);
}
+ HapticFeedbackVibrationProvider createHapticFeedbackVibrationProvider(
+ Resources resources, VibratorInfo vibratorInfo) {
+ return new HapticFeedbackVibrationProvider(resources, vibratorInfo);
+ }
+
void addService(String name, IBinder service) {
ServiceManager.addService(name, service);
}
@@ -1831,6 +1893,22 @@
}
}
+ private HapticFeedbackVibrationProvider getHapticVibrationProvider() {
+ synchronized (mLock) {
+ // Used a cached haptic vibration provider if one exists.
+ if (mHapticFeedbackVibrationProvider != null) {
+ return mHapticFeedbackVibrationProvider;
+ }
+ VibratorInfo combinedVibratorInfo = getCombinedVibratorInfo();
+ if (combinedVibratorInfo == null) {
+ return null;
+ }
+ return mHapticFeedbackVibrationProvider =
+ mInjector.createHapticFeedbackVibrationProvider(
+ mContext.getResources(), combinedVibratorInfo);
+ }
+ }
+
private VibratorInfo getCombinedVibratorInfo() {
synchronized (mLock) {
// Used a cached resolving vibrator if one exists.
@@ -2098,8 +2176,9 @@
// only cancel background vibrations.
IBinder deathBinder = commonOptions.background ? VibratorManagerService.this
: mShellCallbacksToken;
- HalVibration vib = vibrateInternal(Binder.getCallingUid(), Display.DEFAULT_DISPLAY,
- SHELL_PACKAGE_NAME, combined, attrs, commonOptions.description, deathBinder);
+ HalVibration vib = vibrateWithPermissionCheck(Binder.getCallingUid(),
+ Display.DEFAULT_DISPLAY, SHELL_PACKAGE_NAME, combined, attrs,
+ commonOptions.description, deathBinder);
if (vib != null && !commonOptions.background) {
try {
// Waits for the client vibration to finish, but the VibrationThread may still
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java
index 10b49c6..bc826a3 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java
@@ -30,7 +30,6 @@
import android.content.res.Resources;
import android.os.VibrationEffect;
-import android.os.Vibrator;
import android.os.VibratorInfo;
import android.util.AtomicFile;
import android.util.SparseArray;
@@ -73,12 +72,10 @@
VibrationEffect.createWaveform(new long[] {123}, new int[] {254}, -1);
@Mock private Resources mResourcesMock;
- @Mock private Vibrator mVibratorMock;
@Mock private VibratorInfo mVibratorInfoMock;
@Before
public void setUp() {
- when(mVibratorMock.getInfo()).thenReturn(mVibratorInfoMock);
when(mVibratorInfoMock.areVibrationFeaturesSupported(any())).thenReturn(true);
}
@@ -220,17 +217,17 @@
public void testParseCustomizations_noCustomizationFile_returnsNull() throws Exception {
setCustomizationFilePath("");
- assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorMock))
+ assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorInfoMock))
.isNull();
setCustomizationFilePath(null);
- assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorMock))
+ assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorInfoMock))
.isNull();
setCustomizationFilePath("non_existent_file.xml");
- assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorMock))
+ assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorInfoMock))
.isNull();
}
@@ -387,7 +384,7 @@
String xml, SparseArray<VibrationEffect> expectedCustomizations) throws Exception {
setupCustomizationFile(xml);
assertThat(expectedCustomizations.contentEquals(
- HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorMock)))
+ HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorInfoMock)))
.isTrue();
}
@@ -395,13 +392,15 @@
setupCustomizationFile(xml);
assertThrows("Expected haptic feedback customization to fail for " + xml,
CustomizationParserException.class,
- () -> HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorMock));
+ () -> HapticFeedbackCustomization.loadVibrations(
+ mResourcesMock, mVibratorInfoMock));
}
private void assertParseCustomizationsFails() throws Exception {
assertThrows("Expected haptic feedback customization to fail",
CustomizationParserException.class,
- () -> HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorMock));
+ () -> HapticFeedbackCustomization.loadVibrations(
+ mResourcesMock, mVibratorInfoMock));
}
private void setupCustomizationFile(String xml) throws Exception {
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
index cae811e..a91bd2b 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
@@ -31,8 +31,10 @@
import android.content.Context;
import android.content.res.Resources;
+import android.hardware.vibrator.IVibrator;
+import android.os.VibrationAttributes;
import android.os.VibrationEffect;
-import android.os.test.FakeVibrator;
+import android.os.VibratorInfo;
import android.util.AtomicFile;
import android.util.SparseArray;
@@ -58,7 +60,7 @@
VibrationEffect.startComposition().addPrimitive(PRIMITIVE_CLICK, 0.3497f).compose();
private Context mContext = InstrumentationRegistry.getContext();
- private FakeVibrator mVibrator = new FakeVibrator(mContext);
+ private VibratorInfo mVibratorInfo = VibratorInfo.EMPTY_VIBRATOR_INFO;
@Mock private Resources mResourcesMock;
@@ -66,14 +68,14 @@
public void testNonExistentCustomization_useDefault() throws Exception {
// No customization file is set.
HapticFeedbackVibrationProvider hapticProvider =
- new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator);
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibratorInfo);
assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
.isEqualTo(VibrationEffect.get(EFFECT_TICK));
// The customization file specifies no customization.
setupCustomizationFile("<haptic-feedback-constants></haptic-feedback-constants>");
- hapticProvider = new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator);
+ hapticProvider = new HapticFeedbackVibrationProvider(mResourcesMock, mVibratorInfo);
assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
.isEqualTo(VibrationEffect.get(EFFECT_TICK));
@@ -83,7 +85,7 @@
public void testExceptionParsingCustomizations_useDefault() throws Exception {
setupCustomizationFile("<bad-xml></bad-xml>");
HapticFeedbackVibrationProvider hapticProvider =
- new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator);
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibratorInfo);
assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
.isEqualTo(VibrationEffect.get(EFFECT_TICK));
@@ -96,7 +98,7 @@
customizations.put(CONTEXT_CLICK, PRIMITIVE_CLICK_EFFECT);
HapticFeedbackVibrationProvider hapticProvider =
- new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibratorInfo, customizations);
// The override for `CONTEXT_CLICK` is used.
assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
@@ -109,11 +111,15 @@
@Test
public void testDoNotUseInvalidCustomizedVibration() throws Exception {
mockVibratorPrimitiveSupport(new int[] {});
- SparseArray<VibrationEffect> customizations = new SparseArray<>();
- customizations.put(CONTEXT_CLICK, PRIMITIVE_CLICK_EFFECT);
+ String xml = "<haptic-feedback-constants>"
+ + "<constant id=\"" + CONTEXT_CLICK + "\">"
+ + PRIMITIVE_CLICK_EFFECT
+ + "</constant>"
+ + "</haptic-feedback-constants>";
+ setupCustomizationFile(xml);
HapticFeedbackVibrationProvider hapticProvider =
- new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibratorInfo);
// The override for `CONTEXT_CLICK` is not used because the vibration is not supported.
assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
@@ -132,14 +138,14 @@
// Test with a customization available for `TEXT_HANDLE_MOVE`.
HapticFeedbackVibrationProvider hapticProvider =
- new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibratorInfo, customizations);
assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE)).isNull();
// Test with no customization available for `TEXT_HANDLE_MOVE`.
hapticProvider =
new HapticFeedbackVibrationProvider(
- mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
+ mResourcesMock, mVibratorInfo, /* hapticCustomizations= */ null);
assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE)).isNull();
}
@@ -153,7 +159,7 @@
// Test with a customization available for `TEXT_HANDLE_MOVE`.
HapticFeedbackVibrationProvider hapticProvider =
- new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibratorInfo, customizations);
assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE))
.isEqualTo(PRIMITIVE_CLICK_EFFECT);
@@ -161,7 +167,7 @@
// Test with no customization available for `TEXT_HANDLE_MOVE`.
hapticProvider =
new HapticFeedbackVibrationProvider(
- mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
+ mResourcesMock, mVibratorInfo, /* hapticCustomizations= */ null);
assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE))
.isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK));
@@ -176,14 +182,14 @@
customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT);
HapticFeedbackVibrationProvider hapticProvider =
- new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibratorInfo, customizations);
assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
.isEqualTo(PRIMITIVE_CLICK_EFFECT);
mockSafeModeEnabledVibration(null);
hapticProvider =
- new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibratorInfo, customizations);
assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
.isEqualTo(PRIMITIVE_CLICK_EFFECT);
@@ -193,20 +199,9 @@
public void testNoValidCustomizationPresentForSafeModeEnabled_resourceBasedVibrationUsed()
throws Exception {
mockSafeModeEnabledVibration(10, 20, 30, 40);
- SparseArray<VibrationEffect> customizations = new SparseArray<>();
- customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT);
-
- // Test with a customization that is not supported by the vibrator.
HapticFeedbackVibrationProvider hapticProvider =
- new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
-
- assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
- .isEqualTo(VibrationEffect.createWaveform(new long[] {10, 20, 30, 40}, -1));
-
- // Test with no customizations.
- hapticProvider =
new HapticFeedbackVibrationProvider(
- mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
+ mResourcesMock, mVibratorInfo, /* hapticCustomizations= */ null);
assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
.isEqualTo(VibrationEffect.createWaveform(new long[] {10, 20, 30, 40}, -1));
@@ -216,25 +211,44 @@
public void testNoValidCustomizationAndResourcePresentForSafeModeEnabled_noVibrationUsed()
throws Exception {
mockSafeModeEnabledVibration(null);
- SparseArray<VibrationEffect> customizations = new SparseArray<>();
- customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT);
-
- // Test with a customization that is not supported by the vibrator.
HapticFeedbackVibrationProvider hapticProvider =
- new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
-
- assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)).isNull();
-
- // Test with no customizations.
- hapticProvider =
new HapticFeedbackVibrationProvider(
- mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
+ mResourcesMock, mVibratorInfo, /* hapticCustomizations= */ null);
assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)).isNull();
}
+ @Test
+ public void testVibrationAttribute_forNotBypassingIntensitySettings() {
+ HapticFeedbackVibrationProvider hapticProvider =
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibratorInfo);
+
+ VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
+ SAFE_MODE_ENABLED, /* bypassVibrationIntensitySetting= */ false);
+
+ assertThat(attrs.getFlags() & VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF)
+ .isEqualTo(0);
+ }
+
+ @Test
+ public void testVibrationAttribute_forByassingIntensitySettings() {
+ HapticFeedbackVibrationProvider hapticProvider =
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibratorInfo);
+
+ VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
+ SAFE_MODE_ENABLED, /* bypassVibrationIntensitySetting= */ true);
+
+ assertThat(attrs.getFlags() & VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF)
+ .isNotEqualTo(0);
+ }
+
private void mockVibratorPrimitiveSupport(int... supportedPrimitives) {
- mVibrator = new FakeVibrator(mContext, supportedPrimitives);
+ VibratorInfo.Builder builder = new VibratorInfo.Builder(/* id= */ 1)
+ .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+ for (int primitive : supportedPrimitives) {
+ builder.setSupportedPrimitive(primitive, 10);
+ }
+ mVibratorInfo = builder.build();
}
private void mockHapticTextSupport(boolean supported) {
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index 4e3a893..c25f0cb 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -22,6 +22,7 @@
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -30,6 +31,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -45,6 +47,7 @@
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.PackageManagerInternal;
+import android.content.res.Resources;
import android.hardware.input.IInputManager;
import android.hardware.input.InputManager;
import android.hardware.input.InputManagerGlobal;
@@ -79,8 +82,10 @@
import android.os.vibrator.VibrationEffectSegment;
import android.provider.Settings;
import android.util.ArraySet;
+import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.view.Display;
+import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import androidx.test.InstrumentationRegistry;
@@ -169,6 +174,8 @@
private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>();
+ private SparseArray<VibrationEffect> mHapticFeedbackVibrationMap = new SparseArray<>();
+
private VibratorManagerService mService;
private Context mContextSpy;
private TestLooper mTestLooper;
@@ -309,6 +316,12 @@
mExternalVibratorService =
(VibratorManagerService.ExternalVibratorService) serviceInstance;
}
+
+ HapticFeedbackVibrationProvider createHapticFeedbackVibrationProvider(
+ Resources resources, VibratorInfo vibratorInfo) {
+ return new HapticFeedbackVibrationProvider(
+ resources, vibratorInfo, mHapticFeedbackVibrationMap);
+ }
});
return mService;
}
@@ -623,6 +636,18 @@
}
@Test
+ public void vibrate_withoutVibratePermission_throwsSecurityException() {
+ denyPermission(android.Manifest.permission.VIBRATE);
+ VibratorManagerService service = createSystemReadyService();
+
+ assertThrows("Expected vibrating without permission to fail!",
+ SecurityException.class,
+ () -> vibrate(service,
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK),
+ VibrationAttributes.createForUsage(VibrationAttributes.USAGE_TOUCH)));
+ }
+
+ @Test
public void vibrate_withRingtone_usesRingerModeSettings() throws Exception {
mockVibrators(1);
FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
@@ -1274,6 +1299,60 @@
}
@Test
+ public void performHapticFeedback_doesNotRequirePermission() throws Exception {
+ denyPermission(android.Manifest.permission.VIBRATE);
+ mHapticFeedbackVibrationMap.put(
+ HapticFeedbackConstants.KEYBOARD_TAP,
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
+ mockVibrators(1);
+ FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+ fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+ VibratorManagerService service = createSystemReadyService();
+
+ HalVibration vibration =
+ performHapticFeedbackAndWaitUntilFinished(
+ service, HapticFeedbackConstants.KEYBOARD_TAP, /* always= */ true);
+
+ List<VibrationEffectSegment> playedSegments = fakeVibrator.getAllEffectSegments();
+ assertEquals(1, playedSegments.size());
+ PrebakedSegment segment = (PrebakedSegment) playedSegments.get(0);
+ assertEquals(VibrationEffect.EFFECT_CLICK, segment.getEffectId());
+ assertEquals(VibrationAttributes.USAGE_TOUCH, vibration.callerInfo.attrs.getUsage());
+ }
+
+ @Test
+ public void performHapticFeedback_doesNotVibrateWhenVibratorInfoNotReady() throws Exception {
+ denyPermission(android.Manifest.permission.VIBRATE);
+ mHapticFeedbackVibrationMap.put(
+ HapticFeedbackConstants.KEYBOARD_TAP,
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
+ mockVibrators(1);
+ FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+ fakeVibrator.setVibratorInfoLoadSuccessful(false);
+ fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+ VibratorManagerService service = createService();
+
+ performHapticFeedbackAndWaitUntilFinished(
+ service, HapticFeedbackConstants.KEYBOARD_TAP, /* always= */ true);
+
+ assertTrue(fakeVibrator.getAllEffectSegments().isEmpty());
+ }
+
+ @Test
+ public void performHapticFeedback_doesNotVibrateForInvalidConstant() throws Exception {
+ denyPermission(android.Manifest.permission.VIBRATE);
+ mockVibrators(1);
+ VibratorManagerService service = createSystemReadyService();
+
+ // These are bad haptic feedback IDs, so expect no vibration played.
+ performHapticFeedbackAndWaitUntilFinished(service, /* constant= */ -1, /* always= */ false);
+ performHapticFeedbackAndWaitUntilFinished(
+ service, HapticFeedbackConstants.NO_HAPTICS, /* always= */ true);
+
+ assertTrue(mVibratorProviders.get(1).getAllEffectSegments().isEmpty());
+ }
+
+ @Test
public void vibrate_withIntensitySettings_appliesSettingsToScaleVibrations() throws Exception {
int defaultNotificationIntensity =
mVibrator.getDefaultVibrationIntensity(VibrationAttributes.USAGE_NOTIFICATION);
@@ -2231,6 +2310,18 @@
mContextSpy.getContentResolver(), settingName, value, UserHandle.USER_CURRENT);
}
+ private HalVibration performHapticFeedbackAndWaitUntilFinished(VibratorManagerService service,
+ int constant, boolean always) throws InterruptedException {
+ HalVibration vib =
+ service.performHapticFeedbackInternal(UID, Display.DEFAULT_DISPLAY, PACKAGE_NAME,
+ constant, always, "some reason", service);
+ if (vib != null) {
+ vib.waitForEnd();
+ }
+
+ return vib;
+ }
+
private void vibrateAndWaitUntilFinished(VibratorManagerService service, VibrationEffect effect,
VibrationAttributes attrs) throws InterruptedException {
vibrateAndWaitUntilFinished(service, CombinedVibration.createParallel(effect), attrs);
@@ -2239,8 +2330,8 @@
private void vibrateAndWaitUntilFinished(VibratorManagerService service,
CombinedVibration effect, VibrationAttributes attrs) throws InterruptedException {
HalVibration vib =
- service.vibrateInternal(UID, Display.DEFAULT_DISPLAY, PACKAGE_NAME, effect, attrs,
- "some reason", service);
+ service.vibrateWithPermissionCheck(UID, Display.DEFAULT_DISPLAY, PACKAGE_NAME,
+ effect, attrs, "some reason", service);
if (vib != null) {
vib.waitForEnd();
}
@@ -2271,4 +2362,9 @@
}
return predicateResult;
}
+
+ private void denyPermission(String permission) {
+ doThrow(new SecurityException()).when(mContextSpy)
+ .enforceCallingOrSelfPermission(eq(permission), anyString());
+ }
}