diff options
14 files changed, 452 insertions, 160 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 5cfdeb92c689..e9fcad882749 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -19,6 +19,7 @@ java_defaults { // 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 @@ java_aconfig_library { 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 62753527929c..f30dd20d7087 100644 --- a/core/java/android/os/IVibratorManagerService.aidl +++ b/core/java/android/os/IVibratorManagerService.aidl @@ -36,4 +36,10 @@ interface IVibratorManagerService { 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 1cd0f3b156c2..04c257b92e29 100644 --- a/core/java/android/os/SystemVibrator.java +++ b/core/java/android/os/SystemVibrator.java @@ -206,6 +206,15 @@ public class SystemVibrator extends Vibrator { } @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 284b2464c468..ee90834c15ef 100644 --- a/core/java/android/os/SystemVibratorManager.java +++ b/core/java/android/os/SystemVibratorManager.java @@ -145,6 +145,21 @@ public class SystemVibratorManager extends VibratorManager { } @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 @@ public class SystemVibratorManager extends VibratorManager { } @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 aafa5018af10..99c9925d9cb7 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -510,6 +510,28 @@ public abstract class Vibrator { 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 f506ef8955d4..e0b6a9fd28f0 100644 --- a/core/java/android/os/VibratorManager.java +++ b/core/java/android/os/VibratorManager.java @@ -35,7 +35,8 @@ import android.util.Log; 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 @@ public abstract class VibratorManager { 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 000000000000..361e244ac876 --- /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 9a4cb7298ee6..30fd2cfa5f28 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -105,6 +105,8 @@ import android.os.RemoteCallback; 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 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private PointerIcon mMousePointerIcon; + /** Vibrator for haptic feedback. */ + private Vibrator mVibrator; + /** * @hide */ @@ -27758,8 +27763,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback, && !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 @@ public class View implements Drawable.Callback, KeyEvent.Callback, 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 3fb845f064e3..e4f960763d54 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 @@ package com.android.server.vibrator; 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 @@ final class HapticFeedbackCustomization { * @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 @@ final class HapticFeedbackCustomization { @Nullable private static SparseArray<VibrationEffect> loadVibrationsInternal( - Resources res, Vibrator vibrator) throws + Resources res, VibratorInfo vibratorInfo) throws CustomizationParserException, IOException, VibrationXmlParser.VibrationXmlParserException, @@ -175,7 +175,7 @@ final class HapticFeedbackCustomization { 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 7c9954391d8c..3d89afaae88f 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.content.res.Resources; 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 com.android.internal.annotations.VisibleForTesting; 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 @@ public final class HapticFeedbackVibrationProvider { 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 @@ public final class HapticFeedbackVibrationProvider { /** @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 @@ public final class HapticFeedbackVibrationProvider { 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 @@ public final class HapticFeedbackVibrationProvider { 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 @@ public final class HapticFeedbackVibrationProvider { @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 e296c7b764e5..92bfe2951600 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.Context; 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 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { 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 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @GuardedBy("mLock") @Nullable private VibratorInfo mCombinedVibratorInfo; + @GuardedBy("mLock") + @Nullable private HapticFeedbackVibrationProvider mHapticFeedbackVibrationProvider; private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { @Override @@ -201,6 +205,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @VisibleForTesting VibratorManagerService(Context context, Injector injector) { mContext = context; + mInjector = injector; mHandler = injector.createHandler(Looper.myLooper()); mVibrationSettings = new VibrationSettings(mContext, mHandler); @@ -393,7 +398,42 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @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,90 +443,107 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { */ @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"); + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.VIBRATE, "vibrate"); + return vibrateInternal(uid, displayId, opPkg, effect, attrs, reason, token); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } - 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); + 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); + } + } - 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(); - } + 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); - synchronized (mLock) { - if (DEBUG) { - Slog.d(TAG, "Starting vibrate for vibration " + vib.id); - } + 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(); + } - // Check if user settings or DnD is set to ignore this vibration. - Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationLocked(vib.callerInfo); + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "Starting vibrate for vibration " + vib.id); + } - // Check if ongoing vibration is more important than this vibration. - if (vibrationEndInfo == null) { - vibrationEndInfo = shouldIgnoreVibrationForOngoingLocked(vib); - } + // Check if user settings or DnD is set to ignore this vibration. + Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationLocked(vib.callerInfo); - // 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(); + // 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( - mCurrentExternalVibration.callerInfo); - endExternalVibrateLocked( + mCurrentVibration.getVibration().callerInfo); + mCurrentVibration.notifyCancelled( 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); - } + /* immediate= */ false); } - vibrationEndInfo = startVibrationLocked(vib); - } finally { - Binder.restoreCallingIdentity(ident); } + 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; + // Ignored or failed to start the vibration, end it and report metrics right away. + if (vibrationEndInfo != null) { + endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ true); } - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + return vib; } } @@ -1315,6 +1372,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { 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 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + 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 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // 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 10b49c67e8bb..bc826a3cf4a6 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 static org.mockito.Mockito.when; 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 @@ public class HapticFeedbackCustomizationTest { 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 class HapticFeedbackCustomizationTest { 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 @@ public class HapticFeedbackCustomizationTest { 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 @@ public class HapticFeedbackCustomizationTest { 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 cae811e9bb35..a91bd2b55f76 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 static org.mockito.Mockito.when; 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 @@ public class HapticFeedbackVibrationProviderTest { 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 class HapticFeedbackVibrationProviderTest { 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 class HapticFeedbackVibrationProviderTest { 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 @@ public class HapticFeedbackVibrationProviderTest { 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 @@ public class HapticFeedbackVibrationProviderTest { @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 @@ public class HapticFeedbackVibrationProviderTest { // 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 @@ public class HapticFeedbackVibrationProviderTest { // 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 @@ public class HapticFeedbackVibrationProviderTest { // 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 @@ public class HapticFeedbackVibrationProviderTest { 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 class HapticFeedbackVibrationProviderTest { 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 class HapticFeedbackVibrationProviderTest { 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); + new HapticFeedbackVibrationProvider( + mResourcesMock, mVibratorInfo, /* hapticCustomizations= */ null); assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)).isNull(); + } - // Test with no customizations. - hapticProvider = - new HapticFeedbackVibrationProvider( - mResourcesMock, mVibrator, /* hapticCustomizations= */ null); + @Test + public void testVibrationAttribute_forNotBypassingIntensitySettings() { + HapticFeedbackVibrationProvider hapticProvider = + new HapticFeedbackVibrationProvider(mResourcesMock, mVibratorInfo); - assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)).isNull(); + 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 4e3a893954a2..c25f0cb91caa 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.assertFalse; 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.anyString; 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.ContentResolver; 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.VibrationConfig; 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 @@ public class VibratorManagerServiceTest { 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 @@ public class VibratorManagerServiceTest { mExternalVibratorService = (VibratorManagerService.ExternalVibratorService) serviceInstance; } + + HapticFeedbackVibrationProvider createHapticFeedbackVibrationProvider( + Resources resources, VibratorInfo vibratorInfo) { + return new HapticFeedbackVibrationProvider( + resources, vibratorInfo, mHapticFeedbackVibrationMap); + } }); return mService; } @@ -623,6 +636,18 @@ public class VibratorManagerServiceTest { } @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 @@ public class VibratorManagerServiceTest { } @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 @@ public class VibratorManagerServiceTest { 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 @@ public class VibratorManagerServiceTest { 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 @@ public class VibratorManagerServiceTest { } return predicateResult; } + + private void denyPermission(String permission) { + doThrow(new SecurityException()).when(mContextSpy) + .enforceCallingOrSelfPermission(eq(permission), anyString()); + } } |