Loading packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +19 −14 Original line number Diff line number Diff line Loading @@ -1369,7 +1369,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Trace.endSection(); // on auth success, we sometimes never received an acquired haptic if (!mPlayedAcquiredHaptic) { if (!mPlayedAcquiredHaptic && isUdfpsEnrolled()) { playAcquiredHaptic(); } } Loading @@ -1387,7 +1387,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @Override public void onAuthenticationAcquired(int acquireInfo) { handleFingerprintAcquired(acquireInfo); if (acquireInfo == FingerprintManager.FINGERPRINT_ACQUIRED_GOOD) { if (acquireInfo == FingerprintManager.FINGERPRINT_ACQUIRED_GOOD && isUdfpsEnrolled()) { mPlayedAcquiredHaptic = true; playAcquiredHaptic(); } } Loading @@ -1402,10 +1404,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab public void onUdfpsPointerUp(int sensorId) { Log.d(TAG, "onUdfpsPointerUp, sensorId: " + sensorId); } }; private void playAcquiredHaptic() { if (mAcquiredHapticEnabled && mVibrator != null && isUdfpsEnrolled()) { mPlayedAcquiredHaptic = true; /** * Play haptic to signal udfps fingeprrint acquired. */ @VisibleForTesting public void playAcquiredHaptic() { if (mAcquiredHapticEnabled && mVibrator != null) { String effect = Settings.Global.getString( mContext.getContentResolver(), "udfps_acquired_type"); Loading @@ -1414,7 +1420,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab UdfpsController.VIBRATION_SONIFICATION_ATTRIBUTES); } } }; private final FaceManager.FaceDetectionCallback mFaceDetectionCallback = (sensorId, userId, isStrongBiometric) -> { Loading packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +25 −16 Original line number Diff line number Diff line Loading @@ -430,21 +430,7 @@ public class UdfpsController implements DozeReceiver { mTouchLogTime = SystemClock.elapsedRealtime(); mPowerManager.userActivity(SystemClock.uptimeMillis(), PowerManager.USER_ACTIVITY_EVENT_TOUCH, 0); // TODO: this should eventually be removed after ux testing if (mVibrator != null) { final ContentResolver contentResolver = mContext.getContentResolver(); int startEnabled = Settings.Global.getInt(contentResolver, "udfps_start", 1); if (startEnabled > 0) { String startEffectSetting = Settings.Global.getString( contentResolver, "udfps_start_type"); mVibrator.vibrate(getVibration(startEffectSetting, EFFECT_CLICK), VIBRATION_SONIFICATION_ATTRIBUTES); } } playStartHaptic(); handled = true; } else if (sinceLastLog >= MIN_TOUCH_LOG_INTERVAL) { Log.v(TAG, "onTouch | finger move: " + touchInfo); Loading Loading @@ -498,6 +484,7 @@ public class UdfpsController implements DozeReceiver { @NonNull LockscreenShadeTransitionController lockscreenShadeTransitionController, @NonNull ScreenLifecycle screenLifecycle, @Nullable Vibrator vibrator, @NonNull UdfpsHapticsSimulator udfpsHapticsSimulator, @NonNull Optional<UdfpsHbmProvider> hbmProvider) { mContext = context; mExecution = execution; Loading Loading @@ -544,6 +531,29 @@ public class UdfpsController implements DozeReceiver { final IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); context.registerReceiver(mBroadcastReceiver, filter); udfpsHapticsSimulator.setUdfpsController(this); } /** * Play haptic to signal udfps scanning started. */ @VisibleForTesting public void playStartHaptic() { if (mVibrator != null) { final ContentResolver contentResolver = mContext.getContentResolver(); // TODO: these settings checks should eventually be removed after ux testing // (b/185124905) int startEnabled = Settings.Global.getInt(contentResolver, "udfps_start", 1); if (startEnabled > 0) { String startEffectSetting = Settings.Global.getString( contentResolver, "udfps_start_type"); mVibrator.vibrate(getVibration(startEffectSetting, EFFECT_CLICK), VIBRATION_SONIFICATION_ATTRIBUTES); } } } private int getCoreLayoutParamFlags() { Loading Loading @@ -836,7 +846,6 @@ public class UdfpsController implements DozeReceiver { } } /** * get vibration to play given string * used for testing purposes (b/185124905) Loading packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHapticsSimulator.kt 0 → 100644 +94 −0 Original line number Diff line number Diff line /* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.biometrics import android.media.AudioAttributes import android.os.VibrationEffect import android.os.Vibrator import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry import java.io.PrintWriter import javax.inject.Inject /** * Used to simulate haptics that may be used for udfps authentication. */ @SysUISingleton class UdfpsHapticsSimulator @Inject constructor( commandRegistry: CommandRegistry, val vibrator: Vibrator?, val keyguardUpdateMonitor: KeyguardUpdateMonitor ) : Command { val sonificationEffects = AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .build() var udfpsController: UdfpsController? = null init { commandRegistry.registerCommand("udfps-haptic") { this } } override fun execute(pw: PrintWriter, args: List<String>) { if (args.isEmpty()) { invalidCommand(pw) } else { when (args[0]) { "start" -> { udfpsController?.playStartHaptic() } "acquired" -> { keyguardUpdateMonitor.playAcquiredHaptic() } "success" -> { // needs to be kept up to date with AcquisitionClient#SUCCESS_VIBRATION_EFFECT vibrator?.vibrate( VibrationEffect.get(VibrationEffect.EFFECT_CLICK), sonificationEffects) } "error" -> { // needs to be kept up to date with AcquisitionClient#ERROR_VIBRATION_EFFECT vibrator?.vibrate( VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK), sonificationEffects) } else -> invalidCommand(pw) } } } override fun help(pw: PrintWriter) { pw.println("Usage: adb shell cmd statusbar udfps-haptic <haptic>") pw.println("Available commands:") pw.println(" start") pw.println(" acquired") pw.println(" success, always plays CLICK haptic") pw.println(" error, always plays DOUBLE_CLICK haptic") } fun invalidCommand(pw: PrintWriter) { pw.println("invalid command") help(pw) } } No newline at end of file packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +3 −0 Original line number Diff line number Diff line Loading @@ -126,6 +126,8 @@ public class UdfpsControllerTest extends SysuiTestCase { private ScreenLifecycle mScreenLifecycle; @Mock private Vibrator mVibrator; @Mock private UdfpsHapticsSimulator mUdfpsHapticsSimulator; private FakeExecutor mFgExecutor; Loading Loading @@ -188,6 +190,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mLockscreenShadeTransitionController, mScreenLifecycle, mVibrator, mUdfpsHapticsSimulator, Optional.of(mHbmProvider)); verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); mOverlayController = mOverlayCaptor.getValue(); Loading services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java +18 −46 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.server.biometrics.sensors; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.hardware.biometrics.BiometricConstants; import android.media.AudioAttributes; Loading @@ -27,7 +26,6 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.VibrationEffect; import android.os.Vibrator; import android.text.TextUtils; import android.util.Slog; /** Loading @@ -39,24 +37,18 @@ public abstract class AcquisitionClient<T> extends HalClientMonitor<T> implement private static final String TAG = "Biometrics/AcquisitionClient"; private static final AudioAttributes VIBRATION_SONFICATION_ATTRIBUTES = private static final AudioAttributes VIBRATION_SONIFICATION_ATTRIBUTES = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .build(); private final VibrationEffect mEffectTick = VibrationEffect.get(VibrationEffect.EFFECT_TICK); private final VibrationEffect mEffectTextureTick = VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK); private final VibrationEffect mEffectClick = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); private final VibrationEffect mEffectHeavy = VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK); private final VibrationEffect mDoubleClick = private static final VibrationEffect SUCCESS_VIBRATION_EFFECT = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); private static final VibrationEffect ERROR_VIBRATION_EFFECT = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK); private final PowerManager mPowerManager; private final VibrationEffect mSuccessVibrationEffect; private final VibrationEffect mErrorVibrationEffect; private boolean mShouldSendErrorToClient = true; private boolean mAlreadyCancelled; Loading @@ -72,8 +64,6 @@ public abstract class AcquisitionClient<T> extends HalClientMonitor<T> implement super(context, lazyDaemon, token, listener, userId, owner, cookie, sensorId, statsModality, statsAction, statsClient); mPowerManager = context.getSystemService(PowerManager.class); mSuccessVibrationEffect = mEffectClick; mErrorVibrationEffect = mDoubleClick; } @Override Loading Loading @@ -192,49 +182,31 @@ public abstract class AcquisitionClient<T> extends HalClientMonitor<T> implement mPowerManager.userActivity(now, PowerManager.USER_ACTIVITY_EVENT_TOUCH, 0); } protected @Nullable VibrationEffect getSuccessVibrationEffect() { return mSuccessVibrationEffect; protected boolean successHapticsEnabled() { return true; } protected @Nullable VibrationEffect getErrorVibrationEffect() { return mErrorVibrationEffect; protected boolean errorHapticsEnabled() { return true; } protected final void vibrateSuccess() { if (!successHapticsEnabled()) { return; } Vibrator vibrator = getContext().getSystemService(Vibrator.class); VibrationEffect effect = getSuccessVibrationEffect(); if (vibrator != null && effect != null) { vibrator.vibrate(effect, VIBRATION_SONFICATION_ATTRIBUTES); if (vibrator != null) { vibrator.vibrate(SUCCESS_VIBRATION_EFFECT, VIBRATION_SONIFICATION_ATTRIBUTES); } } protected final void vibrateError() { if (!errorHapticsEnabled()) { return; } Vibrator vibrator = getContext().getSystemService(Vibrator.class); VibrationEffect effect = getErrorVibrationEffect(); if (vibrator != null && effect != null) { vibrator.vibrate(effect, VIBRATION_SONFICATION_ATTRIBUTES); } } protected final @NonNull VibrationEffect getVibration(@Nullable String effect, @NonNull VibrationEffect defaultEffect) { if (TextUtils.isEmpty(effect)) { return defaultEffect; } switch (effect.toLowerCase()) { case "click": return mEffectClick; case "heavy": return mEffectHeavy; case "texture_tick": return mEffectTextureTick; case "tick": return mEffectTick; case "double_click": return mDoubleClick; default: return defaultEffect; if (vibrator != null) { vibrator.vibrate(ERROR_VIBRATION_EFFECT, VIBRATION_SONIFICATION_ATTRIBUTES); } } } Loading
packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +19 −14 Original line number Diff line number Diff line Loading @@ -1369,7 +1369,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Trace.endSection(); // on auth success, we sometimes never received an acquired haptic if (!mPlayedAcquiredHaptic) { if (!mPlayedAcquiredHaptic && isUdfpsEnrolled()) { playAcquiredHaptic(); } } Loading @@ -1387,7 +1387,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @Override public void onAuthenticationAcquired(int acquireInfo) { handleFingerprintAcquired(acquireInfo); if (acquireInfo == FingerprintManager.FINGERPRINT_ACQUIRED_GOOD) { if (acquireInfo == FingerprintManager.FINGERPRINT_ACQUIRED_GOOD && isUdfpsEnrolled()) { mPlayedAcquiredHaptic = true; playAcquiredHaptic(); } } Loading @@ -1402,10 +1404,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab public void onUdfpsPointerUp(int sensorId) { Log.d(TAG, "onUdfpsPointerUp, sensorId: " + sensorId); } }; private void playAcquiredHaptic() { if (mAcquiredHapticEnabled && mVibrator != null && isUdfpsEnrolled()) { mPlayedAcquiredHaptic = true; /** * Play haptic to signal udfps fingeprrint acquired. */ @VisibleForTesting public void playAcquiredHaptic() { if (mAcquiredHapticEnabled && mVibrator != null) { String effect = Settings.Global.getString( mContext.getContentResolver(), "udfps_acquired_type"); Loading @@ -1414,7 +1420,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab UdfpsController.VIBRATION_SONIFICATION_ATTRIBUTES); } } }; private final FaceManager.FaceDetectionCallback mFaceDetectionCallback = (sensorId, userId, isStrongBiometric) -> { Loading
packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +25 −16 Original line number Diff line number Diff line Loading @@ -430,21 +430,7 @@ public class UdfpsController implements DozeReceiver { mTouchLogTime = SystemClock.elapsedRealtime(); mPowerManager.userActivity(SystemClock.uptimeMillis(), PowerManager.USER_ACTIVITY_EVENT_TOUCH, 0); // TODO: this should eventually be removed after ux testing if (mVibrator != null) { final ContentResolver contentResolver = mContext.getContentResolver(); int startEnabled = Settings.Global.getInt(contentResolver, "udfps_start", 1); if (startEnabled > 0) { String startEffectSetting = Settings.Global.getString( contentResolver, "udfps_start_type"); mVibrator.vibrate(getVibration(startEffectSetting, EFFECT_CLICK), VIBRATION_SONIFICATION_ATTRIBUTES); } } playStartHaptic(); handled = true; } else if (sinceLastLog >= MIN_TOUCH_LOG_INTERVAL) { Log.v(TAG, "onTouch | finger move: " + touchInfo); Loading Loading @@ -498,6 +484,7 @@ public class UdfpsController implements DozeReceiver { @NonNull LockscreenShadeTransitionController lockscreenShadeTransitionController, @NonNull ScreenLifecycle screenLifecycle, @Nullable Vibrator vibrator, @NonNull UdfpsHapticsSimulator udfpsHapticsSimulator, @NonNull Optional<UdfpsHbmProvider> hbmProvider) { mContext = context; mExecution = execution; Loading Loading @@ -544,6 +531,29 @@ public class UdfpsController implements DozeReceiver { final IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); context.registerReceiver(mBroadcastReceiver, filter); udfpsHapticsSimulator.setUdfpsController(this); } /** * Play haptic to signal udfps scanning started. */ @VisibleForTesting public void playStartHaptic() { if (mVibrator != null) { final ContentResolver contentResolver = mContext.getContentResolver(); // TODO: these settings checks should eventually be removed after ux testing // (b/185124905) int startEnabled = Settings.Global.getInt(contentResolver, "udfps_start", 1); if (startEnabled > 0) { String startEffectSetting = Settings.Global.getString( contentResolver, "udfps_start_type"); mVibrator.vibrate(getVibration(startEffectSetting, EFFECT_CLICK), VIBRATION_SONIFICATION_ATTRIBUTES); } } } private int getCoreLayoutParamFlags() { Loading Loading @@ -836,7 +846,6 @@ public class UdfpsController implements DozeReceiver { } } /** * get vibration to play given string * used for testing purposes (b/185124905) Loading
packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHapticsSimulator.kt 0 → 100644 +94 −0 Original line number Diff line number Diff line /* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.biometrics import android.media.AudioAttributes import android.os.VibrationEffect import android.os.Vibrator import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry import java.io.PrintWriter import javax.inject.Inject /** * Used to simulate haptics that may be used for udfps authentication. */ @SysUISingleton class UdfpsHapticsSimulator @Inject constructor( commandRegistry: CommandRegistry, val vibrator: Vibrator?, val keyguardUpdateMonitor: KeyguardUpdateMonitor ) : Command { val sonificationEffects = AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .build() var udfpsController: UdfpsController? = null init { commandRegistry.registerCommand("udfps-haptic") { this } } override fun execute(pw: PrintWriter, args: List<String>) { if (args.isEmpty()) { invalidCommand(pw) } else { when (args[0]) { "start" -> { udfpsController?.playStartHaptic() } "acquired" -> { keyguardUpdateMonitor.playAcquiredHaptic() } "success" -> { // needs to be kept up to date with AcquisitionClient#SUCCESS_VIBRATION_EFFECT vibrator?.vibrate( VibrationEffect.get(VibrationEffect.EFFECT_CLICK), sonificationEffects) } "error" -> { // needs to be kept up to date with AcquisitionClient#ERROR_VIBRATION_EFFECT vibrator?.vibrate( VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK), sonificationEffects) } else -> invalidCommand(pw) } } } override fun help(pw: PrintWriter) { pw.println("Usage: adb shell cmd statusbar udfps-haptic <haptic>") pw.println("Available commands:") pw.println(" start") pw.println(" acquired") pw.println(" success, always plays CLICK haptic") pw.println(" error, always plays DOUBLE_CLICK haptic") } fun invalidCommand(pw: PrintWriter) { pw.println("invalid command") help(pw) } } No newline at end of file
packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +3 −0 Original line number Diff line number Diff line Loading @@ -126,6 +126,8 @@ public class UdfpsControllerTest extends SysuiTestCase { private ScreenLifecycle mScreenLifecycle; @Mock private Vibrator mVibrator; @Mock private UdfpsHapticsSimulator mUdfpsHapticsSimulator; private FakeExecutor mFgExecutor; Loading Loading @@ -188,6 +190,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mLockscreenShadeTransitionController, mScreenLifecycle, mVibrator, mUdfpsHapticsSimulator, Optional.of(mHbmProvider)); verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); mOverlayController = mOverlayCaptor.getValue(); Loading
services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java +18 −46 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.server.biometrics.sensors; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.hardware.biometrics.BiometricConstants; import android.media.AudioAttributes; Loading @@ -27,7 +26,6 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.VibrationEffect; import android.os.Vibrator; import android.text.TextUtils; import android.util.Slog; /** Loading @@ -39,24 +37,18 @@ public abstract class AcquisitionClient<T> extends HalClientMonitor<T> implement private static final String TAG = "Biometrics/AcquisitionClient"; private static final AudioAttributes VIBRATION_SONFICATION_ATTRIBUTES = private static final AudioAttributes VIBRATION_SONIFICATION_ATTRIBUTES = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .build(); private final VibrationEffect mEffectTick = VibrationEffect.get(VibrationEffect.EFFECT_TICK); private final VibrationEffect mEffectTextureTick = VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK); private final VibrationEffect mEffectClick = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); private final VibrationEffect mEffectHeavy = VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK); private final VibrationEffect mDoubleClick = private static final VibrationEffect SUCCESS_VIBRATION_EFFECT = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); private static final VibrationEffect ERROR_VIBRATION_EFFECT = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK); private final PowerManager mPowerManager; private final VibrationEffect mSuccessVibrationEffect; private final VibrationEffect mErrorVibrationEffect; private boolean mShouldSendErrorToClient = true; private boolean mAlreadyCancelled; Loading @@ -72,8 +64,6 @@ public abstract class AcquisitionClient<T> extends HalClientMonitor<T> implement super(context, lazyDaemon, token, listener, userId, owner, cookie, sensorId, statsModality, statsAction, statsClient); mPowerManager = context.getSystemService(PowerManager.class); mSuccessVibrationEffect = mEffectClick; mErrorVibrationEffect = mDoubleClick; } @Override Loading Loading @@ -192,49 +182,31 @@ public abstract class AcquisitionClient<T> extends HalClientMonitor<T> implement mPowerManager.userActivity(now, PowerManager.USER_ACTIVITY_EVENT_TOUCH, 0); } protected @Nullable VibrationEffect getSuccessVibrationEffect() { return mSuccessVibrationEffect; protected boolean successHapticsEnabled() { return true; } protected @Nullable VibrationEffect getErrorVibrationEffect() { return mErrorVibrationEffect; protected boolean errorHapticsEnabled() { return true; } protected final void vibrateSuccess() { if (!successHapticsEnabled()) { return; } Vibrator vibrator = getContext().getSystemService(Vibrator.class); VibrationEffect effect = getSuccessVibrationEffect(); if (vibrator != null && effect != null) { vibrator.vibrate(effect, VIBRATION_SONFICATION_ATTRIBUTES); if (vibrator != null) { vibrator.vibrate(SUCCESS_VIBRATION_EFFECT, VIBRATION_SONIFICATION_ATTRIBUTES); } } protected final void vibrateError() { if (!errorHapticsEnabled()) { return; } Vibrator vibrator = getContext().getSystemService(Vibrator.class); VibrationEffect effect = getErrorVibrationEffect(); if (vibrator != null && effect != null) { vibrator.vibrate(effect, VIBRATION_SONFICATION_ATTRIBUTES); } } protected final @NonNull VibrationEffect getVibration(@Nullable String effect, @NonNull VibrationEffect defaultEffect) { if (TextUtils.isEmpty(effect)) { return defaultEffect; } switch (effect.toLowerCase()) { case "click": return mEffectClick; case "heavy": return mEffectHeavy; case "texture_tick": return mEffectTextureTick; case "tick": return mEffectTick; case "double_click": return mDoubleClick; default: return defaultEffect; if (vibrator != null) { vibrator.vibrate(ERROR_VIBRATION_EFFECT, VIBRATION_SONIFICATION_ATTRIBUTES); } } }