| Index: tools/testing/dart/android.dart
|
| diff --git a/tools/testing/dart/android.dart b/tools/testing/dart/android.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..328cd075a2ec4f102809f28fabec8e437a8ace4e
|
| --- /dev/null
|
| +++ b/tools/testing/dart/android.dart
|
| @@ -0,0 +1,327 @@
|
| +// 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:io";
|
| +import "dart:async";
|
| +import "dart:core";
|
| +import "dart:utf";
|
| +
|
| +import "utils.dart";
|
| +
|
| +Future _executeCommand(String executable,
|
| + List<String> args,
|
| + [String stdin = ""]) {
|
| + return _executeCommandRaw(executable, args, stdin).then((results) => null);
|
| +}
|
| +
|
| +Future _executeCommandGetOutput(String executable,
|
| + List<String> 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<String> args,
|
| + [String stdin = ""]) {
|
| + Future<String> getOutput(Stream<List<int>> stream) {
|
| + return stream.transform(new StringDecoder())
|
| + .reduce(new StringBuffer(), (buf, data) {
|
| + buf.write(data);
|
| + return buf;
|
| + }).then((buf) => buf.toString());
|
| + }
|
| +
|
| + 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<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(new StringDecoder()).transform(new LineTransformer());
|
| + }
|
| +
|
| + 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) {
|
| + var args = ['--silent', 'create', 'avd', '--name', '$name',
|
| + '--target', '$target', '--force', '--abi', 'armeabi-v7a'];
|
| + // We're adding newlines to stdin to simulate <enter>.
|
| + 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();
|
| + return _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);
|
| + }
|
| +
|
| + /**
|
| + * Kill all background processes.
|
| + */
|
| + Future killAll() {
|
| + var arguments = ['shell', 'am', 'kill-all'];
|
| + return _adbCommand(arguments);
|
| + }
|
| +
|
| + Future _adbCommand(List<String> adbArgs) {
|
| + if (_deviceId != null) {
|
| + var extendedAdbArgs = ['-s', _deviceId];
|
| + extendedAdbArgs.addAll(adbArgs);
|
| + adbArgs = extendedAdbArgs;
|
| + }
|
| + return _executeCommand("adb", adbArgs);
|
| + }
|
| +
|
| + Future<String> _adbCommandGetOutput(List<String> 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<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]);
|
| +}
|
| +
|
|
|