• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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