| // 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<AdbCommandResult> _executeCommand( |
| String executable, List<String> args, |
| {String stdin, Duration timeout}) { |
| Future<String> getOutput(Stream<List<int>> 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); |
| } |
| 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], results[1], results[2], 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<AndroidEmulator> 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<String> 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<bool> 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 <enter>. |
| 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; |
| |
| 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']); |
| } |
| |
| /** |
| * 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<AdbCommandResult> runAdbCommand(List<String> adbArgs, |
| {Duration timeout}) { |
| return _executeCommand( |
| "adb", _deviceSpecificArgs(adbArgs), timeout: timeout); |
| } |
| |
| Future<AdbCommandResult> runAdbShellCommand(List<String> 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. |
| assert(result.exitCode != 0); |
| } |
| } |
| return new AdbCommandResult( |
| result.command, result.stdout, result.stderr, exitCode, |
| result.timedOut); |
| } |
| |
| Future<AdbCommandResult> _adbCommand(List<String> adbArgs) async { |
| var result = await _executeCommand("adb", _deviceSpecificArgs(adbArgs)); |
| result.throwIfFailed(); |
| return result; |
| } |
| |
| List<String> _deviceSpecificArgs(List<String> 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<List<String>> 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) |
| .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<AdbDevice> _idleDevices = new Queue<AdbDevice>(); |
| final Queue<Completer> _waiter = new Queue<Completer>(); |
| |
| AdbDevicePool(List<AdbDevice> idleDevices) { |
| _idleDevices.addAll(idleDevices); |
| } |
| |
| static Future<AdbDevicePool> 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!'); |
| } |
| return new AdbDevicePool(devices); |
| } |
| |
| Future<AdbDevice> 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); |
| } |
| } |
| } |