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.sdklib.devices; 18 19import com.android.SdkConstants; 20import com.android.annotations.Nullable; 21import com.android.prefs.AndroidLocation; 22import com.android.prefs.AndroidLocation.AndroidLocationException; 23import com.android.resources.Keyboard; 24import com.android.resources.KeyboardState; 25import com.android.resources.Navigation; 26import com.android.sdklib.internal.avd.AvdManager; 27import com.android.sdklib.internal.avd.HardwareProperties; 28import com.android.sdklib.repository.PkgProps; 29import com.android.utils.ILogger; 30 31import org.xml.sax.SAXException; 32 33import java.io.BufferedReader; 34import java.io.File; 35import java.io.FileNotFoundException; 36import java.io.FileOutputStream; 37import java.io.FileReader; 38import java.io.IOException; 39import java.util.ArrayList; 40import java.util.Collection; 41import java.util.Collections; 42import java.util.HashMap; 43import java.util.Iterator; 44import java.util.List; 45import java.util.Map; 46import java.util.Set; 47import java.util.regex.Matcher; 48import java.util.regex.Pattern; 49 50import javax.xml.parsers.ParserConfigurationException; 51import javax.xml.transform.TransformerException; 52import javax.xml.transform.TransformerFactoryConfigurationError; 53 54/** 55 * Manager class for interacting with {@link Device}s within the SDK 56 */ 57public class DeviceManager { 58 59 private final static String sDeviceProfilesProp = "DeviceProfiles"; 60 private final static Pattern sPathPropertyPattern = Pattern.compile("^" + PkgProps.EXTRA_PATH 61 + "=" + sDeviceProfilesProp + "$"); 62 private ILogger mLog; 63 // Vendor devices can't be a static list since they change based on the SDK 64 // Location 65 private List<Device> mVendorDevices; 66 // Keeps track of where the currently loaded vendor devices were loaded from 67 private String mVendorDevicesLocation = ""; 68 private static List<Device> mUserDevices; 69 private static List<Device> mDefaultDevices; 70 private static final Object sLock = new Object(); 71 private static final List<DevicesChangeListener> sListeners = 72 new ArrayList<DevicesChangeListener>(); 73 74 public static enum DeviceStatus { 75 /** 76 * The device exists unchanged from the given configuration 77 */ 78 EXISTS, 79 /** 80 * A device exists with the given name and manufacturer, but has a different configuration 81 */ 82 CHANGED, 83 /** 84 * There is no device with the given name and manufacturer 85 */ 86 MISSING; 87 } 88 89 // TODO: Refactor this to look more like AvdManager so that we don't have 90 // multiple instances in the same application, which forces us to parse 91 // the XML multiple times when we don't have to. 92 public DeviceManager(ILogger log) { 93 mLog = log; 94 } 95 96 /** 97 * Interface implemented by objects which want to know when changes occur to the {@link Device} 98 * lists. 99 */ 100 public static interface DevicesChangeListener { 101 /** 102 * Called after one of the {@link Device} lists has been updated. 103 */ 104 public void onDevicesChange(); 105 } 106 107 /** 108 * Register a listener to be notified when the device lists are modified. 109 * 110 * @param listener The listener to add. Ignored if already registered. 111 */ 112 public void registerListener(DevicesChangeListener listener) { 113 if (listener != null) { 114 synchronized (sListeners) { 115 if (!sListeners.contains(listener)) { 116 sListeners.add(listener); 117 } 118 } 119 } 120 } 121 122 /** 123 * Removes a listener from the notification list such that it will no longer receive 124 * notifications when modifications to the {@link Device} list occur. 125 * 126 * @param listener The listener to remove. 127 */ 128 public boolean unregisterListener(DevicesChangeListener listener) { 129 synchronized (sListeners) { 130 return sListeners.remove(listener); 131 } 132 } 133 134 public DeviceStatus getDeviceStatus( 135 @Nullable String sdkLocation, String name, String manufacturer, int hashCode) { 136 Device d = getDevice(sdkLocation, name, manufacturer); 137 if (d == null) { 138 return DeviceStatus.MISSING; 139 } else { 140 return d.hashCode() == hashCode ? DeviceStatus.EXISTS : DeviceStatus.CHANGED; 141 } 142 } 143 144 public Device getDevice(@Nullable String sdkLocation, String name, String manufacturer) { 145 List<Device> devices; 146 if (sdkLocation != null) { 147 devices = getDevices(sdkLocation); 148 } else { 149 devices = new ArrayList<Device>(getDefaultDevices()); 150 devices.addAll(getUserDevices()); 151 } 152 for (Device d : devices) { 153 if (d.getName().equals(name) && d.getManufacturer().equals(manufacturer)) { 154 return d; 155 } 156 } 157 return null; 158 } 159 160 /** 161 * Returns both vendor provided and user created {@link Device}s. 162 * 163 * @param sdkLocation Location of the Android SDK 164 * @return A list of both vendor and user provided {@link Device}s 165 */ 166 public List<Device> getDevices(String sdkLocation) { 167 List<Device> devices = new ArrayList<Device>(getVendorDevices(sdkLocation)); 168 devices.addAll(getDefaultDevices()); 169 devices.addAll(getUserDevices()); 170 return Collections.unmodifiableList(devices); 171 } 172 173 /** 174 * Gets the {@link List} of {@link Device}s packaged with the SDK. 175 * 176 * @return The {@link List} of default {@link Device}s 177 */ 178 public List<Device> getDefaultDevices() { 179 synchronized (sLock) { 180 if (mDefaultDevices == null) { 181 try { 182 mDefaultDevices = DeviceParser.parse( 183 DeviceManager.class.getResourceAsStream(SdkConstants.FN_DEVICES_XML)); 184 } catch (IllegalStateException e) { 185 // The device builders can throw IllegalStateExceptions if 186 // build gets called before everything is properly setup 187 mLog.error(e, null); 188 mDefaultDevices = new ArrayList<Device>(); 189 } catch (Exception e) { 190 mLog.error(null, "Error reading default devices"); 191 mDefaultDevices = new ArrayList<Device>(); 192 } 193 notifyListeners(); 194 } 195 } 196 return Collections.unmodifiableList(mDefaultDevices); 197 } 198 199 /** 200 * Returns all vendor-provided {@link Device}s 201 * 202 * @param sdkLocation Location of the Android SDK 203 * @return A list of vendor-provided {@link Device}s 204 */ 205 public List<Device> getVendorDevices(String sdkLocation) { 206 synchronized (sLock) { 207 if (mVendorDevices == null || !mVendorDevicesLocation.equals(sdkLocation)) { 208 mVendorDevicesLocation = sdkLocation; 209 List<Device> devices = new ArrayList<Device>(); 210 211 // Load devices from tools folder 212 File toolsDevices = new File(sdkLocation, SdkConstants.OS_SDK_TOOLS_LIB_FOLDER + 213 File.separator + SdkConstants.FN_DEVICES_XML); 214 if (toolsDevices.isFile()) { 215 devices.addAll(loadDevices(toolsDevices)); 216 } 217 218 // Load devices from vendor extras 219 File extrasFolder = new File(sdkLocation, SdkConstants.FD_EXTRAS); 220 List<File> deviceDirs = getExtraDirs(extrasFolder); 221 for (File deviceDir : deviceDirs) { 222 File deviceXml = new File(deviceDir, SdkConstants.FN_DEVICES_XML); 223 if (deviceXml.isFile()) { 224 devices.addAll(loadDevices(deviceXml)); 225 } 226 } 227 mVendorDevices = devices; 228 notifyListeners(); 229 } 230 } 231 return Collections.unmodifiableList(mVendorDevices); 232 } 233 234 /** 235 * Returns all user-created {@link Device}s 236 * 237 * @return All user-created {@link Device}s 238 */ 239 public List<Device> getUserDevices() { 240 synchronized (sLock) { 241 if (mUserDevices == null) { 242 // User devices should be saved out to 243 // $HOME/.android/devices.xml 244 mUserDevices = new ArrayList<Device>(); 245 File userDevicesFile = null; 246 try { 247 userDevicesFile = new File(AndroidLocation.getFolder(), 248 SdkConstants.FN_DEVICES_XML); 249 if (userDevicesFile.exists()) { 250 mUserDevices.addAll(DeviceParser.parse(userDevicesFile)); 251 notifyListeners(); 252 } 253 } catch (AndroidLocationException e) { 254 mLog.warning("Couldn't load user devices: %1$s", e.getMessage()); 255 } catch (SAXException e) { 256 // Probably an old config file which we don't want to overwrite. 257 if (userDevicesFile != null) { 258 String base = userDevicesFile.getAbsoluteFile() + ".old"; 259 File renamedConfig = new File(base); 260 int i = 0; 261 while (renamedConfig.exists()) { 262 renamedConfig = new File(base + '.' + (i++)); 263 } 264 mLog.error(null, "Error parsing %1$s, backing up to %2$s", 265 userDevicesFile.getAbsolutePath(), renamedConfig.getAbsolutePath()); 266 userDevicesFile.renameTo(renamedConfig); 267 } 268 } catch (ParserConfigurationException e) { 269 mLog.error(null, "Error parsing %1$s", 270 userDevicesFile == null ? "(null)" : userDevicesFile.getAbsolutePath()); 271 } catch (IOException e) { 272 mLog.error(null, "Error parsing %1$s", 273 userDevicesFile == null ? "(null)" : userDevicesFile.getAbsolutePath()); 274 } 275 } 276 } 277 return Collections.unmodifiableList(mUserDevices); 278 } 279 280 public void addUserDevice(Device d) { 281 synchronized (sLock) { 282 if (mUserDevices == null) { 283 getUserDevices(); 284 } 285 mUserDevices.add(d); 286 } 287 notifyListeners(); 288 } 289 290 public void removeUserDevice(Device d) { 291 synchronized (sLock) { 292 if (mUserDevices == null) { 293 getUserDevices(); 294 } 295 Iterator<Device> it = mUserDevices.iterator(); 296 while (it.hasNext()) { 297 Device userDevice = it.next(); 298 if (userDevice.getName().equals(d.getName()) 299 && userDevice.getManufacturer().equals(d.getManufacturer())) { 300 it.remove(); 301 notifyListeners(); 302 break; 303 } 304 305 } 306 } 307 } 308 309 public void replaceUserDevice(Device d) { 310 synchronized (sLock) { 311 if (mUserDevices == null) { 312 getUserDevices(); 313 } 314 removeUserDevice(d); 315 addUserDevice(d); 316 } 317 } 318 319 /** 320 * Saves out the user devices to {@link SdkConstants#FN_DEVICES_XML} in 321 * {@link AndroidLocation#getFolder()}. 322 */ 323 public void saveUserDevices() { 324 synchronized (sLock) { 325 if (mUserDevices != null && mUserDevices.size() != 0) { 326 File userDevicesFile; 327 try { 328 userDevicesFile = new File(AndroidLocation.getFolder(), 329 SdkConstants.FN_DEVICES_XML); 330 DeviceWriter.writeToXml(new FileOutputStream(userDevicesFile), mUserDevices); 331 } catch (AndroidLocationException e) { 332 mLog.warning("Couldn't find user directory: %1$s", e.getMessage()); 333 } catch (FileNotFoundException e) { 334 mLog.warning("Couldn't open file: %1$s", e.getMessage()); 335 } catch (ParserConfigurationException e) { 336 mLog.warning("Error writing file: %1$s", e.getMessage()); 337 } catch (TransformerFactoryConfigurationError e) { 338 mLog.warning("Error writing file: %1$s", e.getMessage()); 339 } catch (TransformerException e) { 340 mLog.warning("Error writing file: %1$s", e.getMessage()); 341 } 342 } 343 } 344 } 345 346 /** 347 * Returns hardware properties (defined in hardware.ini) as a {@link Map}. 348 * 349 * @param s The {@link State} from which to derive the hardware properties. 350 * @return A {@link Map} of hardware properties. 351 */ 352 public static Map<String, String> getHardwareProperties(State s) { 353 Hardware hw = s.getHardware(); 354 Map<String, String> props = new HashMap<String, String>(); 355 props.put(HardwareProperties.HW_MAINKEYS, 356 getBooleanVal(hw.getButtonType().equals(ButtonType.HARD))); 357 props.put(HardwareProperties.HW_TRACKBALL, 358 getBooleanVal(hw.getNav().equals(Navigation.TRACKBALL))); 359 props.put(HardwareProperties.HW_KEYBOARD, 360 getBooleanVal(hw.getKeyboard().equals(Keyboard.QWERTY))); 361 props.put(HardwareProperties.HW_DPAD, 362 getBooleanVal(hw.getNav().equals(Navigation.DPAD))); 363 364 Set<Sensor> sensors = hw.getSensors(); 365 props.put(HardwareProperties.HW_GPS, getBooleanVal(sensors.contains(Sensor.GPS))); 366 props.put(HardwareProperties.HW_BATTERY, 367 getBooleanVal(hw.getChargeType().equals(PowerType.BATTERY))); 368 props.put(HardwareProperties.HW_ACCELEROMETER, 369 getBooleanVal(sensors.contains(Sensor.ACCELEROMETER))); 370 props.put(HardwareProperties.HW_ORIENTATION_SENSOR, 371 getBooleanVal(sensors.contains(Sensor.GYROSCOPE))); 372 props.put(HardwareProperties.HW_AUDIO_INPUT, getBooleanVal(hw.hasMic())); 373 props.put(HardwareProperties.HW_SDCARD, getBooleanVal(hw.getRemovableStorage().size() > 0)); 374 props.put(HardwareProperties.HW_LCD_DENSITY, 375 Integer.toString(hw.getScreen().getPixelDensity().getDpiValue())); 376 props.put(HardwareProperties.HW_PROXIMITY_SENSOR, 377 getBooleanVal(sensors.contains(Sensor.PROXIMITY_SENSOR))); 378 return props; 379 } 380 381 /** 382 * Returns the hardware properties defined in 383 * {@link AvdManager#HARDWARE_INI} as a {@link Map}. 384 * 385 * @param d The {@link Device} from which to derive the hardware properties. 386 * @return A {@link Map} of hardware properties. 387 */ 388 public static Map<String, String> getHardwareProperties(Device d) { 389 Map<String, String> props = getHardwareProperties(d.getDefaultState()); 390 for (State s : d.getAllStates()) { 391 if (s.getKeyState().equals(KeyboardState.HIDDEN)) { 392 props.put("hw.keyboard.lid", getBooleanVal(true)); 393 } 394 } 395 props.put(AvdManager.AVD_INI_DEVICE_HASH, Integer.toString(d.hashCode())); 396 props.put(AvdManager.AVD_INI_DEVICE_NAME, d.getName()); 397 props.put(AvdManager.AVD_INI_DEVICE_MANUFACTURER, d.getManufacturer()); 398 return props; 399 } 400 401 /** 402 * Takes a boolean and returns the appropriate value for 403 * {@link HardwareProperties} 404 * 405 * @param bool The boolean value to turn into the appropriate 406 * {@link HardwareProperties} value. 407 * @return {@code HardwareProperties#BOOLEAN_VALUES[0]} if true, 408 * {@code HardwareProperties#BOOLEAN_VALUES[1]} otherwise. 409 */ 410 private static String getBooleanVal(boolean bool) { 411 if (bool) { 412 return HardwareProperties.BOOLEAN_VALUES[0]; 413 } 414 return HardwareProperties.BOOLEAN_VALUES[1]; 415 } 416 417 private Collection<Device> loadDevices(File deviceXml) { 418 try { 419 return DeviceParser.parse(deviceXml); 420 } catch (SAXException e) { 421 mLog.error(null, "Error parsing %1$s", deviceXml.getAbsolutePath()); 422 } catch (ParserConfigurationException e) { 423 mLog.error(null, "Error parsing %1$s", deviceXml.getAbsolutePath()); 424 } catch (IOException e) { 425 mLog.error(null, "Error reading %1$s", deviceXml.getAbsolutePath()); 426 } catch (IllegalStateException e) { 427 // The device builders can throw IllegalStateExceptions if 428 // build gets called before everything is properly setup 429 mLog.error(e, null); 430 } 431 return new ArrayList<Device>(); 432 } 433 434 private void notifyListeners() { 435 synchronized (sListeners) { 436 for (DevicesChangeListener listener : sListeners) { 437 listener.onDevicesChange(); 438 } 439 } 440 } 441 442 /* Returns all of DeviceProfiles in the extras/ folder */ 443 private List<File> getExtraDirs(File extrasFolder) { 444 List<File> extraDirs = new ArrayList<File>(); 445 // All OEM provided device profiles are in 446 // $SDK/extras/$VENDOR/$ITEM/devices.xml 447 if (extrasFolder != null && extrasFolder.isDirectory()) { 448 for (File vendor : extrasFolder.listFiles()) { 449 if (vendor.isDirectory()) { 450 for (File item : vendor.listFiles()) { 451 if (item.isDirectory() && isDevicesExtra(item)) { 452 extraDirs.add(item); 453 } 454 } 455 } 456 } 457 } 458 459 return extraDirs; 460 } 461 462 /* 463 * Returns whether a specific folder for a specific vendor is a 464 * DeviceProfiles folder 465 */ 466 private boolean isDevicesExtra(File item) { 467 File properties = new File(item, SdkConstants.FN_SOURCE_PROP); 468 try { 469 BufferedReader propertiesReader = new BufferedReader(new FileReader(properties)); 470 try { 471 String line; 472 while ((line = propertiesReader.readLine()) != null) { 473 Matcher m = sPathPropertyPattern.matcher(line); 474 if (m.matches()) { 475 return true; 476 } 477 } 478 } finally { 479 propertiesReader.close(); 480 } 481 } catch (IOException ignore) { 482 } 483 return false; 484 } 485} 486