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