| /* |
| * Copyright (C) 2010 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.tradefed.device; |
| |
| import com.android.ddmlib.AdbCommandRejectedException; |
| import com.android.ddmlib.IDevice; |
| import com.android.ddmlib.Log; |
| import com.android.ddmlib.ShellCommandUnresponsiveException; |
| import com.android.ddmlib.TimeoutException; |
| import com.android.tradefed.config.Option; |
| import com.android.tradefed.log.LogUtil.CLog; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Map; |
| |
| /** |
| * Container for for device selection criteria. |
| */ |
| public class DeviceSelectionOptions implements IDeviceSelection { |
| |
| private static final String LOG_TAG = "DeviceSelectionOptions"; |
| |
| @Option(name = "serial", shortName = 's', description = |
| "run this test on a specific device with given serial number(s).") |
| private Collection<String> mSerials = new ArrayList<String>(); |
| |
| @Option(name = "exclude-serial", description = |
| "run this test on any device except those with this serial number(s).") |
| private Collection<String> mExcludeSerials = new ArrayList<String>(); |
| |
| @Option(name = "product-type", description = |
| "run this test on device with this product type(s). May also filter by variant " + |
| "using product:variant.") |
| private Collection<String> mProductTypes = new ArrayList<String>(); |
| |
| @Option(name = "property", description = |
| "run this test on device with this property value. " + |
| "Expected format <propertyname>=<propertyvalue>.") |
| private Collection<String> mPropertyStrings = new ArrayList<String>(); |
| |
| @Option(name = "emulator", shortName = 'e', description = |
| "force this test to run on emulator.") |
| private boolean mEmulatorRequested = false; |
| |
| @Option(name = "device", shortName = 'd', description = |
| "force this test to run on a physical device, not an emulator.") |
| private boolean mDeviceRequested = false; |
| |
| @Option(name = "new-emulator", description = |
| "allocate a placeholder emulator. Should be used when config intends to launch an emulator") |
| private boolean mStubEmulatorRequested = false; |
| |
| @Option(name = "null-device", shortName = 'n', description = |
| "do not allocate a device for this test.") |
| private boolean mNullDeviceRequested = false; |
| |
| @Option(name = "min-battery", description = |
| "only run this test on a device whose battery level is at least the given amount. " + |
| "Scale: 0-100") |
| private Integer mMinBattery = null; |
| |
| @Option(name = "max-battery", description = |
| "only run this test on a device whose battery level is strictly less than the given " + |
| "amount. Scale: 0-100") |
| private Integer mMaxBattery = null; |
| |
| @Option(name = "require-battery-check", description = "_If_ --min-battery and/or " + |
| "--max-battery is specified, skip devices that have an unknown battery level. Note " + |
| "that this may leave restart-looping devices in limbo indefinitely without manual " + |
| "intervention.") |
| private boolean mRequireBatteryCheck = false; |
| |
| // If we have tried to fetch the environment variable ANDROID_SERIAL before. |
| private boolean mFetchedEnvVariable = false; |
| |
| private static final String VARIANT_SEPARATOR = ":"; |
| |
| public static final String DEVICE_PRODUCT_PROPERTY = "ro.hardware"; |
| public static final String DEVICE_VARIANT_PROPERTY = "ro.product.device"; |
| |
| /** |
| * Add a serial number to the device selection options. |
| * |
| * @param serialNumber |
| */ |
| public void addSerial(String serialNumber) { |
| mSerials.add(serialNumber); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void setSerial(String... serialNumber) { |
| mSerials.clear(); |
| mSerials.addAll(Arrays.asList(serialNumber)); |
| } |
| |
| /** |
| * Add a serial number to exclusion list. |
| * |
| * @param serialNumber |
| */ |
| public void addExcludeSerial(String serialNumber) { |
| mExcludeSerials.add(serialNumber); |
| } |
| |
| /** |
| * Add a product type to the device selection options. |
| * |
| * @param productType |
| */ |
| public void addProductType(String productType) { |
| mProductTypes.add(productType); |
| } |
| |
| /** |
| * Add a property criteria to the device selection options |
| * |
| * @param propertyKeyValue a property to match. Expected format propertykey=propertyvalue |
| */ |
| public void addProperty(String propertyKeyValue) { |
| mPropertyStrings.add(propertyKeyValue); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public Collection<String> getSerials() { |
| // If no serial was explicitly set, use the environment variable ANDROID_SERIAL. |
| if (mSerials.isEmpty() && !mFetchedEnvVariable) { |
| String env_serial = fetchEnvironmentVariable("ANDROID_SERIAL"); |
| if (env_serial != null) { |
| mSerials.add(env_serial); |
| } |
| mFetchedEnvVariable = true; |
| } |
| return copyCollection(mSerials); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public Collection<String> getExcludeSerials() { |
| return copyCollection(mExcludeSerials); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public Collection<String> getProductTypes() { |
| return copyCollection(mProductTypes); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean deviceRequested() { |
| return mDeviceRequested; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean emulatorRequested() { |
| return mEmulatorRequested; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean stubEmulatorRequested() { |
| return mStubEmulatorRequested; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean nullDeviceRequested() { |
| return mNullDeviceRequested; |
| } |
| |
| /** |
| * Sets the emulator requested flag |
| */ |
| public void setEmulatorRequested(boolean emulatorRequested) { |
| mEmulatorRequested = emulatorRequested; |
| } |
| |
| /** |
| * Sets the stub emulator requested flag |
| */ |
| public void setStubEmulatorRequested(boolean stubEmulatorRequested) { |
| mStubEmulatorRequested = stubEmulatorRequested; |
| } |
| |
| /** |
| * Sets the emulator requested flag |
| */ |
| public void setDeviceRequested(boolean deviceRequested) { |
| mDeviceRequested = deviceRequested; |
| } |
| |
| /** |
| * Sets the null device requested flag |
| */ |
| public void setNullDeviceRequested(boolean nullDeviceRequested) { |
| mNullDeviceRequested = nullDeviceRequested; |
| } |
| |
| /** |
| * Sets the minimum battery level |
| */ |
| public void setMinBatteryLevel(Integer minBattery) { |
| mMinBattery = minBattery; |
| } |
| |
| /** |
| * Gets the requested minimum battery level |
| */ |
| public Integer getMinBatteryLevel() { |
| return mMinBattery; |
| } |
| |
| /** |
| * Sets the maximum battery level |
| */ |
| public void setMaxBatteryLevel(Integer maxBattery) { |
| mMaxBattery = maxBattery; |
| } |
| |
| /** |
| * Gets the requested maximum battery level |
| */ |
| public Integer getMaxBatteryLevel() { |
| return mMaxBattery; |
| } |
| |
| /** |
| * Sets whether battery check is required for devices with unknown battery level |
| */ |
| public void setRequireBatteryCheck(boolean requireCheck) { |
| mRequireBatteryCheck = requireCheck; |
| } |
| |
| /** |
| * Gets whether battery check is required for devices with unknown battery level |
| */ |
| public boolean getRequireBatteryCheck() { |
| return mRequireBatteryCheck; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public Map<String, String> getProperties() { |
| Map<String, String> propertyMap = new HashMap<String, String>(mPropertyStrings.size()); |
| for (String propertyKeyValue : mPropertyStrings) { |
| String[] keyValuePair = propertyKeyValue.split("="); |
| if (keyValuePair.length == 2) { |
| propertyMap.put(keyValuePair[0], keyValuePair[1]); |
| } else { |
| Log.e(LOG_TAG, String.format("Unrecognized property key value pair: '%s'", |
| propertyKeyValue)); |
| } |
| } |
| return propertyMap; |
| } |
| |
| private Collection<String> copyCollection(Collection<String> original) { |
| Collection<String> listCopy = new ArrayList<String>(original.size()); |
| listCopy.addAll(original); |
| return listCopy; |
| } |
| |
| /** |
| * Helper function used to fetch environment variable. It is essentially a wrapper around |
| * {@link System#getenv(String)} This is done for unit testing purposes. |
| * |
| * @param name the environment variable to fetch. |
| * @return a {@link String} value of the environment variable or null if not available. |
| */ |
| String fetchEnvironmentVariable(String name) { |
| return System.getenv(name); |
| } |
| |
| /** |
| * @return <code>true</code> if the given {@link IDevice} is a match for the provided options. |
| * <code>false</code> otherwise |
| */ |
| @Override |
| public boolean matches(IDevice device) { |
| Collection<String> serials = getSerials(); |
| Collection<String> excludeSerials = getExcludeSerials(); |
| Map<String, Collection<String>> productVariants = splitOnVariant(getProductTypes()); |
| Collection<String> productTypes = productVariants.keySet(); |
| Map<String, String> properties = getProperties(); |
| |
| if (!serials.isEmpty() && |
| !serials.contains(device.getSerialNumber())) { |
| return false; |
| } |
| if (excludeSerials.contains(device.getSerialNumber())) { |
| return false; |
| } |
| if (!productTypes.isEmpty()) { |
| String productType = getDeviceProductType(device); |
| if (productTypes.contains(productType)) { |
| // check variant |
| String productVariant = getDeviceProductVariant(device); |
| Collection<String> variants = productVariants.get(productType); |
| if (variants != null && !variants.contains(productVariant)) { |
| return false; |
| } |
| } else { |
| // no product type matches; bye-bye |
| return false; |
| } |
| } |
| for (Map.Entry<String, String> propEntry : properties.entrySet()) { |
| if (!propEntry.getValue().equals(device.getProperty(propEntry.getKey()))) { |
| return false; |
| } |
| } |
| if ((emulatorRequested() || stubEmulatorRequested()) && !device.isEmulator()) { |
| return false; |
| } |
| if (deviceRequested() && device.isEmulator()) { |
| return false; |
| } |
| if (device.isEmulator() && (device instanceof StubDevice) && !stubEmulatorRequested()) { |
| // only allocate the stub emulator if requested |
| return false; |
| } |
| if (nullDeviceRequested() != (device instanceof NullDevice)) { |
| return false; |
| } |
| if ((mMinBattery != null) || (mMaxBattery != null)) { |
| Integer deviceBattery = getBatteryLevel(device); |
| if (mRequireBatteryCheck && (deviceBattery == null)) { |
| // Couldn't determine battery level when that check is required; reject device |
| return false; |
| } |
| if (isLessAndNotNull(deviceBattery, mMinBattery)) { |
| // deviceBattery < mMinBattery |
| return false; |
| } |
| if (isLessEqAndNotNull(mMaxBattery, deviceBattery)) { |
| // mMaxBattery <= deviceBattery |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| /** Determine if x is less-than y, given that both are non-Null */ |
| private static boolean isLessAndNotNull(Integer x, Integer y) { |
| if ((x == null) || (y == null)) { |
| return false; |
| } |
| return x < y; |
| } |
| |
| /** Determine if x is less-than y, given that both are non-Null */ |
| private static boolean isLessEqAndNotNull(Integer x, Integer y) { |
| if ((x == null) || (y == null)) { |
| return false; |
| } |
| return x <= y; |
| } |
| |
| private Map<String, Collection<String>> splitOnVariant(Collection<String> products) { |
| // FIXME: we should validate all provided device selection options once, on the first |
| // FIXME: call to #matches |
| Map<String, Collection<String>> splitProducts = |
| new HashMap<String, Collection<String>>(products.size()); |
| // FIXME: cache this |
| for (String prod : products) { |
| String[] parts = prod.split(VARIANT_SEPARATOR); |
| if (parts.length == 1) { |
| splitProducts.put(parts[0], null); |
| } else if (parts.length == 2) { |
| // A variant was specified as product:variant |
| Collection<String> variants = splitProducts.get(parts[0]); |
| if (variants == null) { |
| variants = new HashSet<String>(); |
| splitProducts.put(parts[0], variants); |
| } |
| variants.add(parts[1]); |
| } else { |
| throw new IllegalArgumentException(String.format("The product type filter \"%s\" " + |
| "is invalid. It must contain 0 or 1 '%s' characters, not %d.", |
| prod, VARIANT_SEPARATOR, parts.length)); |
| } |
| } |
| |
| return splitProducts; |
| } |
| |
| @Override |
| public String getDeviceProductType(IDevice device) { |
| return getProperty(device, DEVICE_PRODUCT_PROPERTY); |
| } |
| |
| private String getProperty(IDevice device, String propName) { |
| try { |
| return device.getPropertyCacheOrSync(propName); |
| } catch (TimeoutException e) { |
| handlePropException(device, e); |
| } catch (AdbCommandRejectedException e) { |
| handlePropException(device, e); |
| } catch (IOException e) { |
| handlePropException(device, e); |
| } catch (ShellCommandUnresponsiveException e) { |
| handlePropException(device, e); |
| } |
| return null; |
| } |
| |
| private void handlePropException(IDevice device, Exception e) { |
| CLog.w("Failed to query device property for %s: %s", device.getSerialNumber(), |
| e.toString()); |
| } |
| |
| @Override |
| public String getDeviceProductVariant(IDevice device) { |
| return getProperty(device, DEVICE_VARIANT_PROPERTY); |
| } |
| |
| @Override |
| public Integer getBatteryLevel(IDevice device) { |
| try { |
| return device.getBatteryLevel(); |
| } catch (TimeoutException e) { |
| handleBatteryException(device, e); |
| } catch (AdbCommandRejectedException e) { |
| handleBatteryException(device, e); |
| } catch (IOException e) { |
| handleBatteryException(device, e); |
| } catch (ShellCommandUnresponsiveException e) { |
| handleBatteryException(device, e); |
| } |
| return null; |
| } |
| |
| private void handleBatteryException(IDevice device, Exception e) { |
| CLog.w("Failed to query battery level for %s: %s", device.getSerialNumber(), e.toString()); |
| } |
| } |