1 /* 2 * Copyright 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.vibrator; 18 19 import android.annotation.Nullable; 20 import android.content.res.Resources; 21 import android.os.VibrationAttributes; 22 import android.os.VibrationEffect; 23 import android.os.Vibrator; 24 import android.os.VibratorInfo; 25 import android.os.vibrator.Flags; 26 import android.util.Slog; 27 import android.util.SparseArray; 28 import android.view.HapticFeedbackConstants; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 32 import java.io.IOException; 33 import java.io.PrintWriter; 34 35 /** 36 * Provides the {@link VibrationEffect} and {@link VibrationAttributes} for haptic feedback. 37 */ 38 public final class HapticFeedbackVibrationProvider { 39 private static final String TAG = "HapticFeedbackVibrationProvider"; 40 41 private static final VibrationAttributes TOUCH_VIBRATION_ATTRIBUTES = 42 VibrationAttributes.createForUsage(VibrationAttributes.USAGE_TOUCH); 43 private static final VibrationAttributes PHYSICAL_EMULATION_VIBRATION_ATTRIBUTES = 44 VibrationAttributes.createForUsage(VibrationAttributes.USAGE_PHYSICAL_EMULATION); 45 private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES = 46 VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK); 47 private static final VibrationAttributes COMMUNICATION_REQUEST_VIBRATION_ATTRIBUTES = 48 VibrationAttributes.createForUsage(VibrationAttributes.USAGE_COMMUNICATION_REQUEST); 49 50 private final VibratorInfo mVibratorInfo; 51 private final boolean mHapticTextHandleEnabled; 52 // Vibrator effect for haptic feedback during boot when safe mode is enabled. 53 private final VibrationEffect mSafeModeEnabledVibrationEffect; 54 // Haptic feedback vibration customizations specific to the device. 55 // If present and valid, a vibration here will be used for an effect. 56 // Otherwise, the system's default vibration will be used. 57 @Nullable private final SparseArray<VibrationEffect> mHapticCustomizations; 58 59 private float mKeyboardVibrationFixedAmplitude; 60 HapticFeedbackVibrationProvider(Resources res, Vibrator vibrator)61 public HapticFeedbackVibrationProvider(Resources res, Vibrator vibrator) { 62 this(res, vibrator.getInfo()); 63 } 64 HapticFeedbackVibrationProvider(Resources res, VibratorInfo vibratorInfo)65 public HapticFeedbackVibrationProvider(Resources res, VibratorInfo vibratorInfo) { 66 this(res, vibratorInfo, loadHapticCustomizations(res, vibratorInfo)); 67 } 68 HapticFeedbackVibrationProvider( Resources res, VibratorInfo vibratorInfo, @Nullable SparseArray<VibrationEffect> hapticCustomizations)69 @VisibleForTesting HapticFeedbackVibrationProvider( 70 Resources res, 71 VibratorInfo vibratorInfo, 72 @Nullable SparseArray<VibrationEffect> hapticCustomizations) { 73 mVibratorInfo = vibratorInfo; 74 mHapticTextHandleEnabled = res.getBoolean( 75 com.android.internal.R.bool.config_enableHapticTextHandle); 76 77 if (hapticCustomizations != null && hapticCustomizations.size() == 0) { 78 hapticCustomizations = null; 79 } 80 mHapticCustomizations = hapticCustomizations; 81 mSafeModeEnabledVibrationEffect = 82 effectHasCustomization(HapticFeedbackConstants.SAFE_MODE_ENABLED) 83 ? mHapticCustomizations.get(HapticFeedbackConstants.SAFE_MODE_ENABLED) 84 : VibrationSettings.createEffectFromResource( 85 res, 86 com.android.internal.R.array.config_safeModeEnabledVibePattern); 87 mKeyboardVibrationFixedAmplitude = res.getFloat( 88 com.android.internal.R.dimen.config_keyboardHapticFeedbackFixedAmplitude); 89 if (mKeyboardVibrationFixedAmplitude < 0 || mKeyboardVibrationFixedAmplitude > 1) { 90 mKeyboardVibrationFixedAmplitude = -1; 91 } 92 } 93 94 /** 95 * Provides the {@link VibrationEffect} for a given haptic feedback effect ID (provided in 96 * {@link HapticFeedbackConstants}). 97 * 98 * @param effectId the haptic feedback effect ID whose respective vibration we want to get. 99 * @return a {@link VibrationEffect} for the given haptic feedback effect ID, or {@code null} if 100 * the provided effect ID is not supported. 101 */ getVibrationForHapticFeedback(int effectId)102 @Nullable public VibrationEffect getVibrationForHapticFeedback(int effectId) { 103 switch (effectId) { 104 case HapticFeedbackConstants.CONTEXT_CLICK: 105 case HapticFeedbackConstants.GESTURE_END: 106 case HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE: 107 case HapticFeedbackConstants.SCROLL_TICK: 108 case HapticFeedbackConstants.SEGMENT_TICK: 109 return getVibration(effectId, VibrationEffect.EFFECT_TICK); 110 111 case HapticFeedbackConstants.TEXT_HANDLE_MOVE: 112 if (!mHapticTextHandleEnabled) { 113 return null; 114 } 115 // fallthrough 116 case HapticFeedbackConstants.CLOCK_TICK: 117 case HapticFeedbackConstants.SEGMENT_FREQUENT_TICK: 118 return getVibration(effectId, VibrationEffect.EFFECT_TEXTURE_TICK); 119 120 case HapticFeedbackConstants.KEYBOARD_RELEASE: 121 case HapticFeedbackConstants.KEYBOARD_TAP: // == KEYBOARD_PRESS 122 return getKeyboardVibration(effectId); 123 124 case HapticFeedbackConstants.VIRTUAL_KEY_RELEASE: 125 case HapticFeedbackConstants.DRAG_CROSSING: 126 return getVibration( 127 effectId, 128 VibrationEffect.EFFECT_TICK, 129 /* fallbackForPredefinedEffect= */ false); 130 131 case HapticFeedbackConstants.VIRTUAL_KEY: 132 case HapticFeedbackConstants.EDGE_RELEASE: 133 case HapticFeedbackConstants.CALENDAR_DATE: 134 case HapticFeedbackConstants.CONFIRM: 135 case HapticFeedbackConstants.BIOMETRIC_CONFIRM: 136 case HapticFeedbackConstants.GESTURE_START: 137 case HapticFeedbackConstants.SCROLL_ITEM_FOCUS: 138 case HapticFeedbackConstants.SCROLL_LIMIT: 139 return getVibration(effectId, VibrationEffect.EFFECT_CLICK); 140 141 case HapticFeedbackConstants.LONG_PRESS: 142 case HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON: 143 case HapticFeedbackConstants.DRAG_START: 144 case HapticFeedbackConstants.EDGE_SQUEEZE: 145 return getVibration(effectId, VibrationEffect.EFFECT_HEAVY_CLICK); 146 147 case HapticFeedbackConstants.REJECT: 148 case HapticFeedbackConstants.BIOMETRIC_REJECT: 149 return getVibration(effectId, VibrationEffect.EFFECT_DOUBLE_CLICK); 150 151 case HapticFeedbackConstants.SAFE_MODE_ENABLED: 152 return mSafeModeEnabledVibrationEffect; 153 154 case HapticFeedbackConstants.ASSISTANT_BUTTON: 155 return getAssistantButtonVibration(); 156 157 case HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE: 158 return getVibration( 159 effectId, 160 VibrationEffect.Composition.PRIMITIVE_TICK, 161 /* primitiveScale= */ 0.4f, 162 VibrationEffect.EFFECT_TEXTURE_TICK); 163 164 case HapticFeedbackConstants.TOGGLE_ON: 165 return getVibration( 166 effectId, 167 VibrationEffect.Composition.PRIMITIVE_TICK, 168 /* primitiveScale= */ 0.5f, 169 VibrationEffect.EFFECT_TICK); 170 171 case HapticFeedbackConstants.TOGGLE_OFF: 172 return getVibration( 173 effectId, 174 VibrationEffect.Composition.PRIMITIVE_LOW_TICK, 175 /* primitiveScale= */ 0.2f, 176 VibrationEffect.EFFECT_TEXTURE_TICK); 177 178 case HapticFeedbackConstants.NO_HAPTICS: 179 default: 180 return null; 181 } 182 } 183 184 /** 185 * Provides the {@link VibrationAttributes} that should be used for a haptic feedback. 186 * 187 * @param effectId the haptic feedback effect ID whose respective vibration attributes we want 188 * to get. 189 * @param bypassVibrationIntensitySetting {@code true} if the returned attribute should bypass 190 * vibration intensity settings. {@code false} otherwise. 191 * @param fromIme the haptic feedback is performed from an IME. 192 * @return the {@link VibrationAttributes} that should be used for the provided haptic feedback. 193 */ getVibrationAttributesForHapticFeedback( int effectId, boolean bypassVibrationIntensitySetting, boolean fromIme)194 public VibrationAttributes getVibrationAttributesForHapticFeedback( 195 int effectId, boolean bypassVibrationIntensitySetting, boolean fromIme) { 196 VibrationAttributes attrs; 197 switch (effectId) { 198 case HapticFeedbackConstants.EDGE_SQUEEZE: 199 case HapticFeedbackConstants.EDGE_RELEASE: 200 attrs = PHYSICAL_EMULATION_VIBRATION_ATTRIBUTES; 201 break; 202 case HapticFeedbackConstants.ASSISTANT_BUTTON: 203 case HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON: 204 case HapticFeedbackConstants.SCROLL_TICK: 205 case HapticFeedbackConstants.SCROLL_ITEM_FOCUS: 206 case HapticFeedbackConstants.SCROLL_LIMIT: 207 attrs = HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES; 208 break; 209 case HapticFeedbackConstants.KEYBOARD_TAP: 210 case HapticFeedbackConstants.KEYBOARD_RELEASE: 211 attrs = createKeyboardVibrationAttributes(fromIme); 212 break; 213 case HapticFeedbackConstants.BIOMETRIC_CONFIRM: 214 case HapticFeedbackConstants.BIOMETRIC_REJECT: 215 attrs = COMMUNICATION_REQUEST_VIBRATION_ATTRIBUTES; 216 break; 217 default: 218 attrs = TOUCH_VIBRATION_ATTRIBUTES; 219 } 220 221 int flags = 0; 222 if (bypassVibrationIntensitySetting) { 223 flags |= VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF; 224 } 225 if (shouldBypassInterruptionPolicy(effectId)) { 226 flags |= VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY; 227 } 228 if (shouldBypassIntensityScale(effectId, fromIme)) { 229 flags |= VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE; 230 } 231 232 return flags == 0 ? attrs : new VibrationAttributes.Builder(attrs).setFlags(flags).build(); 233 } 234 235 /** 236 * Returns true if given haptic feedback is restricted to system apps with permission 237 * {@code android.permission.VIBRATE_SYSTEM_CONSTANTS}. 238 * 239 * @param effectId the haptic feedback effect ID to check. 240 * @return true if the haptic feedback is restricted, false otherwise. 241 */ isRestrictedHapticFeedback(int effectId)242 public boolean isRestrictedHapticFeedback(int effectId) { 243 switch (effectId) { 244 case HapticFeedbackConstants.BIOMETRIC_CONFIRM: 245 case HapticFeedbackConstants.BIOMETRIC_REJECT: 246 return true; 247 default: 248 return false; 249 } 250 } 251 252 /** Dumps relevant state. */ dump(String prefix, PrintWriter pw)253 public void dump(String prefix, PrintWriter pw) { 254 pw.print("mHapticTextHandleEnabled="); pw.println(mHapticTextHandleEnabled); 255 } 256 getVibration(int effectId, int predefinedVibrationEffectId)257 private VibrationEffect getVibration(int effectId, int predefinedVibrationEffectId) { 258 return getVibration( 259 effectId, predefinedVibrationEffectId, /* fallbackForPredefinedEffect= */ true); 260 } 261 262 /** 263 * Returns the customized vibration for {@code hapticFeedbackId}, or 264 * {@code predefinedVibrationEffectId} if a customization does not exist for the haptic 265 * feedback. 266 * 267 * <p>If a customization does not exist and the default predefined effect is to be returned, 268 * {@code fallbackForPredefinedEffect} will be used to decide whether or not to fallback 269 * to a generic pattern if the predefined effect is not hardware supported. 270 * 271 * @see VibrationEffect#get(int, boolean) 272 */ getVibration( int hapticFeedbackId, int predefinedVibrationEffectId, boolean fallbackForPredefinedEffect)273 private VibrationEffect getVibration( 274 int hapticFeedbackId, 275 int predefinedVibrationEffectId, 276 boolean fallbackForPredefinedEffect) { 277 if (effectHasCustomization(hapticFeedbackId)) { 278 return mHapticCustomizations.get(hapticFeedbackId); 279 } 280 return VibrationEffect.get(predefinedVibrationEffectId, fallbackForPredefinedEffect); 281 } 282 283 /** 284 * Returns the customized vibration for {@code hapticFeedbackId}, or some fallback vibration if 285 * a customization does not exist for the ID. 286 * 287 * <p>The fallback will be a primitive composition formed of {@code primitiveId} and 288 * {@code primitiveScale}, if the primitive is supported. Otherwise, it will be a predefined 289 * vibration of {@code elsePredefinedVibrationEffectId}. 290 */ getVibration( int hapticFeedbackId, int primitiveId, float primitiveScale, int elsePredefinedVibrationEffectId)291 private VibrationEffect getVibration( 292 int hapticFeedbackId, 293 int primitiveId, 294 float primitiveScale, 295 int elsePredefinedVibrationEffectId) { 296 if (effectHasCustomization(hapticFeedbackId)) { 297 return mHapticCustomizations.get(hapticFeedbackId); 298 } 299 if (mVibratorInfo.isPrimitiveSupported(primitiveId)) { 300 return VibrationEffect.startComposition() 301 .addPrimitive(primitiveId, primitiveScale) 302 .compose(); 303 } else { 304 return VibrationEffect.get(elsePredefinedVibrationEffectId); 305 } 306 } 307 getAssistantButtonVibration()308 private VibrationEffect getAssistantButtonVibration() { 309 if (effectHasCustomization(HapticFeedbackConstants.ASSISTANT_BUTTON)) { 310 return mHapticCustomizations.get(HapticFeedbackConstants.ASSISTANT_BUTTON); 311 } 312 if (mVibratorInfo.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE) 313 && mVibratorInfo.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_TICK)) { 314 // quiet ramp, short pause, then sharp tick 315 return VibrationEffect.startComposition() 316 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.25f) 317 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1f, 50) 318 .compose(); 319 } 320 // fallback for devices without composition support 321 return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK); 322 } 323 effectHasCustomization(int effectId)324 private boolean effectHasCustomization(int effectId) { 325 return mHapticCustomizations != null && mHapticCustomizations.contains(effectId); 326 } 327 getKeyboardVibration(int effectId)328 private VibrationEffect getKeyboardVibration(int effectId) { 329 if (effectHasCustomization(effectId)) { 330 return mHapticCustomizations.get(effectId); 331 } 332 333 int primitiveId; 334 int predefinedEffectId; 335 boolean predefinedEffectFallback; 336 337 switch (effectId) { 338 case HapticFeedbackConstants.KEYBOARD_RELEASE: 339 primitiveId = VibrationEffect.Composition.PRIMITIVE_TICK; 340 predefinedEffectId = VibrationEffect.EFFECT_TICK; 341 predefinedEffectFallback = false; 342 break; 343 case HapticFeedbackConstants.KEYBOARD_TAP: 344 default: 345 primitiveId = VibrationEffect.Composition.PRIMITIVE_CLICK; 346 predefinedEffectId = VibrationEffect.EFFECT_CLICK; 347 predefinedEffectFallback = true; 348 } 349 if (Flags.keyboardCategoryEnabled() && mKeyboardVibrationFixedAmplitude > 0) { 350 if (mVibratorInfo.isPrimitiveSupported(primitiveId)) { 351 return VibrationEffect.startComposition() 352 .addPrimitive(primitiveId, mKeyboardVibrationFixedAmplitude) 353 .compose(); 354 } 355 } 356 return getVibration(effectId, predefinedEffectId, 357 /* fallbackForPredefinedEffect= */ predefinedEffectFallback); 358 } 359 shouldBypassIntensityScale(int effectId, boolean isIme)360 private boolean shouldBypassIntensityScale(int effectId, boolean isIme) { 361 if (!Flags.keyboardCategoryEnabled() || mKeyboardVibrationFixedAmplitude < 0 || !isIme) { 362 // Shouldn't bypass if not support keyboard category, no fixed amplitude or not an IME. 363 return false; 364 } 365 switch (effectId) { 366 case HapticFeedbackConstants.KEYBOARD_TAP: 367 return mVibratorInfo.isPrimitiveSupported( 368 VibrationEffect.Composition.PRIMITIVE_CLICK); 369 case HapticFeedbackConstants.KEYBOARD_RELEASE: 370 return mVibratorInfo.isPrimitiveSupported( 371 VibrationEffect.Composition.PRIMITIVE_TICK); 372 } 373 return false; 374 } 375 createKeyboardVibrationAttributes(boolean fromIme)376 private VibrationAttributes createKeyboardVibrationAttributes(boolean fromIme) { 377 // Use touch attribute when the keyboard category is disable or it's not from an IME. 378 if (!Flags.keyboardCategoryEnabled() || !fromIme) { 379 return TOUCH_VIBRATION_ATTRIBUTES; 380 } 381 382 return new VibrationAttributes.Builder(TOUCH_VIBRATION_ATTRIBUTES) 383 .setCategory(VibrationAttributes.CATEGORY_KEYBOARD) 384 .build(); 385 } 386 387 @Nullable loadHapticCustomizations( Resources res, VibratorInfo vibratorInfo)388 private static SparseArray<VibrationEffect> loadHapticCustomizations( 389 Resources res, VibratorInfo vibratorInfo) { 390 try { 391 return HapticFeedbackCustomization.loadVibrations(res, vibratorInfo); 392 } catch (IOException | HapticFeedbackCustomization.CustomizationParserException e) { 393 Slog.e(TAG, "Unable to load haptic customizations.", e); 394 return null; 395 } 396 } 397 shouldBypassInterruptionPolicy(int effectId)398 private static boolean shouldBypassInterruptionPolicy(int effectId) { 399 switch (effectId) { 400 case HapticFeedbackConstants.SCROLL_TICK: 401 case HapticFeedbackConstants.SCROLL_ITEM_FOCUS: 402 case HapticFeedbackConstants.SCROLL_LIMIT: 403 // The SCROLL_* constants should bypass interruption filter, so that scroll haptics 404 // can play regardless of focus modes like DND. Guard this behavior by the feature 405 // flag controlling the general scroll feedback APIs. 406 return android.view.flags.Flags.scrollFeedbackApi(); 407 default: 408 return false; 409 } 410 } 411 } 412