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 * <input-mananger-state> 54 * <input-devices> 55 * <input-device descriptor="xxxxx" keyboard-layout="yyyyy" /> 56 * >input-devices> 57 * >/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}