1 /*
2  * Copyright (C) 2012 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.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.hardware.input.TouchCalibration;
22 import android.util.ArrayMap;
23 import android.util.AtomicFile;
24 import android.util.Slog;
25 import android.util.SparseIntArray;
26 import android.util.Xml;
27 import android.view.Surface;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.internal.util.XmlUtils;
31 import com.android.modules.utils.TypedXmlPullParser;
32 import com.android.modules.utils.TypedXmlSerializer;
33 
34 import libcore.io.IoUtils;
35 
36 import org.xmlpull.v1.XmlPullParserException;
37 
38 import java.io.File;
39 import java.io.FileNotFoundException;
40 import java.io.FileOutputStream;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.util.ArrayList;
44 import java.util.HashMap;
45 import java.util.HashSet;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Objects;
49 import java.util.OptionalInt;
50 import java.util.Set;
51 
52 /**
53  * Manages persistent state recorded by the input manager service as an XML file.
54  * Caller must acquire lock on the data store before accessing it.
55  *
56  * File format:
57  * <code>
58  * &lt;input-mananger-state>
59  *   &lt;input-devices>
60  *     &lt;input-device descriptor="xxxxx" keyboard-layout="yyyyy" />
61  *   &gt;input-devices>
62  * &gt;/input-manager-state>
63  * </code>
64  */
65 final class PersistentDataStore {
66     static final String TAG = "InputManager";
67 
68     private static final int INVALID_VALUE = -1;
69 
70     // Input device state by descriptor.
71     private final HashMap<String, InputDeviceState> mInputDevices =
72             new HashMap<String, InputDeviceState>();
73 
74     // The interface for methods which should be replaced by the test harness.
75     private final Injector mInjector;
76 
77     // True if the data has been loaded.
78     private boolean mLoaded;
79 
80     // True if there are changes to be saved.
81     private boolean mDirty;
82 
83     // Storing key remapping
84     private final Map<Integer, Integer> mKeyRemapping = new HashMap<>();
85 
PersistentDataStore()86     public PersistentDataStore() {
87         this(new Injector());
88     }
89 
90     @VisibleForTesting
PersistentDataStore(Injector injector)91     PersistentDataStore(Injector injector) {
92         mInjector = injector;
93     }
94 
saveIfNeeded()95     public void saveIfNeeded() {
96         if (mDirty) {
97             save();
98             mDirty = false;
99         }
100     }
101 
hasInputDeviceEntry(String inputDeviceDescriptor)102     public boolean hasInputDeviceEntry(String inputDeviceDescriptor) {
103         return getInputDeviceState(inputDeviceDescriptor) != null;
104     }
105 
getTouchCalibration(String inputDeviceDescriptor, int surfaceRotation)106     public TouchCalibration getTouchCalibration(String inputDeviceDescriptor, int surfaceRotation) {
107         InputDeviceState state = getInputDeviceState(inputDeviceDescriptor);
108         if (state == null) {
109             return TouchCalibration.IDENTITY;
110         }
111 
112         TouchCalibration cal = state.getTouchCalibration(surfaceRotation);
113         if (cal == null) {
114             return TouchCalibration.IDENTITY;
115         }
116         return cal;
117     }
118 
setTouchCalibration(String inputDeviceDescriptor, int surfaceRotation, TouchCalibration calibration)119     public boolean setTouchCalibration(String inputDeviceDescriptor, int surfaceRotation, TouchCalibration calibration) {
120         InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor);
121 
122         if (state.setTouchCalibration(surfaceRotation, calibration)) {
123             setDirty();
124             return true;
125         }
126 
127         return false;
128     }
129 
130     @Nullable
getKeyboardLayout(String inputDeviceDescriptor, String key)131     public String getKeyboardLayout(String inputDeviceDescriptor, String key) {
132         InputDeviceState state = getInputDeviceState(inputDeviceDescriptor);
133         return state != null ? state.getKeyboardLayout(key) : null;
134     }
135 
setKeyboardLayout(String inputDeviceDescriptor, String key, String keyboardLayoutDescriptor)136     public boolean setKeyboardLayout(String inputDeviceDescriptor, String key,
137             String keyboardLayoutDescriptor) {
138         InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor);
139         if (state.setKeyboardLayout(key, keyboardLayoutDescriptor)) {
140             setDirty();
141             return true;
142         }
143         return false;
144     }
145 
setSelectedKeyboardLayouts(String inputDeviceDescriptor, @NonNull Set<String> selectedLayouts)146     public boolean setSelectedKeyboardLayouts(String inputDeviceDescriptor,
147             @NonNull Set<String> selectedLayouts) {
148         InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor);
149         if (state.setSelectedKeyboardLayouts(selectedLayouts)) {
150             setDirty();
151             return true;
152         }
153         return false;
154     }
155 
setKeyboardBacklightBrightness(String inputDeviceDescriptor, int lightId, int brightness)156     public boolean setKeyboardBacklightBrightness(String inputDeviceDescriptor, int lightId,
157             int brightness) {
158         InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor);
159         if (state.setKeyboardBacklightBrightness(lightId, brightness)) {
160             setDirty();
161             return true;
162         }
163         return false;
164     }
165 
getKeyboardBacklightBrightness(String inputDeviceDescriptor, int lightId)166     public OptionalInt getKeyboardBacklightBrightness(String inputDeviceDescriptor, int lightId) {
167         InputDeviceState state = getInputDeviceState(inputDeviceDescriptor);
168         if (state == null) {
169             return OptionalInt.empty();
170         }
171         return state.getKeyboardBacklightBrightness(lightId);
172     }
173 
remapKey(int fromKey, int toKey)174     public boolean remapKey(int fromKey, int toKey) {
175         loadIfNeeded();
176         if (mKeyRemapping.getOrDefault(fromKey, INVALID_VALUE) == toKey) {
177             return false;
178         }
179         mKeyRemapping.put(fromKey, toKey);
180         setDirty();
181         return true;
182     }
183 
clearMappedKey(int key)184     public boolean clearMappedKey(int key) {
185         loadIfNeeded();
186         if (mKeyRemapping.containsKey(key)) {
187             mKeyRemapping.remove(key);
188             setDirty();
189         }
190         return true;
191     }
192 
getKeyRemapping()193     public Map<Integer, Integer> getKeyRemapping() {
194         loadIfNeeded();
195         return new HashMap<>(mKeyRemapping);
196     }
197 
removeUninstalledKeyboardLayouts(Set<String> availableKeyboardLayouts)198     public boolean removeUninstalledKeyboardLayouts(Set<String> availableKeyboardLayouts) {
199         boolean changed = false;
200         for (InputDeviceState state : mInputDevices.values()) {
201             if (state.removeUninstalledKeyboardLayouts(availableKeyboardLayouts)) {
202                 changed = true;
203             }
204         }
205         if (changed) {
206             setDirty();
207             return true;
208         }
209         return false;
210     }
211 
getInputDeviceState(String inputDeviceDescriptor)212     private InputDeviceState getInputDeviceState(String inputDeviceDescriptor) {
213         loadIfNeeded();
214         return mInputDevices.get(inputDeviceDescriptor);
215     }
216 
getOrCreateInputDeviceState(String inputDeviceDescriptor)217     private InputDeviceState getOrCreateInputDeviceState(String inputDeviceDescriptor) {
218         loadIfNeeded();
219         InputDeviceState state = mInputDevices.get(inputDeviceDescriptor);
220         if (state == null) {
221             state = new InputDeviceState();
222             mInputDevices.put(inputDeviceDescriptor, state);
223             setDirty();
224         }
225         return state;
226     }
227 
loadIfNeeded()228     private void loadIfNeeded() {
229         if (!mLoaded) {
230             load();
231             mLoaded = true;
232         }
233     }
234 
setDirty()235     private void setDirty() {
236         mDirty = true;
237     }
238 
clearState()239     private void clearState() {
240         mKeyRemapping.clear();
241         mInputDevices.clear();
242     }
243 
load()244     private void load() {
245         clearState();
246 
247         final InputStream is;
248         try {
249             is = mInjector.openRead();
250         } catch (FileNotFoundException ex) {
251             return;
252         }
253 
254         TypedXmlPullParser parser;
255         try {
256             parser = Xml.resolvePullParser(is);
257             loadFromXml(parser);
258         } catch (IOException ex) {
259             Slog.w(InputManagerService.TAG, "Failed to load input manager persistent store data.", ex);
260             clearState();
261         } catch (XmlPullParserException ex) {
262             Slog.w(InputManagerService.TAG, "Failed to load input manager persistent store data.", ex);
263             clearState();
264         } finally {
265             IoUtils.closeQuietly(is);
266         }
267     }
268 
save()269     private void save() {
270         final FileOutputStream os;
271         try {
272             os = mInjector.startWrite();
273             boolean success = false;
274             try {
275                 TypedXmlSerializer serializer = Xml.resolveSerializer(os);
276                 saveToXml(serializer);
277                 serializer.flush();
278                 success = true;
279             } finally {
280                 mInjector.finishWrite(os, success);
281             }
282         } catch (IOException ex) {
283             Slog.w(InputManagerService.TAG, "Failed to save input manager persistent store data.", ex);
284         }
285     }
286 
loadFromXml(TypedXmlPullParser parser)287     private void loadFromXml(TypedXmlPullParser parser)
288             throws IOException, XmlPullParserException {
289         XmlUtils.beginDocument(parser, "input-manager-state");
290         final int outerDepth = parser.getDepth();
291         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
292             if (parser.getName().equals("key-remapping")) {
293                 loadKeyRemappingFromXml(parser);
294             } else if (parser.getName().equals("input-devices")) {
295                 loadInputDevicesFromXml(parser);
296             }
297         }
298     }
299 
loadInputDevicesFromXml(TypedXmlPullParser parser)300     private void loadInputDevicesFromXml(TypedXmlPullParser parser)
301             throws IOException, XmlPullParserException {
302         final int outerDepth = parser.getDepth();
303         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
304             if (parser.getName().equals("input-device")) {
305                 String descriptor = parser.getAttributeValue(null, "descriptor");
306                 if (descriptor == null) {
307                     throw new XmlPullParserException(
308                             "Missing descriptor attribute on input-device.");
309                 }
310                 if (mInputDevices.containsKey(descriptor)) {
311                     throw new XmlPullParserException("Found duplicate input device.");
312                 }
313 
314                 InputDeviceState state = new InputDeviceState();
315                 state.loadFromXml(parser);
316                 mInputDevices.put(descriptor, state);
317             }
318         }
319     }
320 
loadKeyRemappingFromXml(TypedXmlPullParser parser)321     private void loadKeyRemappingFromXml(TypedXmlPullParser parser)
322             throws IOException, XmlPullParserException {
323         final int outerDepth = parser.getDepth();
324         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
325             if (parser.getName().equals("remap")) {
326                 int fromKey = parser.getAttributeInt(null, "from-key");
327                 int toKey = parser.getAttributeInt(null, "to-key");
328                 mKeyRemapping.put(fromKey, toKey);
329             }
330         }
331     }
332 
saveToXml(TypedXmlSerializer serializer)333     private void saveToXml(TypedXmlSerializer serializer) throws IOException {
334         serializer.startDocument(null, true);
335         serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
336         serializer.startTag(null, "input-manager-state");
337         serializer.startTag(null, "key-remapping");
338         for (int fromKey : mKeyRemapping.keySet()) {
339             int toKey = mKeyRemapping.get(fromKey);
340             serializer.startTag(null, "remap");
341             serializer.attributeInt(null, "from-key", fromKey);
342             serializer.attributeInt(null, "to-key", toKey);
343             serializer.endTag(null, "remap");
344         }
345         serializer.endTag(null, "key-remapping");
346         serializer.startTag(null, "input-devices");
347         for (Map.Entry<String, InputDeviceState> entry : mInputDevices.entrySet()) {
348             final String descriptor = entry.getKey();
349             final InputDeviceState state = entry.getValue();
350             serializer.startTag(null, "input-device");
351             serializer.attribute(null, "descriptor", descriptor);
352             state.saveToXml(serializer);
353             serializer.endTag(null, "input-device");
354         }
355         serializer.endTag(null, "input-devices");
356         serializer.endTag(null, "input-manager-state");
357         serializer.endDocument();
358     }
359 
360     private static final class InputDeviceState {
361         private static final String[] CALIBRATION_NAME = { "x_scale",
362                 "x_ymix", "x_offset", "y_xmix", "y_scale", "y_offset" };
363 
364         private final TouchCalibration[] mTouchCalibration = new TouchCalibration[4];
365         private final SparseIntArray mKeyboardBacklightBrightnessMap = new SparseIntArray();
366 
367         private final Map<String, String> mKeyboardLayoutMap = new ArrayMap<>();
368 
369         private Set<String> mSelectedKeyboardLayouts;
370 
getTouchCalibration(int surfaceRotation)371         public TouchCalibration getTouchCalibration(int surfaceRotation) {
372             try {
373                 return mTouchCalibration[surfaceRotation];
374             } catch (ArrayIndexOutOfBoundsException ex) {
375                 Slog.w(InputManagerService.TAG, "Cannot get touch calibration.", ex);
376                 return null;
377             }
378         }
379 
setTouchCalibration(int surfaceRotation, TouchCalibration calibration)380         public boolean setTouchCalibration(int surfaceRotation, TouchCalibration calibration) {
381             try {
382                 if (!calibration.equals(mTouchCalibration[surfaceRotation])) {
383                     mTouchCalibration[surfaceRotation] = calibration;
384                     return true;
385                 }
386                 return false;
387             } catch (ArrayIndexOutOfBoundsException ex) {
388                 Slog.w(InputManagerService.TAG, "Cannot set touch calibration.", ex);
389                 return false;
390             }
391         }
392 
393         @Nullable
getKeyboardLayout(String key)394         public String getKeyboardLayout(String key) {
395             return mKeyboardLayoutMap.get(key);
396         }
397 
setKeyboardLayout(String key, String keyboardLayout)398         public boolean setKeyboardLayout(String key, String keyboardLayout) {
399             return !Objects.equals(mKeyboardLayoutMap.put(key, keyboardLayout), keyboardLayout);
400         }
401 
setSelectedKeyboardLayouts(@onNull Set<String> selectedLayouts)402         public boolean setSelectedKeyboardLayouts(@NonNull Set<String> selectedLayouts) {
403             if (Objects.equals(mSelectedKeyboardLayouts, selectedLayouts)) {
404                 return false;
405             }
406             mSelectedKeyboardLayouts = new HashSet<>(selectedLayouts);
407             return true;
408         }
409 
setKeyboardBacklightBrightness(int lightId, int brightness)410         public boolean setKeyboardBacklightBrightness(int lightId, int brightness) {
411             if (mKeyboardBacklightBrightnessMap.get(lightId, INVALID_VALUE) == brightness) {
412                 return false;
413             }
414             mKeyboardBacklightBrightnessMap.put(lightId, brightness);
415             return true;
416         }
417 
getKeyboardBacklightBrightness(int lightId)418         public OptionalInt getKeyboardBacklightBrightness(int lightId) {
419             int brightness = mKeyboardBacklightBrightnessMap.get(lightId, INVALID_VALUE);
420             return brightness == INVALID_VALUE ? OptionalInt.empty() : OptionalInt.of(brightness);
421         }
422 
removeUninstalledKeyboardLayouts(Set<String> availableKeyboardLayouts)423         public boolean removeUninstalledKeyboardLayouts(Set<String> availableKeyboardLayouts) {
424             boolean changed = false;
425             List<String> removedEntries = new ArrayList<>();
426             for (String key : mKeyboardLayoutMap.keySet()) {
427                 if (!availableKeyboardLayouts.contains(mKeyboardLayoutMap.get(key))) {
428                     removedEntries.add(key);
429                 }
430             }
431             if (!removedEntries.isEmpty()) {
432                 for (String key : removedEntries) {
433                     mKeyboardLayoutMap.remove(key);
434                 }
435                 changed = true;
436             }
437             return changed;
438         }
439 
loadFromXml(TypedXmlPullParser parser)440         public void loadFromXml(TypedXmlPullParser parser)
441                 throws IOException, XmlPullParserException {
442             final int outerDepth = parser.getDepth();
443             while (XmlUtils.nextElementWithin(parser, outerDepth)) {
444                 if (parser.getName().equals("keyed-keyboard-layout")) {
445                     String key = parser.getAttributeValue(null, "key");
446                     if (key == null) {
447                         throw new XmlPullParserException(
448                                 "Missing key attribute on keyed-keyboard-layout.");
449                     }
450                     String layout = parser.getAttributeValue(null, "layout");
451                     if (layout == null) {
452                         throw new XmlPullParserException(
453                                 "Missing layout attribute on keyed-keyboard-layout.");
454                     }
455                     mKeyboardLayoutMap.put(key, layout);
456                 } else if (parser.getName().equals("selected-keyboard-layout")) {
457                     String layout = parser.getAttributeValue(null, "layout");
458                     if (layout == null) {
459                         throw new XmlPullParserException(
460                                 "Missing layout attribute on selected-keyboard-layout.");
461                     }
462                     if (mSelectedKeyboardLayouts == null) {
463                         mSelectedKeyboardLayouts = new HashSet<>();
464                     }
465                     mSelectedKeyboardLayouts.add(layout);
466                 } else if (parser.getName().equals("light-info")) {
467                     int lightId = parser.getAttributeInt(null, "light-id");
468                     int lightBrightness = parser.getAttributeInt(null, "light-brightness");
469                     mKeyboardBacklightBrightnessMap.put(lightId, lightBrightness);
470                 } else if (parser.getName().equals("calibration")) {
471                     String format = parser.getAttributeValue(null, "format");
472                     String rotation = parser.getAttributeValue(null, "rotation");
473                     int r = -1;
474 
475                     if (format == null) {
476                         throw new XmlPullParserException(
477                                 "Missing format attribute on calibration.");
478                     }
479                     if (!format.equals("affine")) {
480                         throw new XmlPullParserException(
481                                 "Unsupported format for calibration.");
482                     }
483                     if (rotation != null) {
484                         try {
485                             r = stringToSurfaceRotation(rotation);
486                         } catch (IllegalArgumentException e) {
487                             throw new XmlPullParserException(
488                                     "Unsupported rotation for calibration.");
489                         }
490                     }
491 
492                     float[] matrix = TouchCalibration.IDENTITY.getAffineTransform();
493                     int depth = parser.getDepth();
494                     while (XmlUtils.nextElementWithin(parser, depth)) {
495                         String tag = parser.getName().toLowerCase();
496                         String value = parser.nextText();
497 
498                         for (int i = 0; i < matrix.length && i < CALIBRATION_NAME.length; i++) {
499                             if (tag.equals(CALIBRATION_NAME[i])) {
500                                 matrix[i] = Float.parseFloat(value);
501                                 break;
502                             }
503                         }
504                     }
505 
506                     if (r == -1) {
507                         // Assume calibration applies to all rotations
508                         for (r = 0; r < mTouchCalibration.length; r++) {
509                             mTouchCalibration[r] = new TouchCalibration(matrix[0],
510                                 matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
511                         }
512                     } else {
513                         mTouchCalibration[r] = new TouchCalibration(matrix[0],
514                             matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
515                     }
516                 }
517             }
518         }
519 
saveToXml(TypedXmlSerializer serializer)520         public void saveToXml(TypedXmlSerializer serializer) throws IOException {
521             for (String key : mKeyboardLayoutMap.keySet()) {
522                 serializer.startTag(null, "keyed-keyboard-layout");
523                 serializer.attribute(null, "key", key);
524                 serializer.attribute(null, "layout", mKeyboardLayoutMap.get(key));
525                 serializer.endTag(null, "keyed-keyboard-layout");
526             }
527 
528             if (mSelectedKeyboardLayouts != null) {
529                 for (String layout : mSelectedKeyboardLayouts) {
530                     serializer.startTag(null, "selected-keyboard-layout");
531                     serializer.attribute(null, "layout", layout);
532                     serializer.endTag(null, "selected-keyboard-layout");
533                 }
534             }
535 
536             for (int i = 0; i < mKeyboardBacklightBrightnessMap.size(); i++) {
537                 serializer.startTag(null, "light-info");
538                 serializer.attributeInt(null, "light-id", mKeyboardBacklightBrightnessMap.keyAt(i));
539                 serializer.attributeInt(null, "light-brightness",
540                         mKeyboardBacklightBrightnessMap.valueAt(i));
541                 serializer.endTag(null, "light-info");
542             }
543 
544             for (int i = 0; i < mTouchCalibration.length; i++) {
545                 if (mTouchCalibration[i] != null) {
546                     String rotation = surfaceRotationToString(i);
547                     float[] transform = mTouchCalibration[i].getAffineTransform();
548 
549                     serializer.startTag(null, "calibration");
550                     serializer.attribute(null, "format", "affine");
551                     serializer.attribute(null, "rotation", rotation);
552                     for (int j = 0; j < transform.length && j < CALIBRATION_NAME.length; j++) {
553                         serializer.startTag(null, CALIBRATION_NAME[j]);
554                         serializer.text(Float.toString(transform[j]));
555                         serializer.endTag(null, CALIBRATION_NAME[j]);
556                     }
557                     serializer.endTag(null, "calibration");
558                 }
559             }
560         }
561 
surfaceRotationToString(int surfaceRotation)562         private static String surfaceRotationToString(int surfaceRotation) {
563             switch (surfaceRotation) {
564                 case Surface.ROTATION_0:   return "0";
565                 case Surface.ROTATION_90:  return "90";
566                 case Surface.ROTATION_180: return "180";
567                 case Surface.ROTATION_270: return "270";
568             }
569             throw new IllegalArgumentException("Unsupported surface rotation value" + surfaceRotation);
570         }
571 
stringToSurfaceRotation(String s)572         private static int stringToSurfaceRotation(String s) {
573             if ("0".equals(s)) {
574                 return Surface.ROTATION_0;
575             }
576             if ("90".equals(s)) {
577                 return Surface.ROTATION_90;
578             }
579             if ("180".equals(s)) {
580                 return Surface.ROTATION_180;
581             }
582             if ("270".equals(s)) {
583                 return Surface.ROTATION_270;
584             }
585             throw new IllegalArgumentException("Unsupported surface rotation string '" + s + "'");
586         }
587     }
588 
589     @VisibleForTesting
590     static class Injector {
591         private final AtomicFile mAtomicFile;
592 
Injector()593         Injector() {
594             mAtomicFile = new AtomicFile(new File("/data/system/input-manager-state.xml"),
595                     "input-state");
596         }
597 
openRead()598         InputStream openRead() throws FileNotFoundException {
599             return mAtomicFile.openRead();
600         }
601 
startWrite()602         FileOutputStream startWrite() throws IOException {
603             return mAtomicFile.startWrite();
604         }
605 
finishWrite(FileOutputStream fos, boolean success)606         void finishWrite(FileOutputStream fos, boolean success) {
607             if (success) {
608                 mAtomicFile.finishWrite(fos);
609             } else {
610                 mAtomicFile.failWrite(fos);
611             }
612         }
613     }
614 }
615