// 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 'package:retry/retry.dart'; import 'utils.dart'; const String DeviceIdEnvName = 'FLUTTER_DEVICELAB_DEVICEID'; class DeviceException implements Exception { const DeviceException(this.message); final String message; @override String toString() => '$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, fake, fuchsia, ios, linux, macos, windows, } /// 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.linux: return LinuxDeviceDiscovery(); case DeviceOperatingSystem.macos: return MacosDeviceDiscovery(); case DeviceOperatingSystem.windows: return WindowsDeviceDiscovery(); 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; /// Clears the device logs. /// /// This is important because benchmarks tests rely on the logs produced by /// the flutter run command. /// /// On Android, those logs may contain logs from previous test. Future<void> clearLogs(); /// 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); /// Wait for the device to become ready. Future<void> awaitDevice(); Future<void> uninstallApp() async { await flutter('install', options: <String>[ '--uninstall-only', '-d', deviceId]); await Future<void>.delayed(const Duration(seconds: 2)); await awaitDevice(); } @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 LinuxDeviceDiscovery implements DeviceDiscovery { factory LinuxDeviceDiscovery() { return _instance ??= LinuxDeviceDiscovery._(); } LinuxDeviceDiscovery._(); static LinuxDeviceDiscovery? _instance; static const LinuxDevice _device = LinuxDevice(); @override Future<Map<String, HealthCheckResult>> checkDevices() async { return <String, HealthCheckResult>{}; } @override Future<void> chooseWorkingDevice() async { } @override Future<void> chooseWorkingDeviceById(String deviceId) async { } @override Future<List<String>> discoverDevices() async { return <String>['linux']; } @override Future<void> performPreflightTasks() async { } @override Future<Device> get workingDevice async => _device; } class MacosDeviceDiscovery implements DeviceDiscovery { factory MacosDeviceDiscovery() { return _instance ??= MacosDeviceDiscovery._(); } MacosDeviceDiscovery._(); static MacosDeviceDiscovery? _instance; static const MacosDevice _device = MacosDevice(); @override Future<Map<String, HealthCheckResult>> checkDevices() async { return <String, HealthCheckResult>{}; } @override Future<void> chooseWorkingDevice() async { } @override Future<void> chooseWorkingDeviceById(String deviceId) async { } @override Future<List<String>> discoverDevices() async { return <String>['macos']; } @override Future<void> performPreflightTasks() async { } @override Future<Device> get workingDevice async => _device; } class WindowsDeviceDiscovery implements DeviceDiscovery { factory WindowsDeviceDiscovery() { return _instance ??= WindowsDeviceDiscovery._(); } WindowsDeviceDiscovery._(); static WindowsDeviceDiscovery? _instance; static const WindowsDevice _device = WindowsDevice(); @override Future<Map<String, HealthCheckResult>> checkDevices() async { return <String, HealthCheckResult>{}; } @override Future<void> chooseWorkingDevice() async { } @override Future<void> chooseWorkingDeviceById(String deviceId) async { } @override Future<List<String>> discoverDevices() async { return <String>['windows']; } @override Future<void> performPreflightTasks() async { } @override Future<Device> get workingDevice async => _device; } 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', '-f', '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', '-f', '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 = ''; int apiLevel = 0; /// 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']); // A motoG4 phone returns `mWakefulness=Awake`. // A Samsung phone returns `getWakefullnessLocked()=Awake`. final RegExp wakefulnessRegexp = RegExp(r'.*(mWakefulness=|getWakefulnessLocked\(\)=).*'); final String wakefulness = grep(wakefulnessRegexp, 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) { apiLevel = int.parse(list[2]); deviceInfo = 'fingerprint: ${list[0]} os: ${list[1]} api-level: $apiLevel'; } else { apiLevel = 0; deviceInfo = ''; } } /// Executes [command] on `adb shell`. 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, // Catch the whole log. <String>['-s', deviceId, 'logcat'], ); _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 Future<void> clearLogs() { return adb(<String>['logcat', '-c']); } @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 clearLogs(); 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']); } @override Future<void> awaitDevice() async { print('Waiting for device.'); final String waitOut = await adb(<String>['wait-for-device']); print(waitOut); const RetryOptions retryOptions = RetryOptions(delayFactor: Duration(seconds: 1), maxAttempts: 10, maxDelay: Duration(minutes: 1)); await retryOptions.retry(() async { final String adbShellOut = await adb(<String>['shell', 'getprop sys.boot_completed']); if (adbShellOut != '1') { print('Device not ready.'); print(adbShellOut); throw const DeviceException('Phone not ready.'); } }, retryIf: (Exception e) => e is DeviceException); print('Done waiting for device.'); } } 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> clearLogs() async {} @override Future<void> stop(String packageName) async {} @override Future<void> reboot() { return Process.run('idevicediagnostics', <String>['restart', '-u', deviceId]); } @override Future<void> awaitDevice() async {} } class LinuxDevice extends Device { const LinuxDevice(); @override String get deviceId => 'linux'; @override Future<Map<String, dynamic>> getMemoryStats(String packageName) async { return <String, dynamic>{}; } @override Future<void> home() async { } @override Future<bool> isAsleep() async { return false; } @override Future<bool> isAwake() async { return true; } @override Stream<String> get logcat => const Stream<String>.empty(); @override Future<void> clearLogs() async {} @override Future<void> reboot() async { } @override Future<void> sendToSleep() async { } @override Future<void> stop(String packageName) async { } @override Future<void> tap(int x, int y) async { } @override Future<void> togglePower() async { } @override Future<void> unlock() async { } @override Future<void> wakeUp() async { } @override Future<void> awaitDevice() async {} } class MacosDevice extends Device { const MacosDevice(); @override String get deviceId => 'macos'; @override Future<Map<String, dynamic>> getMemoryStats(String packageName) async { return <String, dynamic>{}; } @override Future<void> home() async { } @override Future<bool> isAsleep() async { return false; } @override Future<bool> isAwake() async { return true; } @override Stream<String> get logcat => const Stream<String>.empty(); @override Future<void> clearLogs() async {} @override Future<void> reboot() async { } @override Future<void> sendToSleep() async { } @override Future<void> stop(String packageName) async { } @override Future<void> tap(int x, int y) async { } @override Future<void> togglePower() async { } @override Future<void> unlock() async { } @override Future<void> wakeUp() async { } @override Future<void> awaitDevice() async {} } class WindowsDevice extends Device { const WindowsDevice(); @override String get deviceId => 'windows'; @override Future<Map<String, dynamic>> getMemoryStats(String packageName) async { return <String, dynamic>{}; } @override Future<void> home() async { } @override Future<bool> isAsleep() async { return false; } @override Future<bool> isAwake() async { return true; } @override Stream<String> get logcat => const Stream<String>.empty(); @override Future<void> clearLogs() async {} @override Future<void> reboot() async { } @override Future<void> sendToSleep() async { } @override Future<void> stop(String packageName) async { } @override Future<void> tap(int x, int y) async { } @override Future<void> togglePower() async { } @override Future<void> unlock() async { } @override Future<void> wakeUp() async { } @override Future<void> awaitDevice() async {} } /// 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> clearLogs() async {} @override Future<void> reboot() async { // Unsupported. } @override Future<void> awaitDevice() async {} } /// 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_HOME 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> clearLogs() async {} @override Future<void> stop(String packageName) async {} @override Future<void> reboot() async { // Unsupported. } @override Future<void> awaitDevice() async {} } 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 { } }