1 /* 2 * Copyright 2024 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.input; 18 19 import android.hardware.input.AppLaunchData; 20 import android.hardware.input.InputGestureData; 21 import android.os.Environment; 22 import android.util.AtomicFile; 23 import android.util.Slog; 24 import android.util.SparseArray; 25 import android.util.Xml; 26 27 import com.android.internal.annotations.VisibleForTesting; 28 import com.android.modules.utils.TypedXmlPullParser; 29 import com.android.modules.utils.TypedXmlSerializer; 30 31 import org.xmlpull.v1.XmlPullParser; 32 import org.xmlpull.v1.XmlPullParserException; 33 34 import java.io.File; 35 import java.io.FileNotFoundException; 36 import java.io.FileOutputStream; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.io.OutputStream; 40 import java.nio.charset.StandardCharsets; 41 import java.util.ArrayList; 42 import java.util.List; 43 44 /** 45 * Manages persistent state recorded by the input manager service as a set of XML files. 46 * Caller must acquire lock on the data store before accessing it. 47 */ 48 public final class InputDataStore { 49 private static final String TAG = "InputDataStore"; 50 51 private static final String INPUT_MANAGER_DIRECTORY = "input"; 52 53 private static final String TAG_ROOT = "root"; 54 55 private static final String TAG_INPUT_GESTURE_LIST = "input_gesture_list"; 56 private static final String TAG_INPUT_GESTURE = "input_gesture"; 57 private static final String TAG_KEY_TRIGGER = "key_trigger"; 58 private static final String TAG_TOUCHPAD_TRIGGER = "touchpad_trigger"; 59 private static final String TAG_APP_LAUNCH_DATA = "app_launch_data"; 60 61 private static final String ATTR_KEY_TRIGGER_KEYCODE = "keycode"; 62 private static final String ATTR_KEY_TRIGGER_MODIFIER_STATE = "modifiers"; 63 private static final String ATTR_KEY_GESTURE_TYPE = "key_gesture_type"; 64 private static final String ATTR_TOUCHPAD_TRIGGER_GESTURE_TYPE = "touchpad_gesture_type"; 65 private static final String ATTR_APP_LAUNCH_DATA_CATEGORY = "category"; 66 private static final String ATTR_APP_LAUNCH_DATA_ROLE = "role"; 67 private static final String ATTR_APP_LAUNCH_DATA_PACKAGE_NAME = "package_name"; 68 private static final String ATTR_APP_LAUNCH_DATA_CLASS_NAME = "class_name"; 69 70 private final FileInjector mInputGestureFileInjector; 71 InputDataStore()72 public InputDataStore() { 73 this(new FileInjector("input_gestures.xml")); 74 } 75 InputDataStore(final FileInjector inputGestureFileInjector)76 public InputDataStore(final FileInjector inputGestureFileInjector) { 77 mInputGestureFileInjector = inputGestureFileInjector; 78 } 79 80 /** 81 * Reads from the local disk storage the list of customized input gestures. 82 * 83 * @param userId The user id to fetch the gestures for. 84 * @return List of {@link InputGestureData} which the user previously customized. 85 */ loadInputGestures(int userId)86 public List<InputGestureData> loadInputGestures(int userId) { 87 List<InputGestureData> inputGestureDataList; 88 try { 89 final InputStream inputStream = mInputGestureFileInjector.openRead(userId); 90 inputGestureDataList = readInputGesturesXml(inputStream, false); 91 inputStream.close(); 92 } catch (FileNotFoundException exception) { 93 // There are valid reasons for the file to be missing, such as shortcuts having not 94 // been registered by the user. 95 return List.of(); 96 } catch (IOException exception) { 97 // In case we are unable to read from the file on disk or another IO operation error, 98 // fail gracefully. 99 Slog.e(TAG, "Failed to read from " + mInputGestureFileInjector.getAtomicFileForUserId( 100 userId), exception); 101 return List.of(); 102 } catch (Exception exception) { 103 // In the case of any other exception, we want it to bubble up as this would be due 104 // to malformed trusted XML data. 105 throw new RuntimeException( 106 "Failed to read from " + mInputGestureFileInjector.getAtomicFileForUserId( 107 userId), exception); 108 } 109 return inputGestureDataList; 110 } 111 112 /** 113 * Writes to the local disk storage the list of customized input gestures provided as a param. 114 * 115 * @param userId The user id to store the {@link InputGestureData} list under. 116 * @param inputGestureDataList The list of custom input gestures for the given {@code userId}. 117 */ saveInputGestures(int userId, List<InputGestureData> inputGestureDataList)118 public void saveInputGestures(int userId, List<InputGestureData> inputGestureDataList) { 119 FileOutputStream outputStream = null; 120 try { 121 outputStream = mInputGestureFileInjector.startWrite(userId); 122 writeInputGestureXml(outputStream, false, inputGestureDataList); 123 mInputGestureFileInjector.finishWrite(userId, outputStream, true); 124 } catch (IOException e) { 125 Slog.e(TAG, 126 "Failed to write to file " + mInputGestureFileInjector.getAtomicFileForUserId( 127 userId), e); 128 mInputGestureFileInjector.finishWrite(userId, outputStream, false); 129 } 130 } 131 132 /** 133 * Parses the given input stream and returns the list of {@link InputGestureData} objects. 134 * This parsing happens on a best effort basis. If invalid data exists in the given payload 135 * it will be skipped. An example of this would be a keycode that does not exist in the 136 * present version of Android. If the payload is malformed, instead this will throw an 137 * exception and require the caller to handel this appropriately for its situation. 138 * 139 * @param stream stream of the input payload of XML data 140 * @param utf8Encoded whether or not the input data is UTF-8 encoded 141 * @return list of {@link InputGestureData} objects pulled from the payload 142 * @throws XmlPullParserException 143 * @throws IOException 144 */ readInputGesturesXml(InputStream stream, boolean utf8Encoded)145 public List<InputGestureData> readInputGesturesXml(InputStream stream, boolean utf8Encoded) 146 throws XmlPullParserException, IOException { 147 List<InputGestureData> inputGestureDataList = new ArrayList<>(); 148 TypedXmlPullParser parser; 149 if (utf8Encoded) { 150 parser = Xml.newFastPullParser(); 151 parser.setInput(stream, StandardCharsets.UTF_8.name()); 152 } else { 153 parser = Xml.resolvePullParser(stream); 154 } 155 int type; 156 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 157 if (type != XmlPullParser.START_TAG) { 158 continue; 159 } 160 final String tag = parser.getName(); 161 if (TAG_ROOT.equals(tag)) { 162 continue; 163 } 164 165 if (TAG_INPUT_GESTURE_LIST.equals(tag)) { 166 inputGestureDataList.addAll(readInputGestureListFromXml(parser)); 167 } 168 } 169 return inputGestureDataList; 170 } 171 172 /** 173 * Serializes the given list of {@link InputGestureData} objects to XML in the provided output 174 * stream. 175 * 176 * @param stream output stream to put serialized data. 177 * @param utf8Encoded whether or not to encode the serialized data in UTF-8 format. 178 * @param inputGestureDataList the list of {@link InputGestureData} objects to serialize. 179 */ writeInputGestureXml(OutputStream stream, boolean utf8Encoded, List<InputGestureData> inputGestureDataList)180 public void writeInputGestureXml(OutputStream stream, boolean utf8Encoded, 181 List<InputGestureData> inputGestureDataList) throws IOException { 182 final TypedXmlSerializer serializer; 183 if (utf8Encoded) { 184 serializer = Xml.newFastSerializer(); 185 serializer.setOutput(stream, StandardCharsets.UTF_8.name()); 186 } else { 187 serializer = Xml.resolveSerializer(stream); 188 } 189 190 serializer.startDocument(null, true); 191 serializer.startTag(null, TAG_ROOT); 192 writeInputGestureListToXml(serializer, inputGestureDataList); 193 serializer.endTag(null, TAG_ROOT); 194 serializer.endDocument(); 195 } 196 readInputGestureFromXml(TypedXmlPullParser parser)197 private InputGestureData readInputGestureFromXml(TypedXmlPullParser parser) 198 throws XmlPullParserException, IOException, IllegalArgumentException { 199 InputGestureData.Builder builder = new InputGestureData.Builder(); 200 builder.setKeyGestureType(parser.getAttributeInt(null, ATTR_KEY_GESTURE_TYPE)); 201 int outerDepth = parser.getDepth(); 202 int type; 203 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 204 // If the parser has left the initial scope when it was called, break out. 205 if (outerDepth > parser.getDepth()) { 206 throw new RuntimeException( 207 "Parser has left the initial scope of the tag that was being parsed on " 208 + "line number: " 209 + parser.getLineNumber()); 210 } 211 212 // If the parser has reached the closing tag for the Input Gesture, break out. 213 if (type == XmlPullParser.END_TAG && parser.getName().equals(TAG_INPUT_GESTURE)) { 214 break; 215 } 216 217 if (type != XmlPullParser.START_TAG) { 218 continue; 219 } 220 221 final String tag = parser.getName(); 222 if (TAG_KEY_TRIGGER.equals(tag)) { 223 builder.setTrigger(InputGestureData.createKeyTrigger( 224 parser.getAttributeInt(null, ATTR_KEY_TRIGGER_KEYCODE), 225 parser.getAttributeInt(null, ATTR_KEY_TRIGGER_MODIFIER_STATE))); 226 } else if (TAG_TOUCHPAD_TRIGGER.equals(tag)) { 227 builder.setTrigger(InputGestureData.createTouchpadTrigger( 228 parser.getAttributeInt(null, ATTR_TOUCHPAD_TRIGGER_GESTURE_TYPE))); 229 } else if (TAG_APP_LAUNCH_DATA.equals(tag)) { 230 final String roleValue = parser.getAttributeValue(null, ATTR_APP_LAUNCH_DATA_ROLE); 231 final String categoryValue = parser.getAttributeValue(null, 232 ATTR_APP_LAUNCH_DATA_CATEGORY); 233 final String classNameValue = parser.getAttributeValue(null, 234 ATTR_APP_LAUNCH_DATA_CLASS_NAME); 235 final String packageNameValue = parser.getAttributeValue(null, 236 ATTR_APP_LAUNCH_DATA_PACKAGE_NAME); 237 final AppLaunchData appLaunchData = AppLaunchData.createLaunchData(categoryValue, 238 roleValue, packageNameValue, classNameValue); 239 if (appLaunchData != null) { 240 builder.setAppLaunchData(appLaunchData); 241 } 242 } 243 } 244 return builder.build(); 245 } 246 readInputGestureListFromXml(TypedXmlPullParser parser)247 private List<InputGestureData> readInputGestureListFromXml(TypedXmlPullParser parser) throws 248 XmlPullParserException, IOException { 249 List<InputGestureData> inputGestureDataList = new ArrayList<>(); 250 final int outerDepth = parser.getDepth(); 251 int type; 252 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 253 // If the parser has left the initial scope when it was called, break out. 254 if (outerDepth > parser.getDepth()) { 255 throw new RuntimeException( 256 "Parser has left the initial scope of the tag that was being parsed on " 257 + "line number: " 258 + parser.getLineNumber()); 259 } 260 261 // If the parser has reached the closing tag for the Input Gesture List, break out. 262 if (type == XmlPullParser.END_TAG && parser.getName().equals(TAG_INPUT_GESTURE_LIST)) { 263 break; 264 } 265 266 if (type != XmlPullParser.START_TAG) { 267 continue; 268 } 269 270 final String tag = parser.getName(); 271 if (TAG_INPUT_GESTURE.equals(tag)) { 272 try { 273 inputGestureDataList.add(readInputGestureFromXml(parser)); 274 } catch (IllegalArgumentException exception) { 275 Slog.w(TAG, "Invalid parameters for input gesture data: ", exception); 276 continue; 277 } 278 } 279 } 280 return inputGestureDataList; 281 } 282 writeInputGestureToXml(TypedXmlSerializer serializer, InputGestureData inputGestureData)283 private void writeInputGestureToXml(TypedXmlSerializer serializer, 284 InputGestureData inputGestureData) throws IOException { 285 serializer.startTag(null, TAG_INPUT_GESTURE); 286 serializer.attributeInt(null, ATTR_KEY_GESTURE_TYPE, 287 inputGestureData.getAction().keyGestureType()); 288 289 final InputGestureData.Trigger trigger = inputGestureData.getTrigger(); 290 if (trigger instanceof InputGestureData.KeyTrigger keyTrigger) { 291 serializer.startTag(null, TAG_KEY_TRIGGER); 292 serializer.attributeInt(null, ATTR_KEY_TRIGGER_KEYCODE, keyTrigger.getKeycode()); 293 serializer.attributeInt(null, ATTR_KEY_TRIGGER_MODIFIER_STATE, 294 keyTrigger.getModifierState()); 295 serializer.endTag(null, TAG_KEY_TRIGGER); 296 } else if (trigger instanceof InputGestureData.TouchpadTrigger touchpadTrigger) { 297 serializer.startTag(null, TAG_TOUCHPAD_TRIGGER); 298 serializer.attributeInt(null, ATTR_TOUCHPAD_TRIGGER_GESTURE_TYPE, 299 touchpadTrigger.getTouchpadGestureType()); 300 serializer.endTag(null, TAG_TOUCHPAD_TRIGGER); 301 } 302 303 if (inputGestureData.getAction().appLaunchData() != null) { 304 serializer.startTag(null, TAG_APP_LAUNCH_DATA); 305 final AppLaunchData appLaunchData = inputGestureData.getAction().appLaunchData(); 306 if (appLaunchData instanceof AppLaunchData.RoleData roleData) { 307 serializer.attribute(null, ATTR_APP_LAUNCH_DATA_ROLE, roleData.getRole()); 308 } else if (appLaunchData 309 instanceof AppLaunchData.CategoryData categoryData) { 310 serializer.attribute(null, ATTR_APP_LAUNCH_DATA_CATEGORY, 311 categoryData.getCategory()); 312 } else if (appLaunchData instanceof AppLaunchData.ComponentData componentData) { 313 serializer.attribute(null, ATTR_APP_LAUNCH_DATA_PACKAGE_NAME, 314 componentData.getPackageName()); 315 serializer.attribute(null, ATTR_APP_LAUNCH_DATA_CLASS_NAME, 316 componentData.getClassName()); 317 } 318 serializer.endTag(null, TAG_APP_LAUNCH_DATA); 319 } 320 321 serializer.endTag(null, TAG_INPUT_GESTURE); 322 } 323 writeInputGestureListToXml(TypedXmlSerializer serializer, List<InputGestureData> inputGestureDataList)324 private void writeInputGestureListToXml(TypedXmlSerializer serializer, 325 List<InputGestureData> inputGestureDataList) throws IOException { 326 serializer.startTag(null, TAG_INPUT_GESTURE_LIST); 327 for (final InputGestureData inputGestureData : inputGestureDataList) { 328 writeInputGestureToXml(serializer, inputGestureData); 329 } 330 serializer.endTag(null, TAG_INPUT_GESTURE_LIST); 331 } 332 333 @VisibleForTesting 334 static class FileInjector { 335 private final SparseArray<AtomicFile> mAtomicFileMap = new SparseArray<>(); 336 private final String mFileName; 337 FileInjector(String fileName)338 FileInjector(String fileName) { 339 mFileName = fileName; 340 } 341 openRead(int userId)342 InputStream openRead(int userId) throws FileNotFoundException { 343 return getAtomicFileForUserId(userId).openRead(); 344 } 345 startWrite(int userId)346 FileOutputStream startWrite(int userId) throws IOException { 347 return getAtomicFileForUserId(userId).startWrite(); 348 } 349 finishWrite(int userId, FileOutputStream os, boolean success)350 void finishWrite(int userId, FileOutputStream os, boolean success) { 351 if (success) { 352 getAtomicFileForUserId(userId).finishWrite(os); 353 } else { 354 getAtomicFileForUserId(userId).failWrite(os); 355 } 356 } 357 getAtomicFileForUserId(int userId)358 AtomicFile getAtomicFileForUserId(int userId) { 359 if (!mAtomicFileMap.contains(userId)) { 360 mAtomicFileMap.put(userId, new AtomicFile(new File( 361 Environment.buildPath(Environment.getDataSystemDeDirectory(userId), 362 INPUT_MANAGER_DIRECTORY), mFileName))); 363 } 364 return mAtomicFileMap.get(userId); 365 } 366 } 367 } 368