mirror of
https://github.com/flutter/flutter
synced 2024-10-03 06:54:10 +00:00
1125 lines
32 KiB
Dart
1125 lines
32 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:path/path.dart' as path;
|
|
|
|
import '../common.dart';
|
|
import 'utils.dart';
|
|
|
|
const String DeviceIdEnvName = 'FLUTTER_DEVICELAB_DEVICEID';
|
|
|
|
class DeviceException implements Exception {
|
|
const DeviceException(this.message);
|
|
|
|
final String message;
|
|
|
|
@override
|
|
String toString() => message == null ? '$DeviceException' : '$DeviceException: $message';
|
|
}
|
|
|
|
/// Gets the artifact path relative to the current directory.
|
|
String getArtifactPath() {
|
|
return path.normalize(
|
|
path.join(
|
|
path.current,
|
|
'../../bin/cache/artifacts',
|
|
)
|
|
);
|
|
}
|
|
|
|
/// Return the item is in idList if find a match, otherwise return null
|
|
String? _findMatchId(List<String> idList, String idPattern) {
|
|
String? candidate;
|
|
idPattern = idPattern.toLowerCase();
|
|
for(final String id in idList) {
|
|
if (id.toLowerCase() == idPattern) {
|
|
return id;
|
|
}
|
|
if (id.toLowerCase().startsWith(idPattern)) {
|
|
candidate ??= id;
|
|
}
|
|
}
|
|
return candidate;
|
|
}
|
|
|
|
/// The root of the API for controlling devices.
|
|
DeviceDiscovery get devices => DeviceDiscovery();
|
|
|
|
/// Device operating system the test is configured to test.
|
|
enum DeviceOperatingSystem { android, androidArm, androidArm64 ,ios, fuchsia, fake }
|
|
|
|
/// Device OS to test on.
|
|
DeviceOperatingSystem deviceOperatingSystem = DeviceOperatingSystem.android;
|
|
|
|
/// Discovers available devices and chooses one to work with.
|
|
abstract class DeviceDiscovery {
|
|
factory DeviceDiscovery() {
|
|
switch (deviceOperatingSystem) {
|
|
case DeviceOperatingSystem.android:
|
|
return AndroidDeviceDiscovery();
|
|
case DeviceOperatingSystem.androidArm:
|
|
return AndroidDeviceDiscovery(cpu: AndroidCPU.arm);
|
|
case DeviceOperatingSystem.androidArm64:
|
|
return AndroidDeviceDiscovery(cpu: AndroidCPU.arm64);
|
|
case DeviceOperatingSystem.ios:
|
|
return IosDeviceDiscovery();
|
|
case DeviceOperatingSystem.fuchsia:
|
|
return FuchsiaDeviceDiscovery();
|
|
case DeviceOperatingSystem.fake:
|
|
print('Looking for fake devices! You should not see this in release builds.');
|
|
return FakeDeviceDiscovery();
|
|
}
|
|
}
|
|
|
|
/// Selects a device to work with, load-balancing between devices if more than
|
|
/// one are available.
|
|
///
|
|
/// Calling this method does not guarantee that the same device will be
|
|
/// returned. For such behavior see [workingDevice].
|
|
Future<void> chooseWorkingDevice();
|
|
|
|
/// Selects a device to work with by device ID.
|
|
Future<void> chooseWorkingDeviceById(String deviceId);
|
|
|
|
/// A device to work with.
|
|
///
|
|
/// Returns the same device when called repeatedly (unlike
|
|
/// [chooseWorkingDevice]). This is useful when you need to perform multiple
|
|
/// operations on one.
|
|
Future<Device> get workingDevice;
|
|
|
|
/// Lists all available devices' IDs.
|
|
Future<List<String>> discoverDevices();
|
|
|
|
/// Checks the health of the available devices.
|
|
Future<Map<String, HealthCheckResult>> checkDevices();
|
|
|
|
/// Prepares the system to run tasks.
|
|
Future<void> performPreflightTasks();
|
|
}
|
|
|
|
/// A proxy for one specific device.
|
|
abstract class Device {
|
|
// Const constructor so subclasses may be const.
|
|
const Device();
|
|
|
|
/// A unique device identifier.
|
|
String get deviceId;
|
|
|
|
/// Whether the device is awake.
|
|
Future<bool> isAwake();
|
|
|
|
/// Whether the device is asleep.
|
|
Future<bool> isAsleep();
|
|
|
|
/// Wake up the device if it is not awake.
|
|
Future<void> wakeUp();
|
|
|
|
/// Send the device to sleep mode.
|
|
Future<void> sendToSleep();
|
|
|
|
/// Emulates pressing the home button.
|
|
Future<void> home();
|
|
|
|
/// Emulates pressing the power button, toggling the device's on/off state.
|
|
Future<void> togglePower();
|
|
|
|
/// Unlocks the device.
|
|
///
|
|
/// Assumes the device doesn't have a secure unlock pattern.
|
|
Future<void> unlock();
|
|
|
|
/// Attempt to reboot the phone, if possible.
|
|
Future<void> reboot();
|
|
|
|
/// Emulate a tap on the touch screen.
|
|
Future<void> tap(int x, int y);
|
|
|
|
/// Read memory statistics for a process.
|
|
Future<Map<String, dynamic>> getMemoryStats(String packageName);
|
|
|
|
/// Stream the system log from the device.
|
|
///
|
|
/// Flutter applications' `print` statements end up in this log
|
|
/// with some prefix.
|
|
Stream<String> get logcat;
|
|
|
|
/// Whether this device supports calls to [startLoggingToSink]
|
|
/// and [stopLoggingToSink].
|
|
bool get canStreamLogs => false;
|
|
|
|
/// Starts logging to an [IOSink].
|
|
///
|
|
/// If `clear` is set to true, the log will be cleared before starting. This
|
|
/// is not supported on all platforms.
|
|
Future<void> startLoggingToSink(IOSink sink, {bool clear = true}) {
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
/// Stops logging that was started by [startLoggingToSink].
|
|
Future<void> stopLoggingToSink() {
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
/// Stop a process.
|
|
Future<void> stop(String packageName);
|
|
|
|
@override
|
|
String toString() {
|
|
return 'device: $deviceId';
|
|
}
|
|
}
|
|
|
|
enum AndroidCPU {
|
|
arm,
|
|
arm64,
|
|
}
|
|
|
|
class AndroidDeviceDiscovery implements DeviceDiscovery {
|
|
factory AndroidDeviceDiscovery({AndroidCPU? cpu}) {
|
|
return _instance ??= AndroidDeviceDiscovery._(cpu);
|
|
}
|
|
|
|
AndroidDeviceDiscovery._(this.cpu);
|
|
|
|
final AndroidCPU? cpu;
|
|
|
|
// Parses information about a device. Example:
|
|
//
|
|
// 015d172c98400a03 device usb:340787200X product:nakasi model:Nexus_7 device:grouper
|
|
static final RegExp _kDeviceRegex = RegExp(r'^(\S+)\s+(\S+)(.*)');
|
|
|
|
static AndroidDeviceDiscovery? _instance;
|
|
|
|
AndroidDevice? _workingDevice;
|
|
|
|
@override
|
|
Future<AndroidDevice> get workingDevice async {
|
|
if (_workingDevice == null) {
|
|
if (Platform.environment.containsKey(DeviceIdEnvName)) {
|
|
final String deviceId = Platform.environment[DeviceIdEnvName]!;
|
|
await chooseWorkingDeviceById(deviceId);
|
|
return _workingDevice!;
|
|
}
|
|
await chooseWorkingDevice();
|
|
}
|
|
|
|
return _workingDevice!;
|
|
}
|
|
|
|
Future<bool> _matchesCPURequirement(AndroidDevice device) async {
|
|
switch (cpu) {
|
|
case null:
|
|
return true;
|
|
case AndroidCPU.arm64:
|
|
return device.isArm64();
|
|
case AndroidCPU.arm:
|
|
return device.isArm();
|
|
}
|
|
}
|
|
|
|
/// Picks a random Android device out of connected devices and sets it as
|
|
/// [workingDevice].
|
|
@override
|
|
Future<void> chooseWorkingDevice() async {
|
|
final List<AndroidDevice> allDevices = (await discoverDevices())
|
|
.map<AndroidDevice>((String id) => AndroidDevice(deviceId: id))
|
|
.toList();
|
|
|
|
if (allDevices.isEmpty)
|
|
throw const DeviceException('No Android devices detected');
|
|
|
|
if (cpu != null) {
|
|
for (final AndroidDevice device in allDevices) {
|
|
if (await _matchesCPURequirement(device)) {
|
|
_workingDevice = device;
|
|
break;
|
|
}
|
|
}
|
|
|
|
} else {
|
|
// TODO(yjbanov): filter out and warn about those with low battery level
|
|
_workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
|
|
}
|
|
|
|
if (_workingDevice == null)
|
|
throw const DeviceException('Cannot find a suitable Android device');
|
|
|
|
print('Device chosen: $_workingDevice');
|
|
}
|
|
|
|
@override
|
|
Future<void> chooseWorkingDeviceById(String deviceId) async {
|
|
final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
|
|
if (matchedId != null) {
|
|
_workingDevice = AndroidDevice(deviceId: matchedId);
|
|
if (cpu != null) {
|
|
if (!await _matchesCPURequirement(_workingDevice!)) {
|
|
throw DeviceException('The selected device $matchedId does not match the cpu requirement');
|
|
}
|
|
}
|
|
print('Choose device by ID: $matchedId');
|
|
return;
|
|
}
|
|
throw DeviceException(
|
|
'Device with ID $deviceId is not found for operating system: '
|
|
'$deviceOperatingSystem'
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<String>> discoverDevices() async {
|
|
final List<String> output = (await eval(adbPath, <String>['devices', '-l']))
|
|
.trim().split('\n');
|
|
final List<String> results = <String>[];
|
|
for (final String line in output) {
|
|
// Skip lines like: * daemon started successfully *
|
|
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 FormatException('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 (final String deviceId in await discoverDevices()) {
|
|
try {
|
|
final AndroidDevice device = AndroidDevice(deviceId: deviceId);
|
|
// Just a smoke test that we can read wakefulness state
|
|
// TODO(yjbanov): check battery level
|
|
await device._getWakefulness();
|
|
results['android-device-$deviceId'] = HealthCheckResult.success();
|
|
} on Exception catch (e, s) {
|
|
results['android-device-$deviceId'] = HealthCheckResult.error(e, s);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
@override
|
|
Future<void> performPreflightTasks() async {
|
|
// Kills the `adb` server causing it to start a new instance upon next
|
|
// command.
|
|
//
|
|
// Restarting `adb` helps with keeping device connections alive. When `adb`
|
|
// runs non-stop for too long it loses connections to devices. There may be
|
|
// a better method, but so far that's the best one I've found.
|
|
await exec(adbPath, <String>['kill-server']);
|
|
}
|
|
}
|
|
|
|
class FuchsiaDeviceDiscovery implements DeviceDiscovery {
|
|
factory FuchsiaDeviceDiscovery() {
|
|
return _instance ??= FuchsiaDeviceDiscovery._();
|
|
}
|
|
|
|
FuchsiaDeviceDiscovery._();
|
|
|
|
static FuchsiaDeviceDiscovery? _instance;
|
|
|
|
FuchsiaDevice? _workingDevice;
|
|
|
|
String get _ffx {
|
|
final String ffx = path.join(getArtifactPath(), 'fuchsia', 'tools','x64', 'ffx');
|
|
if (!File(ffx).existsSync()) {
|
|
throw FileSystemException("Couldn't find ffx at location $ffx");
|
|
}
|
|
return ffx;
|
|
}
|
|
|
|
@override
|
|
Future<FuchsiaDevice> get workingDevice async {
|
|
if (_workingDevice == null) {
|
|
if (Platform.environment.containsKey(DeviceIdEnvName)) {
|
|
final String deviceId = Platform.environment[DeviceIdEnvName]!;
|
|
await chooseWorkingDeviceById(deviceId);
|
|
return _workingDevice!;
|
|
}
|
|
await chooseWorkingDevice();
|
|
}
|
|
return _workingDevice!;
|
|
}
|
|
|
|
/// Picks the first connected Fuchsia device.
|
|
@override
|
|
Future<void> chooseWorkingDevice() async {
|
|
final List<FuchsiaDevice> allDevices = (await discoverDevices())
|
|
.map<FuchsiaDevice>((String id) => FuchsiaDevice(deviceId: id))
|
|
.toList();
|
|
|
|
if (allDevices.isEmpty) {
|
|
throw const DeviceException('No Fuchsia devices detected');
|
|
}
|
|
_workingDevice = allDevices.first;
|
|
print('Device chosen: $_workingDevice');
|
|
}
|
|
|
|
@override
|
|
Future<void> chooseWorkingDeviceById(String deviceId) async {
|
|
final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
|
|
if (matchedId != null) {
|
|
_workingDevice = FuchsiaDevice(deviceId: matchedId);
|
|
print('Choose device by ID: $matchedId');
|
|
return;
|
|
}
|
|
throw DeviceException(
|
|
'Device with ID $deviceId is not found for operating system: '
|
|
'$deviceOperatingSystem'
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<String>> discoverDevices() async {
|
|
final List<String> output = (await eval(_ffx, <String>['target', 'list', '--format', 's']))
|
|
.trim()
|
|
.split('\n');
|
|
|
|
final List<String> devices = <String>[];
|
|
for (final String line in output) {
|
|
final List<String> parts = line.split(' ');
|
|
assert(parts.length == 2);
|
|
devices.add(parts.last); // The device id.
|
|
}
|
|
return devices;
|
|
}
|
|
|
|
@override
|
|
Future<Map<String, HealthCheckResult>> checkDevices() async {
|
|
final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
|
|
for (final String deviceId in await discoverDevices()) {
|
|
try {
|
|
final int resolveResult = await exec(
|
|
_ffx,
|
|
<String>[
|
|
'target',
|
|
'list',
|
|
'--format',
|
|
'a',
|
|
deviceId,
|
|
]
|
|
);
|
|
if (resolveResult == 0) {
|
|
results['fuchsia-device-$deviceId'] = HealthCheckResult.success();
|
|
} else {
|
|
results['fuchsia-device-$deviceId'] = HealthCheckResult.failure('Cannot resolve device $deviceId');
|
|
}
|
|
} on Exception catch (error, stacktrace) {
|
|
results['fuchsia-device-$deviceId'] = HealthCheckResult.error(error, stacktrace);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
@override
|
|
Future<void> performPreflightTasks() async {}
|
|
}
|
|
|
|
class AndroidDevice extends Device {
|
|
AndroidDevice({required this.deviceId}) {
|
|
_updateDeviceInfo();
|
|
}
|
|
|
|
@override
|
|
final String deviceId;
|
|
String deviceInfo = '';
|
|
|
|
/// Whether the device is awake.
|
|
@override
|
|
Future<bool> isAwake() async {
|
|
return await _getWakefulness() == 'Awake';
|
|
}
|
|
|
|
/// Whether the device is asleep.
|
|
@override
|
|
Future<bool> isAsleep() async {
|
|
return await _getWakefulness() == 'Asleep';
|
|
}
|
|
|
|
/// Wake up the device if it is not awake using [togglePower].
|
|
@override
|
|
Future<void> wakeUp() async {
|
|
if (!(await isAwake()))
|
|
await togglePower();
|
|
}
|
|
|
|
/// Send the device to sleep mode if it is not asleep using [togglePower].
|
|
@override
|
|
Future<void> sendToSleep() async {
|
|
if (!(await isAsleep()))
|
|
await togglePower();
|
|
}
|
|
|
|
/// Sends `KEYCODE_HOME` (3), which causes the device to go to the home screen.
|
|
@override
|
|
Future<void> home() async {
|
|
await shellExec('input', const <String>['keyevent', '3']);
|
|
}
|
|
|
|
/// Sends `KEYCODE_POWER` (26), which causes the device to toggle its mode
|
|
/// between awake and asleep.
|
|
@override
|
|
Future<void> togglePower() async {
|
|
await shellExec('input', const <String>['keyevent', '26']);
|
|
}
|
|
|
|
/// Unlocks the device by sending `KEYCODE_MENU` (82).
|
|
///
|
|
/// This only works when the device doesn't have a secure unlock pattern.
|
|
@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']);
|
|
}
|
|
|
|
/// Retrieves device's wakefulness state.
|
|
///
|
|
/// See: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/PowerManagerInternal.java
|
|
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<bool> isArm64() async {
|
|
final String cpuInfo = await shellEval('getprop', const <String>['ro.product.cpu.abi']);
|
|
return cpuInfo.contains('arm64');
|
|
}
|
|
|
|
Future<bool> isArm() async {
|
|
final String cpuInfo = await shellEval('getprop', const <String>['ro.product.cpu.abi']);
|
|
return cpuInfo.contains('armeabi');
|
|
}
|
|
|
|
Future<void> _updateDeviceInfo() async {
|
|
String info;
|
|
try {
|
|
info = await shellEval(
|
|
'getprop',
|
|
<String>[
|
|
'ro.bootimage.build.fingerprint', ';',
|
|
'getprop', 'ro.build.version.release', ';',
|
|
'getprop', 'ro.build.version.sdk',
|
|
],
|
|
silent: true,
|
|
);
|
|
} on IOException {
|
|
info = '';
|
|
}
|
|
final List<String> list = info.split('\n');
|
|
if (list.length == 3) {
|
|
deviceInfo = 'fingerprint: ${list[0]} os: ${list[1]} api-level: ${list[2]}';
|
|
} else {
|
|
deviceInfo = '';
|
|
}
|
|
}
|
|
|
|
/// Executes [command] on `adb shell` and returns its exit code.
|
|
Future<void> shellExec(String command, List<String> arguments, { Map<String, String>? environment, bool silent = false }) async {
|
|
await adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
|
|
}
|
|
|
|
/// Executes [command] on `adb shell` and returns its standard output as a [String].
|
|
Future<String> shellEval(String command, List<String> arguments, { Map<String, String>? environment, bool silent = false }) {
|
|
return adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
|
|
}
|
|
|
|
/// Runs `adb` with the given [arguments], selecting this device.
|
|
Future<String> adb(
|
|
List<String> arguments, {
|
|
Map<String, String>? environment,
|
|
bool silent = false,
|
|
}) {
|
|
return eval(
|
|
adbPath,
|
|
<String>['-s', deviceId, ...arguments],
|
|
environment: environment,
|
|
printStdout: !silent,
|
|
printStderr: !silent,
|
|
);
|
|
}
|
|
|
|
@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
|
|
bool get canStreamLogs => true;
|
|
|
|
bool _abortedLogging = false;
|
|
Process? _loggingProcess;
|
|
|
|
@override
|
|
Future<void> startLoggingToSink(IOSink sink, {bool clear = true}) async {
|
|
if (clear) {
|
|
await adb(<String>['logcat', '--clear'], silent: true);
|
|
}
|
|
_loggingProcess = await startProcess(
|
|
adbPath,
|
|
// Make logcat less chatty by filtering down to just ActivityManager
|
|
// (to let us know when app starts), flutter (needed by tests to see
|
|
// log output), and fatal messages (hopefully catches tombstones).
|
|
// For local testing, this can just be:
|
|
// <String>['-s', deviceId, 'logcat']
|
|
// to view the whole log, or just run logcat alongside this.
|
|
<String>['-s', deviceId, 'logcat', 'ActivityManager:I', 'flutter:V', '*:F'],
|
|
);
|
|
_loggingProcess!.stdout
|
|
.transform<String>(const Utf8Decoder(allowMalformed: true))
|
|
.listen((String line) {
|
|
sink.write(line);
|
|
});
|
|
_loggingProcess!.stderr
|
|
.transform<String>(const Utf8Decoder(allowMalformed: true))
|
|
.listen((String line) {
|
|
sink.write(line);
|
|
});
|
|
unawaited(_loggingProcess!.exitCode.then<void>((int exitCode) {
|
|
if (!_abortedLogging) {
|
|
sink.writeln('adb logcat failed with exit code $exitCode.\n');
|
|
}
|
|
}));
|
|
}
|
|
|
|
@override
|
|
Future<void> stopLoggingToSink() async {
|
|
if (_loggingProcess != null) {
|
|
_abortedLogging = true;
|
|
_loggingProcess!.kill();
|
|
await _loggingProcess!.exitCode;
|
|
}
|
|
}
|
|
|
|
@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;
|
|
late final StreamController<String> stream;
|
|
stream = StreamController<String>(
|
|
onListen: () async {
|
|
await adb(<String>['logcat', '--clear']);
|
|
final Process process = await startProcess(
|
|
adbPath,
|
|
// Make logcat less chatty by filtering down to just ActivityManager
|
|
// (to let us know when app starts), flutter (needed by tests to see
|
|
// log output), and fatal messages (hopefully catches tombstones).
|
|
// For local testing, this can just be:
|
|
// <String>['-s', deviceId, 'logcat']
|
|
// to view the whole log, or just run logcat alongside this.
|
|
<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');
|
|
if (!stream.isClosed) {
|
|
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(); });
|
|
unawaited(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.\n'));
|
|
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]);
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return '$deviceId $deviceInfo';
|
|
}
|
|
|
|
@override
|
|
Future<void> reboot() {
|
|
return adb(<String>['reboot']);
|
|
}
|
|
}
|
|
|
|
class IosDeviceDiscovery implements DeviceDiscovery {
|
|
factory IosDeviceDiscovery() {
|
|
return _instance ??= IosDeviceDiscovery._();
|
|
}
|
|
|
|
IosDeviceDiscovery._();
|
|
|
|
static IosDeviceDiscovery? _instance;
|
|
|
|
IosDevice? _workingDevice;
|
|
|
|
@override
|
|
Future<IosDevice> get workingDevice async {
|
|
if (_workingDevice == null) {
|
|
if (Platform.environment.containsKey(DeviceIdEnvName)) {
|
|
final String deviceId = Platform.environment[DeviceIdEnvName]!;
|
|
await chooseWorkingDeviceById(deviceId);
|
|
return _workingDevice!;
|
|
}
|
|
await chooseWorkingDevice();
|
|
}
|
|
|
|
return _workingDevice!;
|
|
}
|
|
|
|
/// Picks a random iOS device out of connected devices and sets it as
|
|
/// [workingDevice].
|
|
@override
|
|
Future<void> chooseWorkingDevice() async {
|
|
final List<IosDevice> allDevices = (await discoverDevices())
|
|
.map<IosDevice>((String id) => IosDevice(deviceId: id))
|
|
.toList();
|
|
|
|
if (allDevices.isEmpty)
|
|
throw const DeviceException('No iOS devices detected');
|
|
|
|
// TODO(yjbanov): filter out and warn about those with low battery level
|
|
_workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
|
|
print('Device chosen: $_workingDevice');
|
|
}
|
|
|
|
@override
|
|
Future<void> chooseWorkingDeviceById(String deviceId) async {
|
|
final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
|
|
if (matchedId != null) {
|
|
_workingDevice = IosDevice(deviceId: matchedId);
|
|
print('Choose device by ID: $matchedId');
|
|
return;
|
|
}
|
|
throw DeviceException(
|
|
'Device with ID $deviceId is not found for operating system: '
|
|
'$deviceOperatingSystem'
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<String>> discoverDevices() async {
|
|
final List<dynamic> results = json.decode(await eval(
|
|
path.join(flutterDirectory.path, 'bin', 'flutter'),
|
|
<String>['devices', '--machine', '--suppress-analytics', '--device-timeout', '5'],
|
|
)) as List<dynamic>;
|
|
|
|
// [
|
|
// {
|
|
// "name": "Flutter's iPhone",
|
|
// "id": "00008020-00017DA80CC1002E",
|
|
// "isSupported": true,
|
|
// "targetPlatform": "ios",
|
|
// "emulator": false,
|
|
// "sdk": "iOS 13.2",
|
|
// "capabilities": {
|
|
// "hotReload": true,
|
|
// "hotRestart": true,
|
|
// "screenshot": true,
|
|
// "fastStart": false,
|
|
// "flutterExit": true,
|
|
// "hardwareRendering": false,
|
|
// "startPaused": false
|
|
// }
|
|
// }
|
|
// ]
|
|
|
|
final List<String> deviceIds = <String>[];
|
|
|
|
for (final dynamic result in results) {
|
|
final Map<String, dynamic> device = result as Map<String, dynamic>;
|
|
if (device['targetPlatform'] == 'ios' &&
|
|
device['id'] != null &&
|
|
device['emulator'] != true &&
|
|
device['isSupported'] == true) {
|
|
deviceIds.add(device['id'] as String);
|
|
}
|
|
}
|
|
|
|
if (deviceIds.isEmpty) {
|
|
throw const DeviceException('No connected physical iOS devices found.');
|
|
}
|
|
return deviceIds;
|
|
}
|
|
|
|
@override
|
|
Future<Map<String, HealthCheckResult>> checkDevices() async {
|
|
final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
|
|
for (final String deviceId in await discoverDevices()) {
|
|
// TODO(ianh): do a more meaningful connectivity check than just recording the ID
|
|
results['ios-device-$deviceId'] = HealthCheckResult.success();
|
|
}
|
|
return results;
|
|
}
|
|
|
|
@override
|
|
Future<void> performPreflightTasks() async {
|
|
// Currently we do not have preflight tasks for iOS.
|
|
}
|
|
}
|
|
|
|
/// iOS device.
|
|
class IosDevice extends Device {
|
|
IosDevice({ required this.deviceId });
|
|
|
|
@override
|
|
final String deviceId;
|
|
|
|
String get idevicesyslogPath {
|
|
return path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libimobiledevice', 'idevicesyslog');
|
|
}
|
|
|
|
String get dyldLibraryPath {
|
|
final List<String> dylibsPaths = <String>[
|
|
path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libimobiledevice'),
|
|
path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'openssl'),
|
|
path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'usbmuxd'),
|
|
path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libplist'),
|
|
];
|
|
return dylibsPaths.join(':');
|
|
}
|
|
|
|
@override
|
|
bool get canStreamLogs => true;
|
|
|
|
bool _abortedLogging = false;
|
|
Process? _loggingProcess;
|
|
|
|
@override
|
|
Future<void> startLoggingToSink(IOSink sink, {bool clear = true}) async {
|
|
// Clear is not supported.
|
|
_loggingProcess = await startProcess(
|
|
idevicesyslogPath,
|
|
<String>['-u', deviceId, '--quiet'],
|
|
environment: <String, String>{
|
|
'DYLD_LIBRARY_PATH': dyldLibraryPath,
|
|
},
|
|
);
|
|
_loggingProcess!.stdout
|
|
.transform<String>(const Utf8Decoder(allowMalformed: true))
|
|
.listen((String line) {
|
|
sink.write(line);
|
|
});
|
|
_loggingProcess!.stderr
|
|
.transform<String>(const Utf8Decoder(allowMalformed: true))
|
|
.listen((String line) {
|
|
sink.write(line);
|
|
});
|
|
unawaited(_loggingProcess!.exitCode.then<void>((int exitCode) {
|
|
if (!_abortedLogging) {
|
|
sink.writeln('idevicesyslog failed with exit code $exitCode.\n');
|
|
}
|
|
}));
|
|
}
|
|
|
|
@override
|
|
Future<void> stopLoggingToSink() async {
|
|
if (_loggingProcess != null) {
|
|
_abortedLogging = true;
|
|
_loggingProcess!.kill();
|
|
await _loggingProcess!.exitCode;
|
|
}
|
|
}
|
|
|
|
// The methods below are stubs for now. They will need to be expanded.
|
|
// We currently do not have a way to lock/unlock iOS devices. So we assume the
|
|
// devices are already unlocked. For now we'll just keep them at minimum
|
|
// screen brightness so they don't drain battery too fast.
|
|
|
|
@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> home() async {}
|
|
|
|
@override
|
|
Future<void> togglePower() async {}
|
|
|
|
@override
|
|
Future<void> unlock() async {}
|
|
|
|
@override
|
|
Future<void> tap(int x, int y) async {
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
@override
|
|
Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
@override
|
|
Stream<String> get logcat {
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
@override
|
|
Future<void> stop(String packageName) async {}
|
|
|
|
@override
|
|
Future<void> reboot() {
|
|
return Process.run('idevicediagnostics', <String>['restart', '-u', deviceId]);
|
|
}
|
|
}
|
|
|
|
/// Fuchsia device.
|
|
class FuchsiaDevice extends Device {
|
|
const FuchsiaDevice({ required this.deviceId });
|
|
|
|
@override
|
|
final String deviceId;
|
|
|
|
// TODO(egarciad): Implement these for Fuchsia.
|
|
@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> home() async {}
|
|
|
|
@override
|
|
Future<void> togglePower() async {}
|
|
|
|
@override
|
|
Future<void> unlock() async {}
|
|
|
|
@override
|
|
Future<void> tap(int x, int y) async {}
|
|
|
|
@override
|
|
Future<void> stop(String packageName) async {}
|
|
|
|
@override
|
|
Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
@override
|
|
Stream<String> get logcat {
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
@override
|
|
Future<void> reboot() async {
|
|
// Unsupported.
|
|
}
|
|
}
|
|
|
|
/// Path to the `adb` executable.
|
|
String get adbPath {
|
|
final String? androidHome = Platform.environment['ANDROID_HOME'] ?? Platform.environment['ANDROID_SDK_ROOT'];
|
|
|
|
if (androidHome == null) {
|
|
throw const DeviceException(
|
|
'The ANDROID_SDK_ROOT environment variable is '
|
|
'missing. The variable must point to the Android '
|
|
'SDK directory containing platform-tools.'
|
|
);
|
|
}
|
|
|
|
final String adbPath = path.join(androidHome, 'platform-tools/adb');
|
|
|
|
if (!canRun(adbPath))
|
|
throw DeviceException('adb not found at: $adbPath');
|
|
|
|
return path.absolute(adbPath);
|
|
}
|
|
|
|
class FakeDevice extends Device {
|
|
const FakeDevice({ 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> home() async {}
|
|
|
|
@override
|
|
Future<void> togglePower() async {}
|
|
|
|
@override
|
|
Future<void> unlock() async {}
|
|
|
|
@override
|
|
Future<void> tap(int x, int y) async {
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
@override
|
|
Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
@override
|
|
Stream<String> get logcat {
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
@override
|
|
Future<void> stop(String packageName) async {}
|
|
|
|
@override
|
|
Future<void> reboot() async {
|
|
// Unsupported.
|
|
}
|
|
}
|
|
|
|
class FakeDeviceDiscovery implements DeviceDiscovery {
|
|
factory FakeDeviceDiscovery() {
|
|
return _instance ??= FakeDeviceDiscovery._();
|
|
}
|
|
|
|
FakeDeviceDiscovery._();
|
|
|
|
static FakeDeviceDiscovery? _instance;
|
|
|
|
FakeDevice? _workingDevice;
|
|
|
|
@override
|
|
Future<FakeDevice> get workingDevice async {
|
|
if (_workingDevice == null) {
|
|
if (Platform.environment.containsKey(DeviceIdEnvName)) {
|
|
final String deviceId = Platform.environment[DeviceIdEnvName]!;
|
|
await chooseWorkingDeviceById(deviceId);
|
|
return _workingDevice!;
|
|
}
|
|
await chooseWorkingDevice();
|
|
}
|
|
|
|
return _workingDevice!;
|
|
}
|
|
|
|
/// The Fake is only available for by ID device discovery.
|
|
@override
|
|
Future<void> chooseWorkingDevice() async {
|
|
throw const DeviceException('No fake devices detected');
|
|
}
|
|
|
|
@override
|
|
Future<void> chooseWorkingDeviceById(String deviceId) async {
|
|
final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
|
|
if (matchedId != null) {
|
|
_workingDevice = FakeDevice(deviceId: matchedId);
|
|
print('Choose device by ID: $matchedId');
|
|
return;
|
|
}
|
|
throw DeviceException(
|
|
'Device with ID $deviceId is not found for operating system: '
|
|
'$deviceOperatingSystem'
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<String>> discoverDevices() async {
|
|
return <String>['FAKE_SUCCESS', 'THIS_IS_A_FAKE'];
|
|
}
|
|
|
|
@override
|
|
Future<Map<String, HealthCheckResult>> checkDevices() async {
|
|
final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
|
|
for (final String deviceId in await discoverDevices()) {
|
|
results['fake-device-$deviceId'] = HealthCheckResult.success();
|
|
}
|
|
return results;
|
|
}
|
|
|
|
@override
|
|
Future<void> performPreflightTasks() async {
|
|
}
|
|
}
|