import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'utils.dart';
DeviceDiscovery get devices => DeviceDiscovery();
enum DeviceOperatingSystem { android, ios }
DeviceOperatingSystem deviceOperatingSystem = DeviceOperatingSystem.android;
abstract class DeviceDiscovery {
factory DeviceDiscovery() {
switch (deviceOperatingSystem) {
case DeviceOperatingSystem.android:
return AndroidDeviceDiscovery();
case DeviceOperatingSystem.ios:
return IosDeviceDiscovery();
default:
throw StateError('Unsupported device operating system: {config.deviceOperatingSystem}');
}
}
Future<void> chooseWorkingDevice();
Future<Device> get workingDevice;
Future<List<String>> discoverDevices();
Future<Map<String, HealthCheckResult>> checkDevices();
Future<void> performPreflightTasks();
}
abstract class Device {
String get deviceId;
Future<bool> isAwake();
Future<bool> isAsleep();
Future<void> wakeUp();
Future<void> sendToSleep();
Future<void> togglePower();
Future<void> unlock();
Future<void> tap(int x, int y);
Future<Map<String, dynamic>> getMemoryStats(String packageName);
Stream<String> get logcat;
Future<void> stop(String packageName);
}
class AndroidDeviceDiscovery implements DeviceDiscovery {
factory AndroidDeviceDiscovery() {
return _instance ??= AndroidDeviceDiscovery._();
}
AndroidDeviceDiscovery._();
static final RegExp _kDeviceRegex = RegExp(r'^(\S+)\s+(\S+)(.*)');
static AndroidDeviceDiscovery _instance;
AndroidDevice _workingDevice;
@override
Future<AndroidDevice> get workingDevice async {
if (_workingDevice == null) {
await chooseWorkingDevice();
}
return _workingDevice;
}
@override
Future<void> chooseWorkingDevice() async {
final List<Device> allDevices = (await discoverDevices())
.map<Device>((String id) => AndroidDevice(deviceId: id))
.toList();
if (allDevices.isEmpty)
throw 'No Android devices detected';
_workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
}
@override
Future<List<String>> discoverDevices() async {
final List<String> output = (await eval(adbPath, <String>['devices', '-l'], canFail: false))
.trim().split('\n');
final List<String> results = <String>[];
for (String line in output) {
if (line.startsWith('* daemon '))
continue;
if (line.startsWith('List of devices'))
continue;
if (_kDeviceRegex.hasMatch(line)) {
final Match match = _kDeviceRegex.firstMatch(line);
final String deviceID = match[1];
final String deviceState = match[2];
if (!const <String>['unauthorized', 'offline'].contains(deviceState)) {
results.add(deviceID);
}
} else {
throw 'Failed to parse device from adb output: "$line"';
}
}
return results;
}
@override
Future<Map<String, HealthCheckResult>> checkDevices() async {
final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
for (String deviceId in await discoverDevices()) {
try {
final AndroidDevice device = AndroidDevice(deviceId: deviceId);
await device._getWakefulness();
results['android-device-$deviceId'] = HealthCheckResult.success();
} catch (e, s) {
results['android-device-$deviceId'] = HealthCheckResult.error(e, s);
}
}
return results;
}
@override
Future<void> performPreflightTasks() async {
await exec(adbPath, <String>['kill-server'], canFail: false);
}
}
class AndroidDevice implements Device {
AndroidDevice({@required this.deviceId});
@override
final String deviceId;
@override
Future<bool> isAwake() async {
return await _getWakefulness() == 'Awake';
}
@override
Future<bool> isAsleep() async {
return await _getWakefulness() == 'Asleep';
}
@override
Future<void> wakeUp() async {
if (!(await isAwake()))
await togglePower();
}
@override
Future<void> sendToSleep() async {
if (!(await isAsleep()))
await togglePower();
}
@override
Future<void> togglePower() async {
await shellExec('input', const <String>['keyevent', '26']);
}
@override
Future<void> unlock() async {
await wakeUp();
await shellExec('input', const <String>['keyevent', '82']);
}
@override
Future<void> tap(int x, int y) async {
await shellExec('input', <String>['tap', '$x', '$y']);
}
Future<String> _getWakefulness() async {
final String powerInfo = await shellEval('dumpsys', <String>['power']);
final String wakefulness = grep('mWakefulness=', from: powerInfo).single.split('=')[1].trim();
return wakefulness;
}
Future<void> shellExec(String command, List<String> arguments, { Map<String, String> environment }) async {
await adb(<String>['shell', command, ...arguments], environment: environment);
}
Future<String> shellEval(String command, List<String> arguments, { Map<String, String> environment }) {
return adb(<String>['shell', command, ...arguments], environment: environment);
}
Future<String> adb(List<String> arguments, { Map<String, String> environment }) {
return eval(adbPath, <String>['-s', deviceId, ...arguments], environment: environment, canFail: false);
}
@override
Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
final String meminfo = await shellEval('dumpsys', <String>['meminfo', packageName]);
final Match match = RegExp(r'TOTAL\s+(\d+)').firstMatch(meminfo);
assert(match != null, 'could not parse dumpsys meminfo output');
return <String, dynamic>{
'total_kb': int.parse(match.group(1)),
};
}
@override
Stream<String> get logcat {
final Completer<void> stdoutDone = Completer<void>();
final Completer<void> stderrDone = Completer<void>();
final Completer<void> processDone = Completer<void>();
final Completer<void> abort = Completer<void>();
bool aborted = false;
StreamController<String> stream;
stream = StreamController<String>(
onListen: () async {
await adb(<String>['logcat', '--clear']);
final Process process = await startProcess(
adbPath,
<String>['-s', deviceId, 'logcat', 'ActivityManager:I', 'flutter:V', '*:F'],
);
process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('adb logcat: $line');
stream.sink.add(line);
}, onDone: () { stdoutDone.complete(); });
process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('adb logcat stderr: $line');
}, onDone: () { stderrDone.complete(); });
process.exitCode.then<void>((int exitCode) {
print('adb logcat process terminated with exit code $exitCode');
if (!aborted) {
stream.addError(BuildFailedError('adb logcat failed with exit code $exitCode.'));
processDone.complete();
}
});
await Future.any<dynamic>(<Future<dynamic>>[
Future.wait<void>(<Future<void>>[
stdoutDone.future,
stderrDone.future,
processDone.future,
]),
abort.future,
]);
aborted = true;
print('terminating adb logcat');
process.kill();
print('closing logcat stream');
await stream.close();
},
onCancel: () {
if (!aborted) {
print('adb logcat aborted');
aborted = true;
abort.complete();
}
},
);
return stream.stream;
}
@override
Future<void> stop(String packageName) async {
return shellExec('am', <String>['force-stop', packageName]);
}
}
class IosDeviceDiscovery implements DeviceDiscovery {
factory IosDeviceDiscovery() {
return _instance ??= IosDeviceDiscovery._();
}
IosDeviceDiscovery._();
static IosDeviceDiscovery _instance;
IosDevice _workingDevice;
@override
Future<IosDevice> get workingDevice async {
if (_workingDevice == null) {
await chooseWorkingDevice();
}
return _workingDevice;
}
@override
Future<void> chooseWorkingDevice() async {
final List<IosDevice> allDevices = (await discoverDevices())
.map<IosDevice>((String id) => IosDevice(deviceId: id))
.toList();
if (allDevices.isEmpty)
throw 'No iOS devices detected';
_workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
}
String get _artifactDirPath {
return path.normalize(
path.join(
path.current,
'../../bin/cache/artifacts',
)
);
}
Map<String, String> get _ideviceIdEnvironment {
final String libPath = const <String>[
'libimobiledevice',
'usbmuxd',
'libplist',
'openssl',
'ideviceinstaller',
'ios-deploy',
].map((String packageName) => path.join(_artifactDirPath, packageName)).join(':');
return <String, String>{'DYLD_LIBRARY_PATH': libPath};
}
@override
Future<List<String>> discoverDevices() async {
final String ideviceIdPath = path.join(_artifactDirPath, 'libimobiledevice', 'idevice_id');
final List<String> iosDeviceIDs = LineSplitter.split(await eval(ideviceIdPath, <String>['-l'], environment: _ideviceIdEnvironment))
.map<String>((String line) => line.trim())
.where((String line) => line.isNotEmpty)
.toList();
if (iosDeviceIDs.isEmpty)
throw 'No connected iOS devices found.';
return iosDeviceIDs;
}
@override
Future<Map<String, HealthCheckResult>> checkDevices() async {
final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
for (String deviceId in await discoverDevices()) {
results['ios-device-$deviceId'] = HealthCheckResult.success();
}
return results;
}
@override
Future<void> performPreflightTasks() async {
}
}
class IosDevice implements Device {
const IosDevice({ @required this.deviceId });
@override
final String deviceId;
@override
Future<bool> isAwake() async => true;
@override
Future<bool> isAsleep() async => false;
@override
Future<void> wakeUp() async {}
@override
Future<void> sendToSleep() async {}
@override
Future<void> togglePower() async {}
@override
Future<void> unlock() async {}
@override
Future<void> tap(int x, int y) async {
throw 'Not implemented';
}
@override
Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
throw 'Not implemented';
}
@override
Stream<String> get logcat {
throw 'Not implemented';
}
@override
Future<void> stop(String packageName) async {}
}
String get adbPath {
final String androidHome = Platform.environment['ANDROID_HOME'] ?? Platform.environment['ANDROID_SDK_ROOT'];
if (androidHome == null)
throw 'The ANDROID_SDK_ROOT and ANDROID_HOME environment variables are '
'missing. At least one of these variables must point to the Android '
'SDK directory containing platform-tools.';
final String adbPath = path.join(androidHome, 'platform-tools/adb');
if (!canRun(adbPath))
throw 'adb not found at: $adbPath';
return path.absolute(adbPath);
}