1 /* 2 * Copyright (C) 2008 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 17 package com.android.cts; 18 19 import com.android.ddmlib.AdbCommandRejectedException; 20 import com.android.ddmlib.AndroidDebugBridge; 21 import com.android.ddmlib.IDevice; 22 import com.android.ddmlib.TimeoutException; 23 import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; 24 25 import java.io.IOException; 26 import java.util.ArrayList; 27 import java.util.concurrent.Semaphore; 28 import java.util.concurrent.TimeUnit; 29 30 /** 31 * Initializing and managing devices. 32 */ 33 public class DeviceManager implements IDeviceChangeListener { 34 35 private static final int SHORT_DELAY = 1000 * 15; // 15 seconds 36 private static final int LONG_DELAY = 1000 * 60 * 10; // 10 minutes 37 /** Time to wait after issuing reboot command */ 38 private static final int REBOOT_DELAY = 5 * 1000; // 5 seconds 39 /** Time to wait after device reports that boot is complete. */ 40 private static final int POST_BOOT_DELAY = 1000 * 60; // 1 minute 41 /** Maximal number of attempts to restart ADB connection. */ 42 private static final int MAX_ADB_RESTART_ATTEMPTS = 10; 43 ArrayList<TestDevice> mDevices; 44 /** This is used during device restart for blocking until the device has been reconnected. */ 45 private Semaphore mSemaphore = new Semaphore(0); 46 DeviceManager()47 public DeviceManager() { 48 mDevices = new ArrayList<TestDevice>(); 49 } 50 51 /** 52 * Initialize Android debug bridge. This function should be called after 53 * {@link DeviceManager} initialized. 54 */ initAdb()55 public void initAdb() { 56 String adbLocation = getAdbLocation(); 57 58 Log.d("init adb..."); 59 AndroidDebugBridge.init(true); 60 AndroidDebugBridge.addDeviceChangeListener(this); 61 AndroidDebugBridge.createBridge(adbLocation, true); 62 } 63 64 /** 65 * Get the location of the adb command. 66 * 67 * @return The location of the adb location. 68 */ getAdbLocation()69 public static String getAdbLocation() { 70 return "adb"; 71 } 72 73 /** 74 * Allocate devices by specified number for testing. 75 * @param num the number of required device 76 * @return the specified number of devices. 77 */ allocateDevices(final int num)78 public TestDevice[] allocateDevices(final int num) throws DeviceNotAvailableException { 79 80 ArrayList<TestDevice> deviceList; 81 TestDevice td; 82 int index = 0; 83 84 if (num < 0) { 85 throw new IllegalArgumentException(); 86 } 87 if (num > mDevices.size()) { 88 throw new DeviceNotAvailableException("The number of connected device(" 89 + mDevices.size() + " is less than the specified number(" 90 + num + "). Please plug in enough devices"); 91 } 92 deviceList = new ArrayList<TestDevice>(); 93 94 while (index < mDevices.size() && deviceList.size() != num) { 95 td = mDevices.get(index); 96 if (td.getStatus() == TestDevice.STATUS_IDLE) { 97 deviceList.add(td); 98 } 99 index++; 100 } 101 if (deviceList.size() != num) { 102 throw new DeviceNotAvailableException("Can't get the specified number(" 103 + num + ") of idle device(s)."); 104 } 105 return deviceList.toArray(new TestDevice[num]); 106 } 107 108 /** 109 * Get TestDevice list that available for executing tests. 110 * 111 * @return The device list. 112 */ getDeviceList()113 public final TestDevice[] getDeviceList() { 114 return mDevices.toArray(new TestDevice[mDevices.size()]); 115 } 116 117 /** 118 * Get the number of all free devices. 119 * 120 * @return the number of all free devices 121 */ getCountOfFreeDevices()122 public int getCountOfFreeDevices() { 123 int count = 0; 124 for (TestDevice td : mDevices) { 125 if (td.getStatus() == TestDevice.STATUS_IDLE) { 126 count++; 127 } 128 } 129 return count; 130 } 131 132 /** 133 * Append the device to the device list. 134 * 135 * @param device The device to be appended to the device list. 136 */ appendDevice(final IDevice device)137 private void appendDevice(final IDevice device) { 138 if (-1 == getDeviceIndex(device)) { 139 TestDevice td = new TestDevice(device); 140 mDevices.add(td); 141 } 142 } 143 144 /** 145 * Remove specified TestDevice from managed list. 146 * 147 * @param device The device to be removed from the device list. 148 */ removeDevice(final IDevice device)149 private void removeDevice(final IDevice device) { 150 int index = getDeviceIndex(device); 151 if (index == -1) { 152 Log.d("Can't find " + device + " in device list of DeviceManager"); 153 return; 154 } 155 mDevices.get(index).disconnected(); 156 mDevices.remove(index); 157 } 158 159 /** 160 * Get the index of the specified device in the device array. 161 * 162 * @param device The device to be found. 163 * @return The index of the device if it exists; else -1. 164 */ getDeviceIndex(final IDevice device)165 private int getDeviceIndex(final IDevice device) { 166 TestDevice td; 167 168 for (int index = 0; index < mDevices.size(); index++) { 169 td = mDevices.get(index); 170 if (td.getSerialNumber().equals(device.getSerialNumber())) { 171 return index; 172 } 173 } 174 return -1; 175 } 176 177 /** 178 * Search a <code>TestDevice</code> by serial number. 179 * 180 * @param deviceSerialNumber The serial number of the device to be found. 181 * @return The test device, if it exists, otherwise null. 182 */ searchTestDevice(final String deviceSerialNumber)183 private TestDevice searchTestDevice(final String deviceSerialNumber) { 184 for (TestDevice td : mDevices) { 185 if (td.getSerialNumber().equals(deviceSerialNumber)) { 186 return td; 187 } 188 } 189 return null; 190 } 191 192 /** {@inheritDoc} */ deviceChanged(IDevice device, int changeMask)193 public void deviceChanged(IDevice device, int changeMask) { 194 Log.d("device " + device.getSerialNumber() + " changed with changeMask=" + changeMask); 195 Log.d("Device state:" + device.getState()); 196 } 197 198 /** {@inheritDoc} */ deviceConnected(IDevice device)199 public void deviceConnected(IDevice device) { 200 new DeviceServiceMonitor(device).start(); 201 } 202 203 /** 204 * To make sure that connection between {@link AndroidDebugBridge} 205 * and {@link IDevice} is initialized properly. In fact, it just make sure 206 * the sync service isn't null and device's build values are collected 207 * before appending device. 208 */ 209 private class DeviceServiceMonitor extends Thread { 210 private IDevice mDevice; 211 DeviceServiceMonitor(IDevice device)212 public DeviceServiceMonitor(IDevice device) { 213 mDevice = device; 214 } 215 216 @Override run()217 public void run() { 218 try { 219 while (mDevice.getSyncService() == null || mDevice.getPropertyCount() == 0) { 220 try { 221 Thread.sleep(100); 222 } catch (InterruptedException e) { 223 Log.d("polling for device sync service interrupted"); 224 } 225 } 226 CUIOutputStream.println("Device(" + mDevice + ") connected"); 227 if (!TestSession.isADBServerRestartedMode()) { 228 CUIOutputStream.printPrompt(); 229 } 230 appendDevice(mDevice); 231 // increment the counter semaphore to unblock threads waiting for devices 232 mSemaphore.release(); 233 } catch (IOException e) { 234 // FIXME: handle failed connection to device. 235 } catch (TimeoutException e) { 236 // FIXME: handle failed connection to device. 237 } catch (AdbCommandRejectedException e) { 238 // FIXME: handle failed connection to device. 239 } 240 } 241 } 242 243 /** {@inheritDoc} */ deviceDisconnected(IDevice device)244 public void deviceDisconnected(IDevice device) { 245 removeDevice(device); 246 } 247 248 /** 249 * Allocate device by specified Id for testing. 250 * @param deviceId the ID of the test device. 251 * @return a {@link TestDevice} if the specified device is free. 252 */ allocateFreeDeviceById(String deviceId)253 public TestDevice allocateFreeDeviceById(String deviceId) throws DeviceNotAvailableException { 254 for (TestDevice td : mDevices) { 255 if (td.getSerialNumber().equals(deviceId)) { 256 if (td.getStatus() != TestDevice.STATUS_IDLE) { 257 String msg = "The specifed device(" + deviceId + ") is " + 258 td.getStatusAsString(); 259 throw new DeviceNotAvailableException(msg); 260 } 261 return td; 262 } 263 } 264 throw new DeviceNotAvailableException("The specified device(" + 265 deviceId + "cannot be found"); 266 } 267 268 /** 269 * Reset the online {@link TestDevice} to STATUS_IDLE 270 * 271 * @param device of the specified {@link TestDevice} 272 */ resetTestDevice(final TestDevice device)273 public void resetTestDevice(final TestDevice device) { 274 if (device.getStatus() != TestDevice.STATUS_OFFLINE) { 275 device.setStatus(TestDevice.STATUS_IDLE); 276 } 277 } 278 279 /** 280 * Restart ADB server. 281 * 282 * @param ts The test session. 283 */ restartADBServer(TestSession ts)284 public void restartADBServer(TestSession ts) throws DeviceDisconnectedException { 285 try { 286 Thread.sleep(SHORT_DELAY); // time to collect outstanding logs 287 Log.i("Restarting device ..."); 288 rebootDevice(ts); 289 Log.i("Restart complete."); 290 } catch (InterruptedException e) { 291 e.printStackTrace(); 292 } 293 } 294 295 /** 296 * Reboot the device. 297 * 298 * @param ts The test session. 299 */ rebootDevice(TestSession ts)300 private void rebootDevice(TestSession ts) throws InterruptedException, 301 DeviceDisconnectedException { 302 303 String deviceSerialNumber = ts.getDeviceId(); 304 if (!deviceSerialNumber.toLowerCase().startsWith("emulator")) { 305 // try to reboot the device using the command line adb 306 // TODO: do we need logic to retry this 307 executeCommand("adb -s " + deviceSerialNumber + " reboot"); 308 // wait to make sure the reboot gets through before we tear down the connection 309 310 // TODO: this is flaky, no guarantee device has actually rebooted, host should wait till 311 // device goes offline 312 Thread.sleep(REBOOT_DELAY); 313 314 int attempts = 0; 315 boolean deviceConnected = false; 316 while (!deviceConnected && (attempts < MAX_ADB_RESTART_ATTEMPTS)) { 317 AndroidDebugBridge.disconnectBridge(); 318 319 // kill the server while the device is rebooting 320 executeCommand("adb kill-server"); 321 322 // Reset the device counter semaphore. We will wait below until at least one device 323 // has come online. This can happen any time during or after the call to 324 // createBridge(). The counter gets increased by the DeviceServiceMonitor when a 325 // device is added. 326 mSemaphore.drainPermits(); 327 AndroidDebugBridge.createBridge(getAdbLocation(), true); 328 329 boolean deviceFound = false; 330 while (!deviceFound) { 331 // wait until at least one device has been added 332 mSemaphore.tryAcquire(LONG_DELAY, TimeUnit.MILLISECONDS); 333 TestDevice device = searchTestDevice(deviceSerialNumber); 334 if (device != null) { 335 ts.setTestDevice(device); 336 deviceFound = true; 337 deviceConnected = device.waitForBootComplete(); 338 // After boot is complete, the ADB connection sometimes drops 339 // for a short time. Wait for things to stabilize. 340 try { 341 Thread.sleep(POST_BOOT_DELAY); 342 } catch (InterruptedException ignored) { 343 // ignore 344 } 345 // If the connection dropped during the sleep above, the TestDevice 346 // instance is no longer valid. 347 TestDevice newDevice = searchTestDevice(deviceSerialNumber); 348 if (newDevice != null) { 349 ts.setTestDevice(newDevice); 350 if (newDevice != device) { 351 // the connection was dropped or a second reboot occurred 352 // TODO: replace the hardcoded /sdcard 353 String cmd = String.format("adb -s %s shell bugreport -o " + 354 "/sdcard/bugreports/doubleReboot", deviceSerialNumber); 355 executeCommand(cmd); 356 } 357 } else { 358 // connection dropped and has not come back up 359 deviceFound = false; // go wait for next semaphore permit 360 } 361 } 362 } 363 attempts += 1; 364 } 365 } 366 } 367 368 /** 369 * Execute the given command and wait for its completion. 370 * 371 * @param command The command to be executed. 372 * @return True if the command was executed successfully, otherwise false. 373 */ executeCommand(String command)374 private boolean executeCommand(String command) { 375 Log.d("executeCommand(): cmd=" + command); 376 try { 377 Process proc = Runtime.getRuntime().exec(command); 378 TimeoutThread tt = new TimeoutThread(proc, SHORT_DELAY); 379 tt.start(); 380 proc.waitFor(); // ignore exit value 381 tt.interrupt(); // wake timeout thread 382 } catch (Exception e) { 383 return false; 384 } 385 return true; 386 } 387 388 class TimeoutThread extends Thread { 389 Process mProcess; 390 long mTimeout; 391 TimeoutThread(Process process, long timeout)392 TimeoutThread(Process process, long timeout) { 393 mProcess = process; 394 mTimeout = timeout; 395 } 396 397 @Override run()398 public void run() { 399 try { 400 Thread.sleep(mTimeout); 401 } catch (InterruptedException e) { 402 // process has already completed 403 return; 404 } 405 // destroy process and wake up thread waiting for its completion 406 mProcess.destroy(); 407 } 408 } 409 } 410