/* * 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.AndroidDebugBridge.IDeviceChangeListener; import com.android.ddmlib.DdmPreferences; import com.android.ddmlib.EmulatorConsole; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.ddmlib.Log.LogLevel; import com.android.tradefed.config.GlobalConfiguration; import com.android.tradefed.config.IGlobalConfiguration; import com.android.tradefed.device.IDeviceMonitor.DeviceLister; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.util.ArrayUtil; import com.android.tradefed.util.CommandResult; import com.android.tradefed.util.CommandStatus; import com.android.tradefed.util.ConditionPriorityBlockingQueue; import com.android.tradefed.util.ConditionPriorityBlockingQueue.IMatcher; import com.android.tradefed.util.IRunUtil; import com.android.tradefed.util.RunUtil; import com.android.tradefed.util.StreamUtil; import com.android.tradefed.util.TableFormatter; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * {@inheritDoc} */ public class DeviceManager implements IDeviceManager { /** max wait time in ms for fastboot devices command to complete */ private static final long FASTBOOT_CMD_TIMEOUT = 1 * 60 * 1000; /** time to wait in ms between fastboot devices requests */ private static final long FASTBOOT_POLL_WAIT_TIME = 5 * 1000; /** * time to wait for device adb shell responsive connection before declaring * it unavailable for testing */ private static final int CHECK_WAIT_DEVICE_AVAIL_MS = 30 * 1000; /** * a {@link DeviceSelectionOptions} that matches any device. Visible for * testing. */ static final IDeviceSelection ANY_DEVICE_OPTIONS = new DeviceSelectionOptions(); private static DeviceManager sInstance; private final IDeviceMonitor mDvcMon; private boolean mIsInitialized = false; /** * A thread-safe map that tracks the devices currently allocated for * testing. */ private Map<String, IManagedTestDevice> mAllocatedDeviceMap; /** * A FIFO, thread-safe queue for holding devices visible on adb available * for testing */ private ConditionPriorityBlockingQueue<IDevice> mAvailableDeviceQueue; private IAndroidDebugBridge mAdbBridge; private ManagedDeviceListener mManagedDeviceListener; private boolean mFastbootEnabled; private Set<IFastbootListener> mFastbootListeners; private FastbootMonitor mFastbootMonitor; private Map<String, IDeviceStateMonitor> mCheckDeviceMap; private boolean mEnableLogcat = true; private boolean mIsTerminated = false; private IDeviceSelection mGlobalDeviceFilter; /** the maximum number of emulators that can be allocated at one time */ private int mNumEmulatorSupported = 1; /** the maximum number of no device runs that can be allocated at one time */ private int mNumNullDevicesSupported = 1; private boolean mSynchronousMode = false; /** * Package-private constructor, should only be used by this class and its * associated unit test. Use {@link #getInstance()} instead. */ DeviceManager() { mDvcMon = getGlobalConfig().getDeviceMonitor(); } @Override public void init() { init(null); } /** * Initialize the device manager. This must be called once and only once * before any other methods are called. */ @Override public synchronized void init(IDeviceSelection globalDeviceFilter) { if (mIsInitialized) { throw new IllegalStateException("already initialized"); } if (globalDeviceFilter == null) { globalDeviceFilter = getGlobalConfig().getDeviceRequirements(); } mIsInitialized = true; mGlobalDeviceFilter = globalDeviceFilter; // Using ConcurrentHashMap for thread safety: handles concurrent // modification and iteration mAllocatedDeviceMap = new ConcurrentHashMap<String, IManagedTestDevice>(); mAvailableDeviceQueue = new ConditionPriorityBlockingQueue<IDevice>(); mCheckDeviceMap = new ConcurrentHashMap<String, IDeviceStateMonitor>(); if (isFastbootAvailable()) { mFastbootListeners = Collections .synchronizedSet(new HashSet<IFastbootListener>()); mFastbootMonitor = new FastbootMonitor(); startFastbootMonitor(); // don't set fastboot enabled bit until mFastbootListeners has been // initialized mFastbootEnabled = true; // TODO: consider only adding fastboot devices if explicit option is // set, because // device property selection options won't work properly with a // device in fastboot addFastbootDevices(); } else { CLog.w("Fastboot is not available."); mFastbootListeners = null; mFastbootMonitor = null; mFastbootEnabled = false; } // don't start adding devices until fastboot support has been // established // TODO: Temporarily increase default timeout as workaround for // syncFiles timeouts DdmPreferences.setTimeOut(30 * 1000); mAdbBridge = createAdbBridge(); mManagedDeviceListener = new ManagedDeviceListener(); // It's important to add the listener before initializing the ADB bridge // to avoid a race // condition when detecting devices. mAdbBridge.addDeviceChangeListener(mManagedDeviceListener); if (mDvcMon != null) { mDvcMon.setDeviceLister(new DeviceLister() { @Override public Map<IDevice, String> listDevices() { return fetchDevicesInfo(); } }); mDvcMon.run(); } // assume "adb" is in PATH // TODO: make this configurable mAdbBridge.init(false /* client support */, "adb"); addEmulators(); addNullDevices(); } /** * Instruct DeviceManager whether to use background threads or not. * <p/> * Exposed to make unit tests more deterministic. * * @param syncMode */ void setSynchronousMode(boolean syncMode) { mSynchronousMode = syncMode; } private void checkInit() { if (!mIsInitialized) { throw new IllegalStateException( "DeviceManager has not been initialized"); } } /** * Determine if fastboot is available for use. */ private boolean isFastbootAvailable() { CommandResult fastbootResult = getRunUtil().runTimedCmdSilently(5000, "fastboot", "help"); if (fastbootResult.getStatus() == CommandStatus.SUCCESS) { return true; } if (fastbootResult.getStderr() != null && fastbootResult.getStderr().indexOf("usage: fastboot") >= 0) { CLog.logAndDisplay(LogLevel.WARN, "You are running an older version of fastboot, please update it."); return true; } return false; } /** * Start fastboot monitoring. * <p/> * Exposed for unit testing. */ void startFastbootMonitor() { mFastbootMonitor.start(); } /** * Get the {@link IGlobalConfiguration} instance to use. * <p /> * Exposed for unit testing. */ IGlobalConfiguration getGlobalConfig() { return GlobalConfiguration.getInstance(); } /** * Get the {@link RunUtil} instance to use. * <p/> * Exposed for unit testing. */ IRunUtil getRunUtil() { return RunUtil.getDefault(); } /** * Toggle whether allocated devices should capture logcat in background */ public void setEnableLogcat(boolean enableLogcat) { mEnableLogcat = enableLogcat; } /** * Asynchronously checks if device is available, and adds to queue * * @param device */ private void checkAndAddAvailableDevice(final IDevice device) { if (mCheckDeviceMap.containsKey(device.getSerialNumber())) { // device already being checked, ignore CLog.d("Already checking new device %s, ignoring", device.getSerialNumber()); return; } if (!mGlobalDeviceFilter.matches(device)) { CLog.v("New device %s doesn't match global filter, ignoring", device.getSerialNumber()); return; } final IDeviceStateMonitor monitor = createStateMonitor(device); mCheckDeviceMap.put(device.getSerialNumber(), monitor); final String threadName = String.format("Check device %s", device.getSerialNumber()); Runnable checkRunnable = new Runnable() { @Override public void run() { CLog.d("checking new device %s responsiveness", device.getSerialNumber()); if (monitor.waitForDeviceShell(CHECK_WAIT_DEVICE_AVAIL_MS)) { // CLog.logAndDisplay(LogLevel.INFO, // "DeviceManager",String.format("Detected new device %s", // device.getSerialNumber())); Log.logAndDisplay( LogLevel.INFO, "DeviceManager", String.format("Detected new device %s", device.getSerialNumber())); addAvailableDevice(device); } else { CLog.e("Device %s is not responsive to adb shell command , " + "skip adding to available pool", device.getSerialNumber()); } mCheckDeviceMap.remove(device.getSerialNumber()); } }; if (mSynchronousMode) { checkRunnable.run(); } else { Thread checkThread = new Thread(checkRunnable, threadName); // Device checking threads shouldn't hold the JVM open checkThread.setDaemon(true); checkThread.start(); } } /** * Add placeholder objects for the max number of 'no device required' * concurrent allocations */ private void addNullDevices() { for (int i = 0; i < mNumNullDevicesSupported; i++) { addAvailableDevice(new NullDevice( String.format("null-device-%d", i))); } } /** * Add placeholder objects for the max number of emulators that can be * allocated */ private void addEmulators() { // TODO currently this means 'additional emulators not already running' int port = 5554; for (int i = 0; i < mNumEmulatorSupported; i++) { addAvailableDevice(new StubDevice( String.format("emulator-%d", port), true)); port += 2; } } private void addFastbootDevices() { Set<String> serials = getDevicesOnFastboot(); if (serials != null) { for (String serial : serials) { addAvailableDevice(new FastbootDevice(serial)); } } } private static class FastbootDevice extends StubDevice { FastbootDevice(String serial) { super(serial, false); } } /** * Creates a {@link IDeviceStateMonitor} to use. * <p/> * Exposed so unit tests can mock */ IDeviceStateMonitor createStateMonitor(IDevice device) { return new DeviceStateMonitor(this, device, mFastbootEnabled); } private void addAvailableDevice(final IDevice device) { IMatcher<IDevice> deviceSerialMatcher = new IMatcher<IDevice>() { @Override public boolean matches(IDevice element) { return element.getSerialNumber().equals( device.getSerialNumber()); } }; // add IDevice to available queue, replacing any existing IDevice with // same serial IDevice existingObject = mAvailableDeviceQueue.addUnique( deviceSerialMatcher, device); if (existingObject != null) { // TODO: reduce severity level for this log. Leaving high for now to // understand // circumstances where this can happen CLog.w("Found existing device for available device %s", device.getSerialNumber()); } updateDeviceMonitor(); } /** * Get the available device queue. * <p/> * Exposed for unit testing * * @return */ ConditionPriorityBlockingQueue<IDevice> getAvailableDeviceQueue() { return mAvailableDeviceQueue; } /** * Return the {@link IDeviceManager} singleton, creating if necessary. */ public synchronized static IDeviceManager getInstance() { if (sInstance == null) { sInstance = new DeviceManager(); } return sInstance; } void updateDeviceMonitor() { if (mDvcMon == null) return; if (!mIsInitialized) { CLog.w("updateDeviceMonitor called before DeviceManager was initialized!"); } if (mAdbBridge == null) return; mDvcMon.notifyDeviceStateChange(); } /** * {@inheritDoc} */ @Override public ITestDevice allocateDevice() { checkInit(); IDevice allocatedDevice = takeAvailableDevice(); if (allocatedDevice == null) { return null; } return createAllocatedDevice(allocatedDevice); } /** * {@inheritDoc} */ @Override public ITestDevice forceAllocateDevice(String serial) { checkInit(); if (mAllocatedDeviceMap.containsKey(serial)) { CLog.w("Device %s is already allocated", serial); return null; } // first try to allocate that device as normal DeviceSelectionOptions options = new DeviceSelectionOptions(); options.addSerial(serial); IDevice allocatedDevice = pollAvailableDevice(1, options); if (allocatedDevice == null) { // not there? allocate a stub device allocatedDevice = new StubDevice(serial, false); } return createAllocatedDevice(allocatedDevice); } /** * Retrieves and removes a IDevice from the available device queue, waiting * indefinitely if necessary until an IDevice becomes available. * * @return the {@link IDevice} or <code>null</code> if interrupted */ private IDevice takeAvailableDevice() { try { return mAvailableDeviceQueue.take(ANY_DEVICE_OPTIONS); } catch (InterruptedException e) { CLog.w("interrupted while taking device"); return null; } } /** * {@inheritDoc} */ @Override public ITestDevice allocateDevice(long timeout) { checkInit(); IDevice allocatedDevice = pollAvailableDevice(timeout, ANY_DEVICE_OPTIONS); if (allocatedDevice == null) { return null; } return createAllocatedDevice(allocatedDevice); } /** * {@inheritDoc} */ @Override public ITestDevice allocateDevice(long timeout, IDeviceSelection options) { checkInit(); IDevice allocatedDevice = pollAvailableDevice(timeout, options); if (allocatedDevice == null) { return null; } return createAllocatedDevice(allocatedDevice); } /** * Retrieves and removes a IDevice from the available device queue, waiting * for timeout if necessary until an IDevice becomes available. * * @param timeout * the number of ms to wait for device * @param options * the {@link DeviceSelectionOptions} the returned device must * meet * * @return the {@link IDevice} or <code>null</code> if interrupted */ private IDevice pollAvailableDevice(long timeout, IDeviceSelection options) { try { return mAvailableDeviceQueue.poll(timeout, TimeUnit.MILLISECONDS, options); } catch (InterruptedException e) { CLog.w("interrupted while polling for device"); return null; } } private ITestDevice createAllocatedDevice(IDevice allocatedDevice) { IManagedTestDevice testDevice = createTestDevice(allocatedDevice, createStateMonitor(allocatedDevice)); if (mEnableLogcat && !(allocatedDevice instanceof StubDevice)) { testDevice.startLogcat(); } mAllocatedDeviceMap.put(allocatedDevice.getSerialNumber(), testDevice); CLog.i("Allocated device %s", testDevice.getSerialNumber()); updateDeviceMonitor(); return testDevice; } /** * Factory method to create a {@link IManagedTestDevice}. * <p/> * Exposed so unit tests can mock * * @param allocatedDevice * @param monitor * @return a {@link IManagedTestDevice} */ IManagedTestDevice createTestDevice(IDevice allocatedDevice, IDeviceStateMonitor monitor) { IManagedTestDevice testDevice = new TestDevice(allocatedDevice, monitor); testDevice.setFastbootEnabled(mFastbootEnabled); if (allocatedDevice instanceof FastbootDevice) { testDevice.setDeviceState(TestDeviceState.FASTBOOT); } else if (allocatedDevice instanceof StubDevice) { testDevice.setDeviceState(TestDeviceState.NOT_AVAILABLE); } return testDevice; } /** * Creates the {@link IAndroidDebugBridge} to use. * <p/> * Exposed so tests can mock this. * * @returns the {@link IAndroidDebugBridge} */ synchronized IAndroidDebugBridge createAdbBridge() { return new AndroidDebugBridgeWrapper(); } /** * {@inheritDoc} */ @Override public void freeDevice(ITestDevice device, FreeDeviceState deviceState) { checkInit(); IManagedTestDevice managedDevice = (IManagedTestDevice) device; managedDevice.stopLogcat(); IDevice ideviceToReturn = device.getIDevice(); // don't kill emulator if it wasn't launched by launchEmulator (ie // emulatorProcess is null). if (ideviceToReturn.isEmulator() && managedDevice.getEmulatorProcess() != null) { try { killEmulator(device); // emulator killed - return a stub device // TODO: this is a bit of a hack. Consider having DeviceManager // inject a StubDevice // when deviceDisconnected event is received ideviceToReturn = new StubDevice( ideviceToReturn.getSerialNumber(), true); deviceState = FreeDeviceState.AVAILABLE; } catch (DeviceNotAvailableException e) { CLog.e(e); deviceState = FreeDeviceState.UNAVAILABLE; } } if (mAllocatedDeviceMap.remove(device.getSerialNumber()) == null) { CLog.e("freeDevice called with unallocated device %s", device.getSerialNumber()); } else if (deviceState == FreeDeviceState.UNRESPONSIVE) { // TODO: add class flag to control if unresponsive device's are // returned to pool // TODO: also consider tracking unresponsive events received per // device - so a // device that is continually unresponsive could be removed from // available queue addAvailableDevice(ideviceToReturn); } else if (deviceState == FreeDeviceState.AVAILABLE) { addAvailableDevice(ideviceToReturn); } else if (deviceState == FreeDeviceState.UNAVAILABLE) { CLog.logAndDisplay(LogLevel.WARN, "Freed device %s is unavailable. Removing from use.", device.getSerialNumber()); } updateDeviceMonitor(); } /** * {@inheritDoc} */ @Override public void launchEmulator(ITestDevice device, long bootTimeout, IRunUtil runUtil, List<String> emulatorArgs) throws DeviceNotAvailableException { if (!device.getIDevice().isEmulator()) { throw new IllegalStateException(String.format( "Device %s is not an emulator", device.getSerialNumber())); } if (!device.getDeviceState().equals(TestDeviceState.NOT_AVAILABLE)) { throw new IllegalStateException(String.format( "Emulator device %s is in state %s. Expected: %s", device.getSerialNumber(), device.getDeviceState(), TestDeviceState.NOT_AVAILABLE)); } Integer port = EmulatorConsole .getEmulatorPort(device.getSerialNumber()); if (port == null) { // serial number is not in expected format throw new IllegalArgumentException(String.format( "Failed to determine emulator port for %s", device.getSerialNumber())); } List<String> fullArgs = new ArrayList<String>(emulatorArgs); fullArgs.add("-port"); fullArgs.add(port.toString()); try { Process p = runUtil.runCmdInBackground(fullArgs); // sleep a small amount to wait for process to start successfully getRunUtil().sleep(500); checkProcessDied(p); IManagedTestDevice managedDevice = (IManagedTestDevice) device; managedDevice.setEmulatorProcess(p); managedDevice.startLogcat(); } catch (IOException e) { // TODO: is this the most appropriate exception to throw? throw new DeviceNotAvailableException( "Failed to start emulator process", e); } device.waitForDeviceAvailable(bootTimeout); } /** * Check if emulator process has died * * @param p * the {@link Process} to check * @throws DeviceNotAvailableException * if process has died */ private void checkProcessDied(Process p) throws DeviceNotAvailableException { try { int exitValue = p.exitValue(); // should have thrown IllegalThreadStateException CLog.e("Emulator process has died with exit value %d. stdout: '%s', stderr: '%s'", exitValue, StreamUtil.getStringFromStream(p.getInputStream()), StreamUtil.getStringFromStream(p.getErrorStream())); } catch (IllegalThreadStateException e) { // expected if process is still alive return; } catch (IOException e) { // fall through } throw new DeviceNotAvailableException( "Emulator process has died unexpectedly"); } /** * {@inheritDoc} */ @Override public void killEmulator(ITestDevice device) throws DeviceNotAvailableException { EmulatorConsole console = EmulatorConsole.getConsole(device .getIDevice()); if (console != null) { console.kill(); // lets ensure process is killed too - fall through } else { CLog.w("Could not get emulator console for %s", device.getSerialNumber()); } // lets try killing the process Process emulatorProcess = ((IManagedTestDevice) device) .getEmulatorProcess(); if (emulatorProcess != null) { emulatorProcess.destroy(); } if (!device.waitForDeviceNotAvailable(20 * 1000)) { throw new DeviceNotAvailableException(String.format( "Failed to kill emulator %s", device.getSerialNumber())); } } /** * {@inheritDoc} */ @Override public ITestDevice connectToTcpDevice(String ipAndPort) { if (mAllocatedDeviceMap.containsKey(ipAndPort)) { CLog.w("Device with tcp serial %s is already allocated", ipAndPort); return null; } // create a mapping between this device, and its soon-to-be associated // tcp serial number // this is done so a) the device can get state updates and b) this // device isn't allocated // to another caller when it goes online with new serial ITestDevice tcpDevice = createAllocatedDevice(new StubDevice(ipAndPort)); if (doAdbConnect(ipAndPort)) { try { tcpDevice.setRecovery(new WaitDeviceRecovery()); tcpDevice.waitForDeviceOnline(); return tcpDevice; } catch (DeviceNotAvailableException e) { CLog.w("Device with tcp serial %s did not come online", ipAndPort); } } freeDevice(tcpDevice, FreeDeviceState.IGNORE); return null; } /** * {@inheritDoc} */ @Override public ITestDevice reconnectDeviceToTcp(ITestDevice usbDevice) throws DeviceNotAvailableException { CLog.i("Reconnecting device %s to adb over tcpip", usbDevice.getSerialNumber()); ITestDevice tcpDevice = null; if (usbDevice instanceof IManagedTestDevice) { IManagedTestDevice managedUsbDevice = (IManagedTestDevice) usbDevice; String ipAndPort = managedUsbDevice.switchToAdbTcp(); if (ipAndPort != null) { CLog.d("Device %s was switched to adb tcp on %s", usbDevice.getSerialNumber(), ipAndPort); tcpDevice = connectToTcpDevice(ipAndPort); if (tcpDevice == null) { // ruh roh, could not connect to device // Try to re-establish connection back to usb device managedUsbDevice.recoverDevice(); } } } else { CLog.e("reconnectDeviceToTcp: unrecognized device type."); } return tcpDevice; } @Override public boolean disconnectFromTcpDevice(ITestDevice tcpDevice) { CLog.i("Disconnecting and freeing tcp device %s", tcpDevice.getSerialNumber()); boolean result = false; try { result = tcpDevice.switchToAdbUsb(); } catch (DeviceNotAvailableException e) { CLog.w("Failed to switch device %s to usb mode: %s", tcpDevice.getSerialNumber(), e.getMessage()); } freeDevice(tcpDevice, FreeDeviceState.IGNORE); return result; } private boolean doAdbConnect(String ipAndPort) { final String resultSuccess = String .format("connected to %s", ipAndPort); for (int i = 1; i <= 3; i++) { String adbConnectResult = executeGlobalAdbCommand("connect", ipAndPort); // runcommand "adb connect ipAndPort" if (adbConnectResult.startsWith(resultSuccess)) { return true; } CLog.w("Failed to connect to device on %s, attempt %d of 3. Response: %s.", ipAndPort, i, adbConnectResult); getRunUtil().sleep(5 * 1000); } return false; } /** * Execute a adb command not targeted to a particular device eg. 'adb * connect' * * @param cmdArgs * @return */ public String executeGlobalAdbCommand(String... cmdArgs) { String[] fullCmd = ArrayUtil .buildArray(new String[] { "adb" }, cmdArgs); CommandResult result = getRunUtil().runTimedCmd(FASTBOOT_CMD_TIMEOUT, fullCmd); if (CommandStatus.SUCCESS.equals(result.getStatus())) { return result.getStdout(); } CLog.w("adb %s failed", cmdArgs[0]); return null; } /** * {@inheritDoc} */ @Override public synchronized void terminate() { checkInit(); if (!mIsTerminated) { mIsTerminated = true; mAdbBridge.removeDeviceChangeListener(mManagedDeviceListener); mAdbBridge.terminate(); if (mFastbootMonitor != null) { mFastbootMonitor.terminate(); } } } /** * {@inheritDoc} */ @Override public synchronized void terminateHard() { checkInit(); if (!mIsTerminated) { for (IManagedTestDevice device : mAllocatedDeviceMap.values()) { device.setRecovery(new AbortRecovery()); } mAdbBridge.disconnectBridge(); terminate(); } } private static class AbortRecovery implements IDeviceRecovery { /** * {@inheritDoc} */ @Override public void recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline) throws DeviceNotAvailableException { throw new DeviceNotAvailableException("aborted test session"); } /** * {@inheritDoc} */ @Override public void recoverDeviceBootloader(IDeviceStateMonitor monitor) throws DeviceNotAvailableException { throw new DeviceNotAvailableException("aborted test session"); } /** * {@inheritDoc} */ @Override public void recoverDeviceRecovery(IDeviceStateMonitor monitor) throws DeviceNotAvailableException { throw new DeviceNotAvailableException("aborted test session"); } } /** * {@inheritDoc} */ @Override public synchronized Collection<String> getAllocatedDevices() { checkInit(); Collection<String> allocatedDeviceSerials = new ArrayList<String>( mAllocatedDeviceMap.size()); allocatedDeviceSerials.addAll(mAllocatedDeviceMap.keySet()); return allocatedDeviceSerials; } /** * {@inheritDoc} */ @Override public synchronized Collection<String> getAvailableDevices() { checkInit(); Collection<String> availableDeviceSerials = new ArrayList<String>( mAvailableDeviceQueue.size()); synchronized (mAvailableDeviceQueue) { for (IDevice device : mAvailableDeviceQueue) { // don't add placeholder devices to available devices display if (!(device instanceof StubDevice)) { availableDeviceSerials.add(device.getSerialNumber()); } } } return availableDeviceSerials; } /** * {@inheritDoc} */ @Override public synchronized Collection<String> getUnavailableDevices() { checkInit(); IDevice[] visibleDevices = mAdbBridge.getDevices(); Collection<String> unavailableSerials = new ArrayList<String>( visibleDevices.length); Collection<String> availSerials = getAvailableDevices(); Collection<String> allocatedSerials = getAllocatedDevices(); for (IDevice device : visibleDevices) { if (!availSerials.contains(device.getSerialNumber()) && !allocatedSerials.contains(device.getSerialNumber())) { unavailableSerials.add(device.getSerialNumber()); } } return unavailableSerials; } private Map<IDevice, String> fetchDevicesInfo() { synchronized (this) { checkInit(); } final Map<IDevice, String> deviceMap = new LinkedHashMap<IDevice, String>(); // these data structures all have their own locks final List<IDevice> allDeviceCopy = ArrayUtil.list(mAdbBridge .getDevices()); final List<IDevice> availableDeviceCopy = mAvailableDeviceQueue .getCopy(); final List<ITestDevice> allocatedDeviceCopy = new ArrayList<ITestDevice>( mAllocatedDeviceMap.values()); final Set<IDevice> visibleDeviceSet = new HashSet<IDevice>(); for (IDevice device : allDeviceCopy) { // ignore devices not matching global filter if (mGlobalDeviceFilter.matches(device)) { visibleDeviceSet.add(device); } } for (ITestDevice device : allocatedDeviceCopy) { deviceMap.put(device.getIDevice(), "Allocated"); visibleDeviceSet.remove(device.getIDevice()); } for (IDevice device : availableDeviceCopy) { // don't add placeholder devices to available devices display if (!(device instanceof StubDevice)) { deviceMap.put(device, "Available"); visibleDeviceSet.remove(device); } } for (IDevice device : visibleDeviceSet) { deviceMap.put(device, "Unavailable"); } return deviceMap; } @Override public void displayDevicesInfo(PrintWriter stream) { ArrayList<List<String>> displayRows = new ArrayList<List<String>>(); displayRows.add(Arrays.asList("Serial", "State", "Product", "Variant", "Build", "Battery")); Map<IDevice, String> deviceMap = fetchDevicesInfo(); IDeviceSelection selector = getDeviceSelectionOptions(); addDevicesInfo(selector, displayRows, deviceMap); new TableFormatter().displayTable(displayRows, stream); } /** * Get the {@link IDeviceSelection} to use to display device info * <p/> * Exposed for unit testing. */ IDeviceSelection getDeviceSelectionOptions() { return new DeviceSelectionOptions(); } private void addDevicesInfo(IDeviceSelection selector, List<List<String>> displayRows, Map<IDevice, String> deviceStateMap) { for (Map.Entry<IDevice, String> deviceEntry : deviceStateMap.entrySet()) { IDevice device = deviceEntry.getKey(); String deviceState = deviceEntry.getValue(); displayRows.add(Arrays.asList(device.getSerialNumber(), deviceState, getDisplay(selector.getDeviceProductType(device)), getDisplay(selector.getDeviceProductVariant(device)), getDisplay(device.getProperty("ro.build.id")), getDisplay(selector.getBatteryLevel(device)))); } } /** * Gets a displayable string for given object * * @param o * @return */ private String getDisplay(Object o) { return o == null ? "unknown" : o.toString(); } /** * A class to listen for and act on device presence updates from ddmlib */ private class ManagedDeviceListener implements IDeviceChangeListener { /** * {@inheritDoc} */ @Override public void deviceChanged(IDevice device, int changeMask) { CLog.i("Device connected " + device.getSerialNumber()); IManagedTestDevice testDevice = mAllocatedDeviceMap.get(device .getSerialNumber()); if ((changeMask & IDevice.CHANGE_STATE) != 0) { if (testDevice != null) { TestDeviceState newState = TestDeviceState .getStateByDdms(device.getState()); testDevice.setDeviceState(newState); } else if (mCheckDeviceMap .containsKey(device.getSerialNumber())) { IDeviceStateMonitor monitor = mCheckDeviceMap.get(device .getSerialNumber()); monitor.setState(TestDeviceState.getStateByDdms(device .getState())); } else if (!mAvailableDeviceQueue.contains(device) && device.getState() == IDevice.DeviceState.ONLINE) { checkAndAddAvailableDevice(device); } } } /** * {@inheritDoc} */ @Override public void deviceConnected(IDevice device) { CLog.d("Detected device connect %s, id %d", device.getSerialNumber(), device.hashCode()); IManagedTestDevice testDevice = mAllocatedDeviceMap.get(device .getSerialNumber()); if (testDevice == null) { if (isValidDeviceSerial(device.getSerialNumber()) && device.getState() == IDevice.DeviceState.ONLINE) { checkAndAddAvailableDevice(device); } else if (mCheckDeviceMap .containsKey(device.getSerialNumber())) { IDeviceStateMonitor monitor = mCheckDeviceMap.get(device .getSerialNumber()); monitor.setState(TestDeviceState.getStateByDdms(device .getState())); } } else { // this device is known already. However DDMS will allocate a // new IDevice, so need // to update the TestDevice record with the new device CLog.d("Updating IDevice for device %s", device.getSerialNumber()); testDevice.setIDevice(device); TestDeviceState newState = TestDeviceState .getStateByDdms(device.getState()); testDevice.setDeviceState(newState); } } private boolean isValidDeviceSerial(String serial) { return serial.length() > 1 && !serial.contains("?"); } /** * {@inheritDoc} */ @Override public void deviceDisconnected(IDevice disconnectedDevice) { if (mAvailableDeviceQueue.remove(disconnectedDevice)) { CLog.i("Removed disconnected device %s from available queue", disconnectedDevice.getSerialNumber()); } IManagedTestDevice testDevice = mAllocatedDeviceMap .get(disconnectedDevice.getSerialNumber()); if (testDevice != null) { testDevice.setDeviceState(TestDeviceState.NOT_AVAILABLE); } else if (mCheckDeviceMap.containsKey(disconnectedDevice .getSerialNumber())) { IDeviceStateMonitor monitor = mCheckDeviceMap .get(disconnectedDevice.getSerialNumber()); monitor.setState(TestDeviceState.NOT_AVAILABLE); } updateDeviceMonitor(); } } /** * {@inheritDoc} */ @Override public void addFastbootListener(IFastbootListener listener) { checkInit(); if (mFastbootEnabled) { mFastbootListeners.add(listener); } else { throw new UnsupportedOperationException("fastboot is not enabled"); } } /** * {@inheritDoc} */ @Override public void removeFastbootListener(IFastbootListener listener) { checkInit(); if (mFastbootEnabled) { mFastbootListeners.remove(listener); } } private class FastbootMonitor extends Thread { private boolean mQuit = false; FastbootMonitor() { super("FastbootMonitor"); } public void terminate() { mQuit = true; interrupt(); } @Override public void run() { while (!mQuit) { // only poll fastboot devices if there are listeners, as polling // it // indiscriminately can cause fastboot commands to hang if (!mFastbootListeners.isEmpty()) { Set<String> serials = getDevicesOnFastboot(); if (serials != null) { for (String serial : serials) { IManagedTestDevice testDevice = mAllocatedDeviceMap .get(serial); if (testDevice != null && !testDevice.getDeviceState().equals( TestDeviceState.FASTBOOT)) { testDevice .setDeviceState(TestDeviceState.FASTBOOT); } } // now update devices that are no longer on fastboot synchronized (mAllocatedDeviceMap) { for (IManagedTestDevice testDevice : mAllocatedDeviceMap .values()) { if (!serials.contains(testDevice .getSerialNumber()) && testDevice.getDeviceState().equals( TestDeviceState.FASTBOOT)) { testDevice .setDeviceState(TestDeviceState.NOT_AVAILABLE); } } } // create a copy of listeners for notification to // prevent deadlocks Collection<IFastbootListener> listenersCopy = new ArrayList<IFastbootListener>( mFastbootListeners.size()); listenersCopy.addAll(mFastbootListeners); for (IFastbootListener listener : listenersCopy) { listener.stateUpdated(); } } } getRunUtil().sleep(FASTBOOT_POLL_WAIT_TIME); } } } private Set<String> getDevicesOnFastboot() { CommandResult fastbootResult = getRunUtil().runTimedCmd( FASTBOOT_CMD_TIMEOUT, "fastboot", "devices"); if (fastbootResult.getStatus().equals(CommandStatus.SUCCESS)) { CLog.v("fastboot devices returned\n %s", fastbootResult.getStdout()); return parseDevicesOnFastboot(fastbootResult.getStdout()); } else { CLog.w("'fastboot devices' failed. Result: %s, stderr: %s", fastbootResult.getStatus(), fastbootResult.getStderr()); } return null; } static Set<String> parseDevicesOnFastboot(String fastbootOutput) { Set<String> serials = new HashSet<String>(); Pattern fastbootPattern = Pattern .compile("([\\w\\d]+)\\s+fastboot\\s*"); Matcher fastbootMatcher = fastbootPattern.matcher(fastbootOutput); while (fastbootMatcher.find()) { serials.add(fastbootMatcher.group(1)); } return serials; } }