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