// 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:io"; import "utils.dart"; Future _executeCommand(String executable, List args, [String stdin = ""]) { return _executeCommandRaw(executable, args, stdin).then((results) => null); } Future _executeCommandGetOutput(String executable, List args, [String stdin = ""]) { return _executeCommandRaw(executable, args, stdin) .then((output) => output); } /** * [_executeCommandRaw] 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 _executeCommandRaw(String executable, List args, [String stdin = ""]) { Future getOutput(Stream> stream) { return stream.transform(UTF8.decoder).toList() .then((data) => data.join("")); } DebugLogger.info("Running: '\$ $executable ${args.join(' ')}'"); return Process.start(executable, args).then((Process process) { if (stdin != null && stdin != '') { process.stdin.write(stdin); } process.stdin.close(); var futures = [getOutput(process.stdout), getOutput(process.stderr), process.exitCode]; return Future.wait(futures).then((results) { bool success = results[2] == 0; if (!success) { var error = "Running: '\$ $executable ${args.join(' ')}' failed:" "stdout: \n ${results[0]}" "stderr: \n ${results[1]}" "exitCode: \n ${results[2]}"; throw new Exception(error); } return results[0]; }); }); } /** * 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) { var args = ['--silent', 'create', 'avd', '--name', '$name', '--target', '$target', '--force', '--abi', 'armeabi-v7a']; // We're adding newlines to stdin to simulate . return _executeCommand("android", args, "\n\n\n\n"); } } /** * 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() { var timeout = const Duration(seconds: 2); var completer = new Completer(); checkUntilBooted() { _adbCommandGetOutput(['shell', 'getprop', 'sys.boot_completed']) .then((String stdout) { stdout = stdout.trim(); if (stdout == '1') { completer.complete(); } else { new Timer(timeout, checkUntilBooted); } }).catchError((error) { new Timer(timeout, checkUntilBooted); }); } checkUntilBooted(); return completer.future; } /** * 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 _adbCommand(List adbArgs) { if (_deviceId != null) { var extendedAdbArgs = ['-s', _deviceId]; extendedAdbArgs.addAll(adbArgs); adbArgs = extendedAdbArgs; } return _executeCommand("adb", adbArgs); } Future _adbCommandGetOutput(List adbArgs) { if (_deviceId != null) { var extendedAdbArgs = ['-s', _deviceId]; extendedAdbArgs.addAll(adbArgs); adbArgs = extendedAdbArgs; } return _executeCommandGetOutput("adb", adbArgs); } } /** * Helper to list all adb devicess 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) .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]); }