blob: f2b1734005af7adb072ec9a9e1c1e873875a4a6f [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.sdklib.devices;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.prefs.AndroidLocation;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.resources.Keyboard;
import com.android.resources.KeyboardState;
import com.android.resources.Navigation;
import com.android.sdklib.internal.avd.AvdManager;
import com.android.sdklib.internal.avd.HardwareProperties;
import com.android.sdklib.io.FileOp;
import com.android.sdklib.repository.PkgProps;
import com.android.utils.ILogger;
import com.google.common.base.Charsets;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.io.Closeables;
import org.xml.sax.SAXException;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactoryConfigurationError;
/**
* Manager class for interacting with {@link Device}s within the SDK
*/
public class DeviceManager {
private static final String DEVICE_PROFILES_PROP = "DeviceProfiles";
private static final Pattern PATH_PROPERTY_PATTERN =
Pattern.compile('^' + PkgProps.EXTRA_PATH + '=' + DEVICE_PROFILES_PROP + '$');
private ILogger mLog;
private Table<String, String, Device> mVendorDevices;
private Table<String, String, Device> mSysImgDevices;
private Table<String, String, Device> mUserDevices;
private Table<String, String, Device> mDefaultDevices;
private final Object mLock = new Object();
private final List<DevicesChangedListener> sListeners = new ArrayList<DevicesChangedListener>();
private final String mOsSdkPath;
public enum DeviceFilter {
/** getDevices() flag to list default devices from the bundled devices.xml definitions. */
DEFAULT,
/** getDevices() flag to list user devices saved in the .android home folder. */
USER,
/** getDevices() flag to list vendor devices -- the bundled nexus.xml devices
* as well as all those coming from extra packages. */
VENDOR,
/** getDevices() flag to list devices from system-images/platform-N/tag/abi/devices.xml */
SYSTEM_IMAGES,
}
/** getDevices() flag to list all devices. */
public static final EnumSet<DeviceFilter> ALL_DEVICES = EnumSet.allOf(DeviceFilter.class);
public enum DeviceStatus {
/**
* The device exists unchanged from the given configuration
*/
EXISTS,
/**
* A device exists with the given name and manufacturer, but has a different configuration
*/
CHANGED,
/**
* There is no device with the given name and manufacturer
*/
MISSING
}
/**
* Creates a new instance of DeviceManager.
*
* @param sdkLocation Path to the current SDK. If null or invalid, vendor and system images
* devices are ignored.
* @param log SDK logger instance. Should be non-null.
*/
public static DeviceManager createInstance(@Nullable File sdkLocation, @NonNull ILogger log) {
// TODO consider using a cache and reusing the same instance of the device manager
// for the same manager/log combo.
return new DeviceManager(sdkLocation == null ? null : sdkLocation.getPath(), log);
}
/**
* Creates a new instance of DeviceManager.
*
* @param osSdkPath Path to the current SDK. If null or invalid, vendor devices are ignored.
* @param log SDK logger instance. Should be non-null.
*/
private DeviceManager(@Nullable String osSdkPath, @NonNull ILogger log) {
mOsSdkPath = osSdkPath;
mLog = log;
}
/**
* Interface implemented by objects which want to know when changes occur to the {@link Device}
* lists.
*/
public interface DevicesChangedListener {
/**
* Called after one of the {@link Device} lists has been updated.
*/
void onDevicesChanged();
}
/**
* Register a listener to be notified when the device lists are modified.
*
* @param listener The listener to add. Ignored if already registered.
*/
public void registerListener(@NonNull DevicesChangedListener listener) {
synchronized (sListeners) {
if (!sListeners.contains(listener)) {
sListeners.add(listener);
}
}
}
/**
* Removes a listener from the notification list such that it will no longer receive
* notifications when modifications to the {@link Device} list occur.
*
* @param listener The listener to remove.
*/
public boolean unregisterListener(@NonNull DevicesChangedListener listener) {
synchronized (sListeners) {
return sListeners.remove(listener);
}
}
@NonNull
public DeviceStatus getDeviceStatus(@NonNull String name, @NonNull String manufacturer) {
Device d = getDevice(name, manufacturer);
if (d == null) {
return DeviceStatus.MISSING;
}
return DeviceStatus.EXISTS;
}
@Nullable
public Device getDevice(@NonNull String id, @NonNull String manufacturer) {
initDevicesLists();
Device d = mUserDevices.get(id, manufacturer);
if (d != null) {
return d;
}
d = mSysImgDevices.get(id, manufacturer);
if (d != null) {
return d;
}
d = mDefaultDevices.get(id, manufacturer);
if (d != null) {
return d;
}
d = mVendorDevices.get(id, manufacturer);
return d;
}
@Nullable
private Device getDeviceImpl(@NonNull Iterable<Device> devicesList,
@NonNull String id,
@NonNull String manufacturer) {
for (Device d : devicesList) {
if (d.getId().equals(id) && d.getManufacturer().equals(manufacturer)) {
return d;
}
}
return null;
}
/**
* Returns the known {@link Device} list.
*
* @param deviceFilter One of the {@link DeviceFilter} constants.
* @return A copy of the list of {@link Device}s. Can be empty but not null.
*/
@NonNull
public Collection<Device> getDevices(@NonNull DeviceFilter deviceFilter) {
return getDevices(EnumSet.of(deviceFilter));
}
/**
* Returns the known {@link Device} list.
*
* @param deviceFilter A combination of the {@link DeviceFilter} constants
* or the constant {@link DeviceManager#ALL_DEVICES}.
* @return A copy of the list of {@link Device}s. Can be empty but not null.
*/
@NonNull
public Collection<Device> getDevices(@NonNull EnumSet<DeviceFilter> deviceFilter) {
initDevicesLists();
Table<String, String, Device> devices = HashBasedTable.create();
if (mUserDevices != null && (deviceFilter.contains(DeviceFilter.USER))) {
devices.putAll(mUserDevices);
}
if (mDefaultDevices != null && (deviceFilter.contains(DeviceFilter.DEFAULT))) {
devices.putAll(mDefaultDevices);
}
if (mVendorDevices != null && (deviceFilter.contains(DeviceFilter.VENDOR))) {
devices.putAll(mVendorDevices);
}
if (mSysImgDevices != null && (deviceFilter.contains(DeviceFilter.SYSTEM_IMAGES))) {
devices.putAll(mSysImgDevices);
}
return Collections.unmodifiableCollection(devices.values());
}
private void initDevicesLists() {
boolean changed = initDefaultDevices();
changed |= initVendorDevices();
changed |= initSysImgDevices();
changed |= initUserDevices();
if (changed) {
notifyListeners();
}
}
/**
* Initializes the {@link Device}s packaged with the SDK.
* @return True if the list has changed.
*/
private boolean initDefaultDevices() {
synchronized (mLock) {
if (mDefaultDevices != null) {
return false;
}
InputStream stream = DeviceManager.class
.getResourceAsStream(SdkConstants.FN_DEVICES_XML);
try {
assert stream != null : SdkConstants.FN_DEVICES_XML + " not bundled in sdklib.";
mDefaultDevices = DeviceParser.parse(stream);
return true;
} catch (IllegalStateException e) {
// The device builders can throw IllegalStateExceptions if
// build gets called before everything is properly setup
mLog.error(e, null);
mDefaultDevices = HashBasedTable.create();
} catch (Exception e) {
mLog.error(e, "Error reading default devices");
mDefaultDevices = HashBasedTable.create();
} finally {
Closeables.closeQuietly(stream);
}
}
return false;
}
/**
* Initializes all vendor-provided {@link Device}s: the bundled nexus.xml devices
* as well as all those coming from extra packages.
* @return True if the list has changed.
*/
private boolean initVendorDevices() {
synchronized (mLock) {
if (mVendorDevices != null) {
return false;
}
mVendorDevices = HashBasedTable.create();
// Load builtin devices
InputStream stream = DeviceManager.class.getResourceAsStream("nexus.xml");
try {
mVendorDevices.putAll(DeviceParser.parse(stream));
} catch (Exception e) {
mLog.error(e, "Could not load nexus devices");
} finally {
Closeables.closeQuietly(stream);
}
stream = DeviceManager.class.getResourceAsStream("wear.xml");
try {
mVendorDevices.putAll(DeviceParser.parse(stream));
} catch (Exception e) {
mLog.error(e, "Could not load wear devices");
} finally {
Closeables.closeQuietly(stream);
}
stream = DeviceManager.class.getResourceAsStream("tv.xml");
try {
mVendorDevices.putAll(DeviceParser.parse(stream));
} catch (Exception e) {
mLog.error(e, "Could not load tv devices");
} finally {
Closeables.closeQuietly(stream);
}
if (mOsSdkPath != null) {
// Load devices from vendor extras
File extrasFolder = new File(mOsSdkPath, SdkConstants.FD_EXTRAS);
List<File> deviceDirs = getExtraDirs(extrasFolder);
for (File deviceDir : deviceDirs) {
File deviceXml = new File(deviceDir, SdkConstants.FN_DEVICES_XML);
if (deviceXml.isFile()) {
mVendorDevices.putAll(loadDevices(deviceXml));
}
}
return true;
}
}
return false;
}
/**
* Initializes all system-image provided {@link Device}s.
* @return True if the list has changed.
*/
private boolean initSysImgDevices() {
synchronized (mLock) {
if (mSysImgDevices != null) {
return false;
}
mSysImgDevices = HashBasedTable.create();
if (mOsSdkPath == null) {
return false;
}
// Load devices from tagged system-images
// Path pattern is /sdk/system-images/<platform-N>/<tag>/<abi>/devices.xml
FileOp fop = new FileOp();
File sysImgFolder = new File(mOsSdkPath, SdkConstants.FD_SYSTEM_IMAGES);
for (File platformFolder : fop.listFiles(sysImgFolder)) {
if (!fop.isDirectory(platformFolder)) {
continue;
}
for (File tagFolder : fop.listFiles(platformFolder)) {
if (!fop.isDirectory(tagFolder)) {
continue;
}
for (File abiFolder : fop.listFiles(tagFolder)) {
if (!fop.isDirectory(abiFolder)) {
continue;
}
File deviceXml = new File(abiFolder, SdkConstants.FN_DEVICES_XML);
if (fop.isFile(deviceXml)) {
mSysImgDevices.putAll(loadDevices(deviceXml));
}
}
}
}
return true;
}
}
/**
* Initializes all user-created {@link Device}s
* @return True if the list has changed.
*/
private boolean initUserDevices() {
synchronized (mLock) {
if (mUserDevices != null) {
return false;
}
// User devices should be saved out to
// $HOME/.android/devices.xml
mUserDevices = HashBasedTable.create();
File userDevicesFile = null;
try {
userDevicesFile = new File(
AndroidLocation.getFolder(),
SdkConstants.FN_DEVICES_XML);
if (userDevicesFile.exists()) {
mUserDevices.putAll(DeviceParser.parse(userDevicesFile));
return true;
}
} catch (AndroidLocationException e) {
mLog.warning("Couldn't load user devices: %1$s", e.getMessage());
} catch (SAXException e) {
// Probably an old config file which we don't want to overwrite.
if (userDevicesFile != null) {
String base = userDevicesFile.getAbsoluteFile() + ".old";
File renamedConfig = new File(base);
int i = 0;
while (renamedConfig.exists()) {
renamedConfig = new File(base + '.' + (i++));
}
mLog.error(e, "Error parsing %1$s, backing up to %2$s",
userDevicesFile.getAbsolutePath(),
renamedConfig.getAbsolutePath());
userDevicesFile.renameTo(renamedConfig);
}
} catch (ParserConfigurationException e) {
mLog.error(e, "Error parsing %1$s",
userDevicesFile == null ? "(null)" : userDevicesFile.getAbsolutePath());
} catch (IOException e) {
mLog.error(e, "Error parsing %1$s",
userDevicesFile == null ? "(null)" : userDevicesFile.getAbsolutePath());
}
}
return false;
}
public void addUserDevice(@NonNull Device d) {
boolean changed = false;
synchronized (mLock) {
if (mUserDevices == null) {
initUserDevices();
assert mUserDevices != null;
}
if (mUserDevices != null) {
mUserDevices.put(d.getId(), d.getManufacturer(), d);
}
changed = true;
}
if (changed) {
notifyListeners();
}
}
public void removeUserDevice(@NonNull Device d) {
synchronized (mLock) {
if (mUserDevices == null) {
initUserDevices();
assert mUserDevices != null;
}
if (mUserDevices != null) {
if (mUserDevices.contains(d.getId(), d.getManufacturer())) {
mUserDevices.remove(d.getId(), d.getManufacturer());
notifyListeners();
}
}
}
}
public void replaceUserDevice(@NonNull Device d) {
synchronized (mLock) {
if (mUserDevices == null) {
initUserDevices();
}
removeUserDevice(d);
addUserDevice(d);
}
}
/**
* Saves out the user devices to {@link SdkConstants#FN_DEVICES_XML} in
* {@link AndroidLocation#getFolder()}.
*/
public void saveUserDevices() {
if (mUserDevices == null) {
return;
}
File userDevicesFile = null;
try {
userDevicesFile = new File(AndroidLocation.getFolder(),
SdkConstants.FN_DEVICES_XML);
} catch (AndroidLocationException e) {
mLog.warning("Couldn't find user directory: %1$s", e.getMessage());
return;
}
if (mUserDevices.isEmpty()) {
userDevicesFile.delete();
return;
}
synchronized (mLock) {
if (!mUserDevices.isEmpty()) {
try {
DeviceWriter.writeToXml(new FileOutputStream(userDevicesFile), mUserDevices.values());
} catch (FileNotFoundException e) {
mLog.warning("Couldn't open file: %1$s", e.getMessage());
} catch (ParserConfigurationException e) {
mLog.warning("Error writing file: %1$s", e.getMessage());
} catch (TransformerFactoryConfigurationError e) {
mLog.warning("Error writing file: %1$s", e.getMessage());
} catch (TransformerException e) {
mLog.warning("Error writing file: %1$s", e.getMessage());
}
}
}
}
/**
* Returns hardware properties (defined in hardware.ini) as a {@link Map}.
*
* @param s The {@link State} from which to derive the hardware properties.
* @return A {@link Map} of hardware properties.
*/
@NonNull
public static Map<String, String> getHardwareProperties(@NonNull State s) {
Hardware hw = s.getHardware();
Map<String, String> props = new HashMap<String, String>();
props.put(HardwareProperties.HW_MAINKEYS,
getBooleanVal(hw.getButtonType().equals(ButtonType.HARD)));
props.put(HardwareProperties.HW_TRACKBALL,
getBooleanVal(hw.getNav().equals(Navigation.TRACKBALL)));
props.put(HardwareProperties.HW_KEYBOARD,
getBooleanVal(hw.getKeyboard().equals(Keyboard.QWERTY)));
props.put(HardwareProperties.HW_DPAD,
getBooleanVal(hw.getNav().equals(Navigation.DPAD)));
Set<Sensor> sensors = hw.getSensors();
props.put(HardwareProperties.HW_GPS, getBooleanVal(sensors.contains(Sensor.GPS)));
props.put(HardwareProperties.HW_BATTERY,
getBooleanVal(hw.getChargeType().equals(PowerType.BATTERY)));
props.put(HardwareProperties.HW_ACCELEROMETER,
getBooleanVal(sensors.contains(Sensor.ACCELEROMETER)));
props.put(HardwareProperties.HW_ORIENTATION_SENSOR,
getBooleanVal(sensors.contains(Sensor.GYROSCOPE)));
props.put(HardwareProperties.HW_AUDIO_INPUT, getBooleanVal(hw.hasMic()));
props.put(HardwareProperties.HW_SDCARD, getBooleanVal(!hw.getRemovableStorage().isEmpty()));
props.put(HardwareProperties.HW_LCD_DENSITY,
Integer.toString(hw.getScreen().getPixelDensity().getDpiValue()));
props.put(HardwareProperties.HW_PROXIMITY_SENSOR,
getBooleanVal(sensors.contains(Sensor.PROXIMITY_SENSOR)));
return props;
}
/**
* Returns the hardware properties defined in
* {@link AvdManager#HARDWARE_INI} as a {@link Map}.
*
* This is intended to be dumped in the config.ini and already contains
* the device name, manufacturer and device hash.
*
* @param d The {@link Device} from which to derive the hardware properties.
* @return A {@link Map} of hardware properties.
*/
@NonNull
public static Map<String, String> getHardwareProperties(@NonNull Device d) {
Map<String, String> props = getHardwareProperties(d.getDefaultState());
for (State s : d.getAllStates()) {
if (s.getKeyState().equals(KeyboardState.HIDDEN)) {
props.put("hw.keyboard.lid", getBooleanVal(true));
}
}
HashFunction md5 = Hashing.md5();
Hasher hasher = md5.newHasher();
ArrayList<String> keys = new ArrayList<String>(props.keySet());
Collections.sort(keys);
for (String key : keys) {
if (key != null) {
hasher.putString(key, Charsets.UTF_8);
String value = props.get(key);
hasher.putString(value == null ? "null" : value, Charsets.UTF_8);
}
}
// store the hash method for potential future compatibility
String hash = "MD5:" + hasher.hash().toString();
props.put(AvdManager.AVD_INI_DEVICE_HASH_V2, hash);
props.remove(AvdManager.AVD_INI_DEVICE_HASH_V1);
props.put(AvdManager.AVD_INI_DEVICE_NAME, d.getId());
props.put(AvdManager.AVD_INI_DEVICE_MANUFACTURER, d.getManufacturer());
return props;
}
/**
* Checks whether the the hardware props have changed.
* If the hash is the same, returns null for success.
* If the hash is not the same or there's not enough information to indicate it's
* the same (e.g. if in the future we change the digest method), simply return the
* new hash, indicating it would be best to update it.
*
* @param d The device.
* @param hashV2 The previous saved AvdManager.AVD_INI_DEVICE_HASH_V2 property.
* @return Null if the same, otherwise returns the new and different hash.
*/
@Nullable
public static String hasHardwarePropHashChanged(@NonNull Device d, @NonNull String hashV2) {
Map<String, String> props = getHardwareProperties(d);
String newHash = props.get(AvdManager.AVD_INI_DEVICE_HASH_V2);
// Implementation detail: don't just return the hash and let the caller decide whether
// the hash is the same. That's because the hash contains the digest method so if in
// the future we decide to change it, we could potentially recompute the hash here
// using an older digest method here and still determine its validity, whereas the
// caller cannot determine that.
if (newHash != null && newHash.equals(hashV2)) {
return null;
}
return newHash;
}
/**
* Takes a boolean and returns the appropriate value for
* {@link HardwareProperties}
*
* @param bool The boolean value to turn into the appropriate
* {@link HardwareProperties} value.
* @return {@code HardwareProperties#BOOLEAN_YES} if true,
* {@code HardwareProperties#BOOLEAN_NO} otherwise.
*/
private static String getBooleanVal(boolean bool) {
if (bool) {
return HardwareProperties.BOOLEAN_YES;
}
return HardwareProperties.BOOLEAN_NO;
}
@NonNull
private Table<String, String, Device> loadDevices(@NonNull File deviceXml) {
try {
return DeviceParser.parse(deviceXml);
} catch (SAXException e) {
mLog.error(e, "Error parsing %1$s", deviceXml.getAbsolutePath());
} catch (ParserConfigurationException e) {
mLog.error(e, "Error parsing %1$s", deviceXml.getAbsolutePath());
} catch (IOException e) {
mLog.error(e, "Error reading %1$s", deviceXml.getAbsolutePath());
} catch (AssertionError e) {
mLog.error(e, "Error parsing %1$s", deviceXml.getAbsolutePath());
} catch (IllegalStateException e) {
// The device builders can throw IllegalStateExceptions if
// build gets called before everything is properly setup
mLog.error(e, null);
}
return HashBasedTable.create();
}
private void notifyListeners() {
synchronized (sListeners) {
for (DevicesChangedListener listener : sListeners) {
listener.onDevicesChanged();
}
}
}
/* Returns all of DeviceProfiles in the extras/ folder */
@NonNull
private List<File> getExtraDirs(@NonNull File extrasFolder) {
List<File> extraDirs = new ArrayList<File>();
// All OEM provided device profiles are in
// $SDK/extras/$VENDOR/$ITEM/devices.xml
if (extrasFolder != null && extrasFolder.isDirectory()) {
for (File vendor : extrasFolder.listFiles()) {
if (vendor.isDirectory()) {
for (File item : vendor.listFiles()) {
if (item.isDirectory() && isDevicesExtra(item)) {
extraDirs.add(item);
}
}
}
}
}
return extraDirs;
}
/*
* Returns whether a specific folder for a specific vendor is a
* DeviceProfiles folder
*/
private boolean isDevicesExtra(@NonNull File item) {
File properties = new File(item, SdkConstants.FN_SOURCE_PROP);
try {
BufferedReader propertiesReader = new BufferedReader(new FileReader(properties));
try {
String line;
while ((line = propertiesReader.readLine()) != null) {
Matcher m = PATH_PROPERTY_PATTERN.matcher(line);
if (m.matches()) {
return true;
}
}
} finally {
propertiesReader.close();
}
} catch (IOException ignore) {
}
return false;
}
}