• 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.NonNull;
20 import android.annotation.Nullable;
21 import android.content.res.Resources;
22 import android.content.res.XmlResourceParser;
23 import android.os.VibrationEffect;
24 import android.os.VibratorInfo;
25 import android.os.vibrator.Flags;
26 import android.os.vibrator.persistence.ParsedVibration;
27 import android.os.vibrator.persistence.VibrationXmlParser;
28 import android.text.TextUtils;
29 import android.util.Slog;
30 import android.util.SparseArray;
31 import android.util.Xml;
32 import android.view.InputDevice;
33 
34 import com.android.internal.R;
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.util.XmlUtils;
37 import com.android.internal.vibrator.persistence.XmlParserException;
38 import com.android.internal.vibrator.persistence.XmlReader;
39 import com.android.internal.vibrator.persistence.XmlValidator;
40 import com.android.modules.utils.TypedXmlPullParser;
41 
42 import org.xmlpull.v1.XmlPullParser;
43 import org.xmlpull.v1.XmlPullParserException;
44 
45 import java.io.FileNotFoundException;
46 import java.io.FileReader;
47 import java.io.IOException;
48 import java.io.Reader;
49 import java.util.Locale;
50 
51 /**
52  * Class that loads custom {@link VibrationEffect} to be performed for each
53  * {@link HapticFeedbackConstants} key.
54  *
55  * <p>The system has its default logic to get the {@link VibrationEffect} that will be played for a
56  * given haptic feedback constant. Devices may choose to override some or all of these supported
57  * haptic feedback vibrations via a customization XML.
58  *
59  * <p>The XML simply provides a mapping of a constant from {@link HapticFeedbackConstants} to its
60  * corresponding {@link VibrationEffect}. Its root tag should be `<haptic-feedback-constants>`. It
61  * should have one or more entries for customizing a haptic feedback constant. A customization is
62  * started by a `<constant id="X">` tag (where `X` is the haptic feedback constant being customized
63  * in this entry) and closed by </constant>. Between these two tags, there should be a valid XML
64  * serialization of a non-repeating {@link VibrationEffect}. Such a valid vibration serialization
65  * should be parse-able by {@link VibrationXmlParser}.
66  *
67  * The example below represents a valid customization for effect IDs 10 and 11.
68  *
69  * <pre>
70  *   {@code
71  *     <haptic-feedback-constants>
72  *          <constant id="10">
73  *              // Valid Vibration Serialization
74  *          </constant>
75  *          <constant id="11">
76  *              // Valid Vibration Serialization
77  *          </constant>
78  *     </haptic-feedback-constants>
79  *   }
80  * </pre>
81  *
82  * <p>After a successful parsing of the customization XML file, it returns a {@link SparseArray}
83  * that maps each customized haptic feedback effect ID to its respective {@link VibrationEffect}.
84  */
85 final class HapticFeedbackCustomization {
86     private static final String TAG = "HapticFeedbackCustomization";
87 
88     /** The outer-most tag for haptic feedback customizations.  */
89     private static final String TAG_CONSTANTS = "haptic-feedback-constants";
90     /** The tag defining a customization for a single haptic feedback constant. */
91     private static final String TAG_CONSTANT = "constant";
92 
93     /**
94      * Attribute for {@link TAG_CONSTANT}, specifying the haptic feedback constant to
95      * customize.
96      */
97     private static final String ATTRIBUTE_ID = "id";
98 
99     /**
100      * A {@link SparseArray} that maps each customized haptic feedback effect ID to its
101      * respective {@link VibrationEffect}. If this is empty, system's default vibration will be
102      * used.
103      */
104     @NonNull
105     private final SparseArray<VibrationEffect> mHapticCustomizations;
106 
107     /**
108      * A {@link SparseArray} similar to {@link mHapticCustomizations} but for rotary input source
109      * specific customization.
110      */
111     @NonNull
112     private final SparseArray<VibrationEffect> mHapticCustomizationsForSourceRotary;
113 
114     /**
115      * A {@link SparseArray} similar to {@link mHapticCustomizations} but for touch screen input
116      * source specific customization.
117      */
118     @NonNull
119     private final SparseArray<VibrationEffect> mHapticCustomizationsForSourceTouchScreen;
120 
HapticFeedbackCustomization(Resources res, VibratorInfo vibratorInfo)121     HapticFeedbackCustomization(Resources res, VibratorInfo vibratorInfo) {
122         if (!Flags.hapticFeedbackVibrationOemCustomizationEnabled()) {
123             Slog.d(TAG, "Haptic feedback customization feature is not enabled.");
124             mHapticCustomizations = new SparseArray<>();
125             mHapticCustomizationsForSourceRotary = new SparseArray<>();
126             mHapticCustomizationsForSourceTouchScreen = new SparseArray<>();
127             return;
128         }
129 
130         // Load base customizations.
131         SparseArray<VibrationEffect> hapticCustomizations;
132         hapticCustomizations = loadCustomizedFeedbackVibrationFromFile(res, vibratorInfo);
133         if (hapticCustomizations.size() == 0) {
134             // Input source customized haptic feedback was directly added in res. So, no need to old
135             // loading path.
136             hapticCustomizations = loadCustomizedFeedbackVibrationFromRes(res, vibratorInfo,
137                     R.xml.haptic_feedback_customization);
138         }
139         mHapticCustomizations = hapticCustomizations;
140 
141         // Load customizations specified by input sources.
142         if (android.os.vibrator.Flags.hapticFeedbackInputSourceCustomizationEnabled()) {
143             mHapticCustomizationsForSourceRotary =
144                     loadCustomizedFeedbackVibrationFromRes(res, vibratorInfo,
145                             R.xml.haptic_feedback_customization_source_rotary_encoder);
146             mHapticCustomizationsForSourceTouchScreen =
147                     loadCustomizedFeedbackVibrationFromRes(res, vibratorInfo,
148                             R.xml.haptic_feedback_customization_source_touchscreen);
149         } else {
150             mHapticCustomizationsForSourceRotary = new SparseArray<>();
151             mHapticCustomizationsForSourceTouchScreen = new SparseArray<>();
152         }
153     }
154 
155     @VisibleForTesting
HapticFeedbackCustomization(@onNull SparseArray<VibrationEffect> hapticCustomizations, @NonNull SparseArray<VibrationEffect> hapticCustomizationsForSourceRotary, @NonNull SparseArray<VibrationEffect> hapticCustomizationsForSourceTouchScreen)156     HapticFeedbackCustomization(@NonNull SparseArray<VibrationEffect> hapticCustomizations,
157             @NonNull SparseArray<VibrationEffect> hapticCustomizationsForSourceRotary,
158             @NonNull SparseArray<VibrationEffect> hapticCustomizationsForSourceTouchScreen) {
159         mHapticCustomizations = hapticCustomizations;
160         mHapticCustomizationsForSourceRotary = hapticCustomizationsForSourceRotary;
161         mHapticCustomizationsForSourceTouchScreen = hapticCustomizationsForSourceTouchScreen;
162     }
163 
164     @Nullable
getEffect(int effectId)165     VibrationEffect getEffect(int effectId) {
166         return mHapticCustomizations.get(effectId);
167     }
168 
169     @Nullable
getEffect(int effectId, int inputSource)170     VibrationEffect getEffect(int effectId, int inputSource) {
171         VibrationEffect resultVibration = null;
172         if ((InputDevice.SOURCE_ROTARY_ENCODER & inputSource) != 0) {
173             resultVibration = mHapticCustomizationsForSourceRotary.get(effectId);
174         } else if ((InputDevice.SOURCE_TOUCHSCREEN & inputSource) != 0) {
175             resultVibration = mHapticCustomizationsForSourceTouchScreen.get(effectId);
176         }
177         if (resultVibration == null) {
178             resultVibration = mHapticCustomizations.get(effectId);
179         }
180         return resultVibration;
181     }
182 
183     /**
184      * Parses the haptic feedback vibration customization XML file for the device whose directory is
185      * specified by config. See {@link R.string.config_hapticFeedbackCustomizationFile}.
186      *
187      * @return Return a mapping of the customized effect IDs to their respective
188      * {@link VibrationEffect}s.
189      */
190     @NonNull
loadCustomizedFeedbackVibrationFromFile( Resources res, VibratorInfo vibratorInfo)191     private static SparseArray<VibrationEffect> loadCustomizedFeedbackVibrationFromFile(
192             Resources res, VibratorInfo vibratorInfo) {
193         try {
194             TypedXmlPullParser parser = readCustomizationFile(res);
195             if (parser == null) {
196                 Slog.d(TAG, "No loadable haptic feedback customization from file.");
197                 return new SparseArray<>();
198             }
199             return parseVibrations(parser, vibratorInfo);
200         } catch (XmlPullParserException | XmlParserException | IOException e) {
201             Slog.e(TAG, "Error parsing haptic feedback customizations from file", e);
202             return new SparseArray<>();
203         }
204     }
205 
206     /**
207      * Parses the haptic feedback vibration customization XML resource for the device.
208      *
209      * @return Return a mapping of the customized effect IDs to their respective
210      * {@link VibrationEffect}s.
211      */
212     @NonNull
loadCustomizedFeedbackVibrationFromRes( Resources res, VibratorInfo vibratorInfo, int xmlResId)213     private static SparseArray<VibrationEffect> loadCustomizedFeedbackVibrationFromRes(
214             Resources res, VibratorInfo vibratorInfo, int xmlResId) {
215         try {
216             TypedXmlPullParser parser = readCustomizationResources(res, xmlResId);
217             if (parser == null) {
218                 Slog.d(TAG, "No loadable haptic feedback customization from res.");
219                 return new SparseArray<>();
220             }
221             return parseVibrations(parser, vibratorInfo);
222         } catch (XmlPullParserException | XmlParserException | IOException e) {
223             Slog.e(TAG, "Error parsing haptic feedback customizations from res", e);
224             return new SparseArray<>();
225         }
226     }
227 
228     // TODO(b/356412421): deprecate old path related files.
readCustomizationFile(Resources res)229     private static TypedXmlPullParser readCustomizationFile(Resources res)
230             throws XmlPullParserException {
231         String customizationFile;
232         try {
233             customizationFile = res.getString(
234                     R.string.config_hapticFeedbackCustomizationFile);
235         } catch (Resources.NotFoundException e) {
236             Slog.e(TAG, "Customization file directory config not found.", e);
237             return null;
238         }
239 
240         if (TextUtils.isEmpty(customizationFile)) {
241             return null;
242         }
243 
244         final Reader customizationReader;
245         try {
246             customizationReader = new FileReader(customizationFile);
247         } catch (FileNotFoundException e) {
248             Slog.e(TAG, "Specified customization file not found.", e);
249             return null;
250         }
251 
252         final TypedXmlPullParser parser;
253         parser = Xml.newFastPullParser();
254         parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
255         parser.setInput(customizationReader);
256         Slog.d(TAG, "Successfully opened customization file.");
257         return parser;
258     }
259 
260     @Nullable
readCustomizationResources(Resources res, int xmlResId)261     private static TypedXmlPullParser readCustomizationResources(Resources res, int xmlResId) {
262         if (!Flags.loadHapticFeedbackVibrationCustomizationFromResources()) {
263             return null;
264         }
265         final XmlResourceParser resParser;
266         try {
267             resParser = res.getXml(xmlResId);
268         } catch (Resources.NotFoundException e) {
269             Slog.e(TAG, "Haptic customization resource not found.", e);
270             return null;
271         }
272         Slog.d(TAG, "Successfully opened customization resource.");
273         return XmlUtils.makeTyped(resParser);
274     }
275 
276     @NonNull
parseVibrations(TypedXmlPullParser parser, VibratorInfo vibratorInfo)277     private static SparseArray<VibrationEffect> parseVibrations(TypedXmlPullParser parser,
278             VibratorInfo vibratorInfo)
279             throws XmlPullParserException, IOException, XmlParserException {
280         XmlUtils.beginDocument(parser, TAG_CONSTANTS);
281         XmlValidator.checkTagHasNoUnexpectedAttributes(parser);
282         int rootDepth = parser.getDepth();
283 
284         SparseArray<VibrationEffect> mapping = new SparseArray<>();
285         while (XmlReader.readNextTagWithin(parser, rootDepth)) {
286             XmlValidator.checkStartTag(parser, TAG_CONSTANT);
287             int customizationDepth = parser.getDepth();
288 
289             // Only attribute in tag is the `id` attribute.
290             XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_ID);
291             int effectId = XmlReader.readAttributeIntNonNegative(parser, ATTRIBUTE_ID);
292             if (mapping.contains(effectId)) {
293                 Slog.e(TAG, "Multiple customizations found for effect " + effectId);
294                 return new SparseArray<>();
295             }
296 
297             // Move the parser one step into the `<constant>` tag.
298             XmlValidator.checkParserCondition(
299                     XmlReader.readNextTagWithin(parser, customizationDepth),
300                     "Unsupported empty customization tag for effect " + effectId);
301 
302             ParsedVibration parsedVibration = VibrationXmlParser.parseElement(
303                     parser, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS);
304             VibrationEffect effect = parsedVibration.resolve(vibratorInfo);
305             if (effect != null) {
306                 if (effect.getDuration() == Long.MAX_VALUE) {
307                     Slog.e(TAG, String.format(Locale.getDefault(),
308                             "Vibration for effect ID %d is repeating, which is not allowed as a"
309                                     + " haptic feedback: %s", effectId, effect));
310                     return new SparseArray<>();
311                 }
312                 mapping.put(effectId, effect);
313             }
314 
315             XmlReader.readEndTag(parser, TAG_CONSTANT, customizationDepth);
316         }
317 
318         // Make checks that the XML ends well.
319         XmlReader.readEndTag(parser, TAG_CONSTANTS, rootDepth);
320         XmlReader.readDocumentEndTag(parser);
321 
322         return mapping;
323     }
324 }
325