blob: 60dcc48f3c8971708508b399d8bbb4280734d3b8 [file] [log] [blame]
// 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<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(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<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) {
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]);
}