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