Home | History | Annotate | Download | only in input
      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 com.android.internal.util.ArrayUtils;
     20 import com.android.internal.util.FastXmlSerializer;
     21 import com.android.internal.util.XmlUtils;
     22 
     23 import org.xmlpull.v1.XmlPullParser;
     24 import org.xmlpull.v1.XmlPullParserException;
     25 import org.xmlpull.v1.XmlSerializer;
     26 
     27 import android.util.AtomicFile;
     28 import android.util.Slog;
     29 import android.util.Xml;
     30 
     31 import java.io.BufferedInputStream;
     32 import java.io.BufferedOutputStream;
     33 import java.io.File;
     34 import java.io.FileNotFoundException;
     35 import java.io.FileOutputStream;
     36 import java.io.IOException;
     37 import java.io.InputStream;
     38 import java.util.ArrayList;
     39 import java.util.Collections;
     40 import java.util.HashMap;
     41 import java.util.Map;
     42 import java.util.Set;
     43 
     44 import libcore.io.IoUtils;
     45 import 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  */
     60 final 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 }