Files
sdk/tools/testing/dart/android.dart
T
Bob Nystrom 7ce978bf91 Tighten types in test.dart even more.
This removes all implicit casts and many implicit uses of dynamic. It
adds a gratuitous number of explicit "as" casts and arguably makes the
code worse.

This is an interim step towards replacing the big configuration map
with an actual typed object. These "as" casts should help catch places
where the configuration object is being used and where the code will
need to be changed to use a new object.

R=sigmund@google.com

Review-Url: https://codereview.chromium.org/2903703002 .
2017-05-30 14:02:24 -07:00

453 lines
12 KiB
Dart

// 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);
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<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<bool>();
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;
Map<String, String> _cachedData = new Map<String, String>();
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<Null> waitForBootCompleted() async {
while (true) {
try {
AdbCommandResult result =
await _adbCommand(['shell', 'getprop', 'sys.boot_completed']);
if (result.stdout.trim() == '1') return;
} catch (_) {}
await new Future<Null>.delayed(const Duration(seconds: 2));
}
}
/**
* Put adb in root mode.
*/
Future<bool> adbRoot() {
var adbRootCompleter = new Completer<bool>();
_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<AdbCommandResult> 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<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 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<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!');
}
print("Found ${devices.length} Android devices.");
return new AdbDevicePool(devices);
}
Future<AdbDevice> acquireDevice() async {
if (_idleDevices.length > 0) {
return _idleDevices.removeFirst();
} else {
var completer = new Completer<AdbDevice>();
_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);
}
}
}