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