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