PersistentDataStore.java revision d6396d67201fb2b64d13070324bb115c9c23b08a
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
17package com.android.server.input;
18
19import com.android.internal.util.ArrayUtils;
20import com.android.internal.util.FastXmlSerializer;
21import com.android.internal.util.XmlUtils;
22
23import org.xmlpull.v1.XmlPullParser;
24import org.xmlpull.v1.XmlPullParserException;
25import org.xmlpull.v1.XmlSerializer;
26
27import android.hardware.input.TouchCalibration;
28import android.util.AtomicFile;
29import android.util.Slog;
30import android.util.Xml;
31
32import java.io.BufferedInputStream;
33import java.io.BufferedOutputStream;
34import java.io.File;
35import java.io.FileNotFoundException;
36import java.io.FileOutputStream;
37import java.io.IOException;
38import java.io.InputStream;
39import java.util.ArrayList;
40import java.util.Collections;
41import java.util.HashMap;
42import java.util.Map;
43import java.util.Set;
44
45import libcore.io.IoUtils;
46import libcore.util.Objects;
47
48/**
49 * Manages persistent state recorded by the input manager service as an XML file.
50 * Caller must acquire lock on the data store before accessing it.
51 *
52 * File format:
53 * <code>
54 * &lt;input-mananger-state>
55 *   &lt;input-devices>
56 *     &lt;input-device descriptor="xxxxx" keyboard-layout="yyyyy" />
57 *   &gt;input-devices>
58 * &gt;/input-manager-state>
59 * </code>
60 */
61final class PersistentDataStore {
62    static final String TAG = "InputManager";
63
64    // Input device state by descriptor.
65    private final HashMap<String, InputDeviceState> mInputDevices =
66            new HashMap<String, InputDeviceState>();
67    private final AtomicFile mAtomicFile;
68
69    // True if the data has been loaded.
70    private boolean mLoaded;
71
72    // True if there are changes to be saved.
73    private boolean mDirty;
74
75    public PersistentDataStore() {
76        mAtomicFile = new AtomicFile(new File("/data/system/input-manager-state.xml"));
77    }
78
79    public void saveIfNeeded() {
80        if (mDirty) {
81            save();
82            mDirty = false;
83        }
84    }
85
86    public TouchCalibration getTouchCalibration(String inputDeviceDescriptor) {
87        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, false);
88        if (state == null) {
89            return TouchCalibration.IDENTITY;
90        }
91        else {
92            return state.getTouchCalibration();
93        }
94    }
95
96    public boolean setTouchCalibration(String inputDeviceDescriptor, TouchCalibration calibration) {
97        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, true);
98        if (state.setTouchCalibration(calibration)) {
99            setDirty();
100            return true;
101        }
102        return false;
103    }
104
105    public String getCurrentKeyboardLayout(String inputDeviceDescriptor) {
106        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, false);
107        return state != null ? state.getCurrentKeyboardLayout() : null;
108    }
109
110    public boolean setCurrentKeyboardLayout(String inputDeviceDescriptor,
111            String keyboardLayoutDescriptor) {
112        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, true);
113        if (state.setCurrentKeyboardLayout(keyboardLayoutDescriptor)) {
114            setDirty();
115            return true;
116        }
117        return false;
118    }
119
120    public String[] getKeyboardLayouts(String inputDeviceDescriptor) {
121        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, false);
122        if (state == null) {
123            return (String[])ArrayUtils.emptyArray(String.class);
124        }
125        return state.getKeyboardLayouts();
126    }
127
128    public boolean addKeyboardLayout(String inputDeviceDescriptor,
129            String keyboardLayoutDescriptor) {
130        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, true);
131        if (state.addKeyboardLayout(keyboardLayoutDescriptor)) {
132            setDirty();
133            return true;
134        }
135        return false;
136    }
137
138    public boolean removeKeyboardLayout(String inputDeviceDescriptor,
139            String keyboardLayoutDescriptor) {
140        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, true);
141        if (state.removeKeyboardLayout(keyboardLayoutDescriptor)) {
142            setDirty();
143            return true;
144        }
145        return false;
146    }
147
148    public boolean switchKeyboardLayout(String inputDeviceDescriptor, int direction) {
149        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, false);
150        if (state != null && state.switchKeyboardLayout(direction)) {
151            setDirty();
152            return true;
153        }
154        return false;
155    }
156
157    public boolean removeUninstalledKeyboardLayouts(Set<String> availableKeyboardLayouts) {
158        boolean changed = false;
159        for (InputDeviceState state : mInputDevices.values()) {
160            if (state.removeUninstalledKeyboardLayouts(availableKeyboardLayouts)) {
161                changed = true;
162            }
163        }
164        if (changed) {
165            setDirty();
166            return true;
167        }
168        return false;
169    }
170
171    private InputDeviceState getInputDeviceState(String inputDeviceDescriptor,
172            boolean createIfAbsent) {
173        loadIfNeeded();
174        InputDeviceState state = mInputDevices.get(inputDeviceDescriptor);
175        if (state == null && createIfAbsent) {
176            state = new InputDeviceState();
177            mInputDevices.put(inputDeviceDescriptor, state);
178            setDirty();
179        }
180        return state;
181    }
182
183    private void loadIfNeeded() {
184        if (!mLoaded) {
185            load();
186            mLoaded = true;
187        }
188    }
189
190    private void setDirty() {
191        mDirty = true;
192    }
193
194    private void clearState() {
195        mInputDevices.clear();
196    }
197
198    private void load() {
199        clearState();
200
201        final InputStream is;
202        try {
203            is = mAtomicFile.openRead();
204        } catch (FileNotFoundException ex) {
205            return;
206        }
207
208        XmlPullParser parser;
209        try {
210            parser = Xml.newPullParser();
211            parser.setInput(new BufferedInputStream(is), null);
212            loadFromXml(parser);
213        } catch (IOException ex) {
214            Slog.w(InputManagerService.TAG, "Failed to load input manager persistent store data.", ex);
215            clearState();
216        } catch (XmlPullParserException ex) {
217            Slog.w(InputManagerService.TAG, "Failed to load input manager persistent store data.", ex);
218            clearState();
219        } finally {
220            IoUtils.closeQuietly(is);
221        }
222    }
223
224    private void save() {
225        final FileOutputStream os;
226        try {
227            os = mAtomicFile.startWrite();
228            boolean success = false;
229            try {
230                XmlSerializer serializer = new FastXmlSerializer();
231                serializer.setOutput(new BufferedOutputStream(os), "utf-8");
232                saveToXml(serializer);
233                serializer.flush();
234                success = true;
235            } finally {
236                if (success) {
237                    mAtomicFile.finishWrite(os);
238                } else {
239                    mAtomicFile.failWrite(os);
240                }
241            }
242        } catch (IOException ex) {
243            Slog.w(InputManagerService.TAG, "Failed to save input manager persistent store data.", ex);
244        }
245    }
246
247    private void loadFromXml(XmlPullParser parser)
248            throws IOException, XmlPullParserException {
249        XmlUtils.beginDocument(parser, "input-manager-state");
250        final int outerDepth = parser.getDepth();
251        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
252            if (parser.getName().equals("input-devices")) {
253                loadInputDevicesFromXml(parser);
254            }
255        }
256    }
257
258    private void loadInputDevicesFromXml(XmlPullParser parser)
259            throws IOException, XmlPullParserException {
260        final int outerDepth = parser.getDepth();
261        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
262            if (parser.getName().equals("input-device")) {
263                String descriptor = parser.getAttributeValue(null, "descriptor");
264                if (descriptor == null) {
265                    throw new XmlPullParserException(
266                            "Missing descriptor attribute on input-device.");
267                }
268                if (mInputDevices.containsKey(descriptor)) {
269                    throw new XmlPullParserException("Found duplicate input device.");
270                }
271
272                InputDeviceState state = new InputDeviceState();
273                state.loadFromXml(parser);
274                mInputDevices.put(descriptor, state);
275            }
276        }
277    }
278
279    private void saveToXml(XmlSerializer serializer) throws IOException {
280        serializer.startDocument(null, true);
281        serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
282        serializer.startTag(null, "input-manager-state");
283        serializer.startTag(null, "input-devices");
284        for (Map.Entry<String, InputDeviceState> entry : mInputDevices.entrySet()) {
285            final String descriptor = entry.getKey();
286            final InputDeviceState state = entry.getValue();
287            serializer.startTag(null, "input-device");
288            serializer.attribute(null, "descriptor", descriptor);
289            state.saveToXml(serializer);
290            serializer.endTag(null, "input-device");
291        }
292        serializer.endTag(null, "input-devices");
293        serializer.endTag(null, "input-manager-state");
294        serializer.endDocument();
295    }
296
297    private static final class InputDeviceState {
298        private static final String[] CALIBRATION_NAME = { "x_scale",
299                "x_ymix", "x_offset", "y_xmix", "y_scale", "y_offset" };
300
301        private TouchCalibration mTouchCalibration = TouchCalibration.IDENTITY;
302        private String mCurrentKeyboardLayout;
303        private ArrayList<String> mKeyboardLayouts = new ArrayList<String>();
304
305        public TouchCalibration getTouchCalibration() {
306            return mTouchCalibration;
307        }
308
309        public boolean setTouchCalibration(TouchCalibration calibration) {
310            if (calibration.equals(mTouchCalibration)) {
311                return false;
312            }
313            mTouchCalibration = calibration;
314            return true;
315        }
316
317        public String getCurrentKeyboardLayout() {
318            return mCurrentKeyboardLayout;
319        }
320
321        public boolean setCurrentKeyboardLayout(String keyboardLayout) {
322            if (Objects.equal(mCurrentKeyboardLayout, keyboardLayout)) {
323                return false;
324            }
325            addKeyboardLayout(keyboardLayout);
326            mCurrentKeyboardLayout = keyboardLayout;
327            return true;
328        }
329
330        public String[] getKeyboardLayouts() {
331            if (mKeyboardLayouts.isEmpty()) {
332                return (String[])ArrayUtils.emptyArray(String.class);
333            }
334            return mKeyboardLayouts.toArray(new String[mKeyboardLayouts.size()]);
335        }
336
337        public boolean addKeyboardLayout(String keyboardLayout) {
338            int index = Collections.binarySearch(mKeyboardLayouts, keyboardLayout);
339            if (index >= 0) {
340                return false;
341            }
342            mKeyboardLayouts.add(-index - 1, keyboardLayout);
343            if (mCurrentKeyboardLayout == null) {
344                mCurrentKeyboardLayout = keyboardLayout;
345            }
346            return true;
347        }
348
349        public boolean removeKeyboardLayout(String keyboardLayout) {
350            int index = Collections.binarySearch(mKeyboardLayouts, keyboardLayout);
351            if (index < 0) {
352                return false;
353            }
354            mKeyboardLayouts.remove(index);
355            updateCurrentKeyboardLayoutIfRemoved(keyboardLayout, index);
356            return true;
357        }
358
359        private void updateCurrentKeyboardLayoutIfRemoved(
360                String removedKeyboardLayout, int removedIndex) {
361            if (Objects.equal(mCurrentKeyboardLayout, removedKeyboardLayout)) {
362                if (!mKeyboardLayouts.isEmpty()) {
363                    int index = removedIndex;
364                    if (index == mKeyboardLayouts.size()) {
365                        index = 0;
366                    }
367                    mCurrentKeyboardLayout = mKeyboardLayouts.get(index);
368                } else {
369                    mCurrentKeyboardLayout = null;
370                }
371            }
372        }
373
374        public boolean switchKeyboardLayout(int direction) {
375            final int size = mKeyboardLayouts.size();
376            if (size < 2) {
377                return false;
378            }
379            int index = Collections.binarySearch(mKeyboardLayouts, mCurrentKeyboardLayout);
380            assert index >= 0;
381            if (direction > 0) {
382                index = (index + 1) % size;
383            } else {
384                index = (index + size - 1) % size;
385            }
386            mCurrentKeyboardLayout = mKeyboardLayouts.get(index);
387            return true;
388        }
389
390        public boolean removeUninstalledKeyboardLayouts(Set<String> availableKeyboardLayouts) {
391            boolean changed = false;
392            for (int i = mKeyboardLayouts.size(); i-- > 0; ) {
393                String keyboardLayout = mKeyboardLayouts.get(i);
394                if (!availableKeyboardLayouts.contains(keyboardLayout)) {
395                    Slog.i(TAG, "Removing uninstalled keyboard layout " + keyboardLayout);
396                    mKeyboardLayouts.remove(i);
397                    updateCurrentKeyboardLayoutIfRemoved(keyboardLayout, i);
398                    changed = true;
399                }
400            }
401            return changed;
402        }
403
404        public void loadFromXml(XmlPullParser parser)
405                throws IOException, XmlPullParserException {
406            final int outerDepth = parser.getDepth();
407            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
408                if (parser.getName().equals("keyboard-layout")) {
409                    String descriptor = parser.getAttributeValue(null, "descriptor");
410                    if (descriptor == null) {
411                        throw new XmlPullParserException(
412                                "Missing descriptor attribute on keyboard-layout.");
413                    }
414                    String current = parser.getAttributeValue(null, "current");
415                    if (mKeyboardLayouts.contains(descriptor)) {
416                        throw new XmlPullParserException(
417                                "Found duplicate keyboard layout.");
418                    }
419
420                    mKeyboardLayouts.add(descriptor);
421                    if (current != null && current.equals("true")) {
422                        if (mCurrentKeyboardLayout != null) {
423                            throw new XmlPullParserException(
424                                    "Found multiple current keyboard layouts.");
425                        }
426                        mCurrentKeyboardLayout = descriptor;
427                    }
428                } else if (parser.getName().equals("calibration")) {
429                    String format = parser.getAttributeValue(null, "format");
430                    if (format == null) {
431                        throw new XmlPullParserException(
432                                "Missing format attribute on calibration.");
433                    }
434                    if (format.equals("affine")) {
435                        float[] matrix = TouchCalibration.IDENTITY.getAffineTransform();
436                        int depth = parser.getDepth();
437                        while (XmlUtils.nextElementWithin(parser, depth)) {
438                            String tag = parser.getName().toLowerCase();
439                            String value = parser.nextText();
440
441                            for (int i = 0; i < matrix.length && i < CALIBRATION_NAME.length; i++) {
442                                if (tag.equals(CALIBRATION_NAME[i])) {
443                                    matrix[i] = Float.parseFloat(value);
444                                    break;
445                                }
446                            }
447                        }
448                        mTouchCalibration = new TouchCalibration(matrix[0], matrix[1], matrix[2],
449                                matrix[3], matrix[4], matrix[5]);
450                    } else {
451                        throw new XmlPullParserException("Unsupported format for calibration.");
452                    }
453                }
454            }
455
456            // Maintain invariant that layouts are sorted.
457            Collections.sort(mKeyboardLayouts);
458
459            // Maintain invariant that there is always a current keyboard layout unless
460            // there are none installed.
461            if (mCurrentKeyboardLayout == null && !mKeyboardLayouts.isEmpty()) {
462                mCurrentKeyboardLayout = mKeyboardLayouts.get(0);
463            }
464        }
465
466        public void saveToXml(XmlSerializer serializer) throws IOException {
467            for (String layout : mKeyboardLayouts) {
468                serializer.startTag(null, "keyboard-layout");
469                serializer.attribute(null, "descriptor", layout);
470                if (layout.equals(mCurrentKeyboardLayout)) {
471                    serializer.attribute(null, "current", "true");
472                }
473                serializer.endTag(null, "keyboard-layout");
474            }
475
476            serializer.startTag(null, "calibration");
477            serializer.attribute(null, "format", "affine");
478            float[] transform = mTouchCalibration.getAffineTransform();
479            for (int i = 0; i < transform.length && i < CALIBRATION_NAME.length; i++) {
480                serializer.startTag(null, CALIBRATION_NAME[i]);
481                serializer.text(Float.toString(transform[i]));
482                serializer.endTag(null, CALIBRATION_NAME[i]);
483            }
484            serializer.endTag(null, "calibration");
485        }
486    }
487}
488