// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. library android; import "dart:async"; import "dart:convert" show LineSplitter, utf8; import "dart:core"; import "dart:collection"; import "dart:io"; import "path.dart"; import "utils.dart"; class AdbCommandResult { final String command; final String stdout; final String stderr; final int exitCode; final bool timedOut; AdbCommandResult( this.command, this.stdout, this.stderr, this.exitCode, this.timedOut); void throwIfFailed() { if (exitCode != 0) { var error = "Running: $command failed:" "stdout:\n ${stdout.trim()}\n" "stderr:\n ${stderr.trim()}\n" "exitCode: $exitCode\n" "timedOut: $timedOut"; throw new Exception(error); } } } /** * [_executeCommand] will write [stdin] to the standard input of the created * process and will return a tuple (stdout, stderr). * * If the exit code of the process was nonzero it will complete with an error. * If starting the process failed, it will complete with an error as well. */ Future _executeCommand(String executable, List args, {String stdin, Duration timeout}) { Future getOutput(Stream> stream) { return stream .transform(utf8.decoder) .toList() .then((data) => data.join("")); } return Process.start(executable, args).then((Process process) async { if (stdin != null && stdin != '') { process.stdin.write(stdin); await process.stdin.flush(); } process.stdin.close(); Timer timer; bool timedOut = false; if (timeout != null) { timer = new Timer(timeout, () { timedOut = true; process.kill(ProcessSignal.sigterm); timer = null; }); } var results = await Future.wait([ getOutput(process.stdout), getOutput(process.stderr), process.exitCode ]); if (timer != null) timer.cancel(); String command = "$executable ${args.join(' ')}"; return new AdbCommandResult(command, results[0] as String, results[1] as String, results[2] as int, timedOut); }); } /** * Helper class to loop through all adb ports. * * The ports come in pairs: * - even number: console connection * - odd number: adb connection * Note that this code doesn't check if the ports are used. */ class AdbServerPortPool { static int MIN_PORT = 5554; static int MAX_PORT = 5584; static int _nextPort = MIN_PORT; static int next() { var port = _nextPort; if (port > MAX_PORT) { throw new Exception("All ports are used."); } _nextPort += 2; return port; } } /** * Represents the interface to the emulator. * New emulators can be launched by calling the static [launchNewEmulator] * method. */ class AndroidEmulator { int _port; Process _emulatorProcess; AdbDevice _adbDevice; int get port => _port; AdbDevice get adbDevice => _adbDevice; static Future launchNewEmulator(String avdName) { var portNumber = AdbServerPortPool.next(); var args = ['-avd', '$avdName', '-port', "$portNumber" /*, '-gpu', 'on'*/]; return Process.start("emulator64-arm", args).then((Process process) { var adbDevice = new AdbDevice('emulator-$portNumber'); return new AndroidEmulator._private(portNumber, adbDevice, process); }); } AndroidEmulator._private(this._port, this._adbDevice, this._emulatorProcess) { Stream getLines(Stream s) { return s.transform(utf8.decoder).transform(new LineSplitter()); } getLines(_emulatorProcess.stdout).listen((line) { log("stdout: ${line.trim()}"); }); getLines(_emulatorProcess.stderr).listen((line) { log("stderr: ${line.trim()}"); }); _emulatorProcess.exitCode.then((exitCode) { log("emulator exited with exitCode: $exitCode."); }); } Future kill() { var completer = new Completer(); if (_emulatorProcess.kill()) { _emulatorProcess.exitCode.then((exitCode) { // TODO: Should we use exitCode to do something clever? completer.complete(true); }); } else { log("Sending kill signal to emulator process failed"); completer.complete(false); } return completer.future; } void log(String msg) { DebugLogger.info("AndroidEmulator(${_adbDevice.deviceId}): $msg"); } } /** * Helper class to create avd device configurations. */ class AndroidHelper { static Future createAvd(String name, String target) async { var args = [ '--silent', 'create', 'avd', '--name', '$name', '--target', '$target', '--force', '--abi', 'armeabi-v7a' ]; // We're adding newlines to stdin to simulate . var result = await _executeCommand("android", args, stdin: "\n\n\n\n"); result.throwIfFailed(); } } /** * Used for communicating with an emulator or with a real device. */ class AdbDevice { static const _adbServerStartupTime = const Duration(seconds: 3); String _deviceId; Map _cachedData = new Map(); String get deviceId => _deviceId; AdbDevice(this._deviceId); /** * Blocks execution until the device is online */ Future waitForDevice() { return _adbCommand(['wait-for-device']); } /** * Polls the 'sys.boot_completed' property. Returns as soon as the property is * 1. */ Future waitForBootCompleted() async { while (true) { try { AdbCommandResult result = await _adbCommand(['shell', 'getprop', 'sys.boot_completed']); if (result.stdout.trim() == '1') return; } catch (_) {} await new Future.delayed(const Duration(seconds: 2)); } } /** * Put adb in root mode. */ Future adbRoot() { var adbRootCompleter = new Completer(); _adbCommand(['root']).then((_) { // TODO: Figure out a way to wait until the adb daemon was restarted in // 'root mode' on the device. new Timer(_adbServerStartupTime, () => adbRootCompleter.complete(true)); }).catchError((error) => adbRootCompleter.completeError(error)); return adbRootCompleter.future; } /** * Download data form the device. */ Future pullData(Path remote, Path local) { return _adbCommand(['pull', '$remote', '$local']); } /** * Upload data to the device. */ Future pushData(Path local, Path remote) { return _adbCommand(['push', '$local', '$remote']); } /** * Upload data to the device, unless [local] is the same as the most recently * used source for [remote]. */ Future pushCachedData(String local, String remote) { if (_cachedData[remote] == local) { return new Future.value( new AdbCommandResult("Skipped cached push", "", "", 0, false)); } _cachedData[remote] = local; return _adbCommand(['push', local, remote]); } /** * Change permission of directory recursively. */ Future chmod(String mode, Path directory) { var arguments = ['shell', 'chmod', '-R', mode, '$directory']; return _adbCommand(arguments); } /** * Install an application on the device. */ Future installApk(Path filename) { return _adbCommand( ['install', '-i', 'com.google.android.feedback', '-r', '$filename']); } /** * Start the given intent on the device. */ Future startActivity(Intent intent) { var arguments = [ 'shell', 'am', 'start', '-W', '-a', intent.action, '-n', "${intent.package}/${intent.activity}" ]; if (intent.dataUri != null) { arguments.addAll(['-d', intent.dataUri]); } return _adbCommand(arguments); } /** * Force to stop everything associated with [package]. */ Future forceStop(String package) { var arguments = ['shell', 'am', 'force-stop', package]; return _adbCommand(arguments); } /** * Set system property name to value. */ Future setProp(String name, String value) { return _adbCommand(['shell', 'setprop', name, value]); } /** * Kill all background processes. */ Future killAll() { var arguments = ['shell', 'am', 'kill-all']; return _adbCommand(arguments); } Future runAdbCommand(List adbArgs, {Duration timeout}) { return _executeCommand("adb", _deviceSpecificArgs(adbArgs), timeout: timeout); } Future runAdbShellCommand(List shellArgs, {Duration timeout}) async { const MARKER = 'AdbShellExitCode: '; // The exitcode of 'adb shell ...' can be 0 even though the command failed // with a non-zero exit code. We therefore explicitly print it to stdout and // search for it. var args = ['shell', "${shellArgs.join(' ')} ; echo $MARKER \$?"]; AdbCommandResult result = await _executeCommand( "adb", _deviceSpecificArgs(args), timeout: timeout); int exitCode = result.exitCode; var lines = result.stdout .split('\n') .where((line) => line.trim().length > 0) .toList(); if (lines.length > 0) { int index = lines.last.indexOf(MARKER); if (index >= 0) { exitCode = int.parse(lines.last.substring(index + MARKER.length).trim()); if (exitCode > 128 && exitCode <= 128 + 31) { // Return negative exit codes for signals 1..31 (128+N for signal N) exitCode = 128 - exitCode; } } else { // In case of timeouts, for example, we won't get the exitcode marker. // TODO(mkroghj): Some times tests fail with the assert below. To better // investigate, write out debug info. DebugLogger.info("======= THIS IS DEBUG INFORMATION ======="); DebugLogger.info("arguments: $args"); DebugLogger.info("exitCode: ${result.exitCode}"); DebugLogger.info("timedOut: ${result.timedOut}"); DebugLogger.info("---- std out ----"); DebugLogger.info(result.stdout); DebugLogger.info("---- std out end ----"); DebugLogger.info("---- std error ----"); DebugLogger.info(result.stderr); DebugLogger.info("---- std error end ----"); DebugLogger.info("======= THIS IS NO LONGER DEBUG INFORMATION ======="); assert(result.exitCode != 0); } } return new AdbCommandResult(result.command, result.stdout, result.stderr, exitCode, result.timedOut); } Future _adbCommand(List adbArgs) async { var result = await _executeCommand("adb", _deviceSpecificArgs(adbArgs)); result.throwIfFailed(); return result; } List _deviceSpecificArgs(List adbArgs) { if (_deviceId != null) { var extendedAdbArgs = ['-s', _deviceId]; extendedAdbArgs.addAll(adbArgs); adbArgs = extendedAdbArgs; } return adbArgs; } } /** * Helper to list all adb devices available. */ class AdbHelper { static RegExp _deviceLineRegexp = new RegExp(r'^([a-zA-Z0-9_-]+)[ \t]+device$', multiLine: true); static Future> listDevices() { return Process.run('adb', ['devices']).then((ProcessResult result) { if (result.exitCode != 0) { throw new Exception("Could not list devices [stdout: ${result.stdout}," "stderr: ${result.stderr}]"); } return _deviceLineRegexp .allMatches(result.stdout as String) .map((Match m) => m.group(1)) .toList(); }); } } /** * Represents an android intent. */ class Intent { String action; String package; String activity; String dataUri; Intent(this.action, this.package, this.activity, [this.dataUri]); } /** * Discovers all available devices and supports acquire/release. */ class AdbDevicePool { final Queue _idleDevices = new Queue(); final Queue _waiter = new Queue(); AdbDevicePool(List idleDevices) { _idleDevices.addAll(idleDevices); } static Future create() async { var names = await AdbHelper.listDevices(); var devices = names.map((id) => new AdbDevice(id)).toList(); if (devices.length == 0) { throw new Exception('No android devices found. ' 'Please make sure "adb devices" shows your device!'); } print("Found ${devices.length} Android devices."); return new AdbDevicePool(devices); } Future acquireDevice() async { if (_idleDevices.length > 0) { return _idleDevices.removeFirst(); } else { var completer = new Completer(); _waiter.add(completer); return completer.future; } } void releaseDevice(AdbDevice device) { if (_waiter.length > 0) { Completer completer = _waiter.removeFirst(); completer.complete(device); } else { _idleDevices.add(device); } } }