1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:convert';
7import 'dart:io';
8import 'dart:math' as math;
9
10import 'package:path/path.dart' as path;
11import 'package:retry/retry.dart';
12
13import 'utils.dart';
14
15const String DeviceIdEnvName = 'FLUTTER_DEVICELAB_DEVICEID';
16
17class DeviceException implements Exception {
18 const DeviceException(this.message);
19
20 final String message;
21
22 @override
23 String toString() => '$DeviceException: $message';
24}
25
26/// Gets the artifact path relative to the current directory.
27String getArtifactPath() {
28 return path.normalize(path.join(path.current, '../../bin/cache/artifacts'));
29}
30
31/// Return the item is in idList if find a match, otherwise return null
32String? _findMatchId(List<String> idList, String idPattern) {
33 String? candidate;
34 idPattern = idPattern.toLowerCase();
35 for (final String id in idList) {
36 if (id.toLowerCase() == idPattern) {
37 return id;
38 }
39 if (id.toLowerCase().startsWith(idPattern)) {
40 candidate ??= id;
41 }
42 }
43 return candidate;
44}
45
46/// The root of the API for controlling devices.
47DeviceDiscovery get devices => DeviceDiscovery();
48
49/// Device operating system the test is configured to test.
50enum DeviceOperatingSystem {
51 android,
52 androidArm,
53 androidArm64,
54 fake,
55 fuchsia,
56 ios,
57 linux,
58 macos,
59 windows,
60}
61
62/// Device OS to test on.
63DeviceOperatingSystem deviceOperatingSystem = DeviceOperatingSystem.android;
64
65/// Discovers available devices and chooses one to work with.
66abstract class DeviceDiscovery {
67 factory DeviceDiscovery() {
68 switch (deviceOperatingSystem) {
69 case DeviceOperatingSystem.android:
70 return AndroidDeviceDiscovery();
71 case DeviceOperatingSystem.androidArm:
72 return AndroidDeviceDiscovery(cpu: AndroidCPU.arm);
73 case DeviceOperatingSystem.androidArm64:
74 return AndroidDeviceDiscovery(cpu: AndroidCPU.arm64);
75 case DeviceOperatingSystem.ios:
76 return IosDeviceDiscovery();
77 case DeviceOperatingSystem.fuchsia:
78 return FuchsiaDeviceDiscovery();
79 case DeviceOperatingSystem.linux:
80 return LinuxDeviceDiscovery();
81 case DeviceOperatingSystem.macos:
82 return MacosDeviceDiscovery();
83 case DeviceOperatingSystem.windows:
84 return WindowsDeviceDiscovery();
85 case DeviceOperatingSystem.fake:
86 print('Looking for fake devices! You should not see this in release builds.');
87 return FakeDeviceDiscovery();
88 }
89 }
90
91 /// Selects a device to work with, load-balancing between devices if more than
92 /// one are available.
93 ///
94 /// Calling this method does not guarantee that the same device will be
95 /// returned. For such behavior see [workingDevice].
96 Future<void> chooseWorkingDevice();
97
98 /// Selects a device to work with by device ID.
99 Future<void> chooseWorkingDeviceById(String deviceId);
100
101 /// A device to work with.
102 ///
103 /// Returns the same device when called repeatedly (unlike
104 /// [chooseWorkingDevice]). This is useful when you need to perform multiple
105 /// operations on one.
106 Future<Device> get workingDevice;
107
108 /// Lists all available devices' IDs.
109 Future<List<String>> discoverDevices();
110
111 /// Checks the health of the available devices.
112 Future<Map<String, HealthCheckResult>> checkDevices();
113
114 /// Prepares the system to run tasks.
115 Future<void> performPreflightTasks();
116}
117
118/// A proxy for one specific device.
119abstract class Device {
120 // Const constructor so subclasses may be const.
121 const Device();
122
123 /// A unique device identifier.
124 String get deviceId;
125
126 /// Switch the device into fixed/regular performance mode.
127 Future<void> toggleFixedPerformanceMode(bool enable) async {}
128
129 /// Whether the device is awake.
130 Future<bool> isAwake();
131
132 /// Whether the device is asleep.
133 Future<bool> isAsleep();
134
135 /// Wake up the device if it is not awake.
136 Future<void> wakeUp();
137
138 /// Send the device to sleep mode.
139 Future<void> sendToSleep();
140
141 /// Emulates pressing the home button.
142 Future<void> home();
143
144 /// Emulates pressing the power button, toggling the device's on/off state.
145 Future<void> togglePower();
146
147 /// Unlocks the device.
148 ///
149 /// Assumes the device doesn't have a secure unlock pattern.
150 Future<void> unlock();
151
152 /// Attempt to reboot the phone, if possible.
153 Future<void> reboot();
154
155 /// Emulate a tap on the touch screen.
156 Future<void> tap(int x, int y);
157
158 /// Read memory statistics for a process.
159 Future<Map<String, dynamic>> getMemoryStats(String packageName);
160
161 /// Stream the system log from the device.
162 ///
163 /// Flutter applications' `print` statements end up in this log
164 /// with some prefix.
165 Stream<String> get logcat;
166
167 /// Clears the device logs.
168 ///
169 /// This is important because benchmarks tests rely on the logs produced by
170 /// the flutter run command.
171 ///
172 /// On Android, those logs may contain logs from previous test.
173 Future<void> clearLogs();
174
175 /// Whether this device supports calls to [startLoggingToSink]
176 /// and [stopLoggingToSink].
177 bool get canStreamLogs => false;
178
179 /// Starts logging to an [IOSink].
180 ///
181 /// If `clear` is set to true, the log will be cleared before starting. This
182 /// is not supported on all platforms.
183 Future<void> startLoggingToSink(IOSink sink, {bool clear = true}) {
184 throw UnimplementedError();
185 }
186
187 /// Stops logging that was started by [startLoggingToSink].
188 Future<void> stopLoggingToSink() {
189 throw UnimplementedError();
190 }
191
192 /// Stop a process.
193 Future<void> stop(String packageName);
194
195 /// Wait for the device to become ready.
196 Future<void> awaitDevice();
197
198 Future<void> uninstallApp() async {
199 await flutter('install', options: <String>['--uninstall-only', '-d', deviceId]);
200
201 await Future<void>.delayed(const Duration(seconds: 2));
202
203 await awaitDevice();
204 }
205
206 @override
207 String toString() {
208 return 'device: $deviceId';
209 }
210}
211
212enum AndroidCPU { arm, arm64 }
213
214class AndroidDeviceDiscovery implements DeviceDiscovery {
215 factory AndroidDeviceDiscovery({AndroidCPU? cpu}) {
216 return _instance ??= AndroidDeviceDiscovery._(cpu);
217 }
218
219 AndroidDeviceDiscovery._(this.cpu);
220
221 final AndroidCPU? cpu;
222
223 // Parses information about a device. Example:
224 //
225 // 015d172c98400a03 device usb:340787200X product:nakasi model:Nexus_7 device:grouper
226 static final RegExp _kDeviceRegex = RegExp(r'^(\S+)\s+(\S+)(.*)');
227
228 static AndroidDeviceDiscovery? _instance;
229
230 AndroidDevice? _workingDevice;
231
232 @override
233 Future<AndroidDevice> get workingDevice async {
234 if (_workingDevice == null) {
235 if (Platform.environment.containsKey(DeviceIdEnvName)) {
236 final String deviceId = Platform.environment[DeviceIdEnvName]!;
237 await chooseWorkingDeviceById(deviceId);
238 return _workingDevice!;
239 }
240 await chooseWorkingDevice();
241 }
242
243 return _workingDevice!;
244 }
245
246 Future<bool> _matchesCPURequirement(AndroidDevice device) async {
247 return switch (cpu) {
248 null => Future<bool>.value(true),
249 AndroidCPU.arm64 => device.isArm64(),
250 AndroidCPU.arm => device.isArm(),
251 };
252 }
253
254 /// Picks a random Android device out of connected devices and sets it as
255 /// [workingDevice].
256 @override
257 Future<void> chooseWorkingDevice() async {
258 final List<AndroidDevice> allDevices = (await discoverDevices())
259 .map<AndroidDevice>((String id) => AndroidDevice(deviceId: id))
260 .toList();
261
262 if (allDevices.isEmpty) {
263 throw const DeviceException('No Android devices detected');
264 }
265
266 if (cpu != null) {
267 for (final AndroidDevice device in allDevices) {
268 if (await _matchesCPURequirement(device)) {
269 _workingDevice = device;
270 break;
271 }
272 }
273 } else {
274 // TODO(yjbanov): filter out and warn about those with low battery level
275 _workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
276 }
277
278 if (_workingDevice == null) {
279 throw const DeviceException('Cannot find a suitable Android device');
280 }
281
282 print('Device chosen: $_workingDevice');
283 }
284
285 @override
286 Future<void> chooseWorkingDeviceById(String deviceId) async {
287 final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
288 if (matchedId != null) {
289 _workingDevice = AndroidDevice(deviceId: matchedId);
290 if (cpu != null) {
291 if (!await _matchesCPURequirement(_workingDevice!)) {
292 throw DeviceException(
293 'The selected device $matchedId does not match the cpu requirement',
294 );
295 }
296 }
297 print('Choose device by ID: $matchedId');
298 return;
299 }
300 throw DeviceException(
301 'Device with ID $deviceId is not found for operating system: '
302 '$deviceOperatingSystem',
303 );
304 }
305
306 @override
307 Future<List<String>> discoverDevices() async {
308 final List<String> output = (await eval(adbPath, <String>['devices', '-l'])).trim().split('\n');
309 final List<String> results = <String>[];
310 for (final String line in output) {
311 // Skip lines like: * daemon started successfully *
312 if (line.startsWith('* daemon ')) {
313 continue;
314 }
315
316 if (line.startsWith('List of devices')) {
317 continue;
318 }
319
320 if (_kDeviceRegex.hasMatch(line)) {
321 final Match match = _kDeviceRegex.firstMatch(line)!;
322
323 final String deviceID = match[1]!;
324 final String deviceState = match[2]!;
325
326 if (!const <String>['unauthorized', 'offline'].contains(deviceState)) {
327 results.add(deviceID);
328 }
329 } else {
330 throw FormatException('Failed to parse device from adb output: "$line"');
331 }
332 }
333
334 return results;
335 }
336
337 @override
338 Future<Map<String, HealthCheckResult>> checkDevices() async {
339 final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
340 for (final String deviceId in await discoverDevices()) {
341 try {
342 final AndroidDevice device = AndroidDevice(deviceId: deviceId);
343 // Just a smoke test that we can read wakefulness state
344 // TODO(yjbanov): check battery level
345 await device._getWakefulness();
346 results['android-device-$deviceId'] = HealthCheckResult.success();
347 } on Exception catch (e, s) {
348 results['android-device-$deviceId'] = HealthCheckResult.error(e, s);
349 }
350 }
351 return results;
352 }
353
354 @override
355 Future<void> performPreflightTasks() async {
356 // Kills the `adb` server causing it to start a new instance upon next
357 // command.
358 //
359 // Restarting `adb` helps with keeping device connections alive. When `adb`
360 // runs non-stop for too long it loses connections to devices. There may be
361 // a better method, but so far that's the best one I've found.
362 await exec(adbPath, <String>['kill-server']);
363 }
364}
365
366class LinuxDeviceDiscovery implements DeviceDiscovery {
367 factory LinuxDeviceDiscovery() {
368 return _instance ??= LinuxDeviceDiscovery._();
369 }
370
371 LinuxDeviceDiscovery._();
372
373 static LinuxDeviceDiscovery? _instance;
374
375 static const LinuxDevice _device = LinuxDevice();
376
377 @override
378 Future<Map<String, HealthCheckResult>> checkDevices() async {
379 return <String, HealthCheckResult>{};
380 }
381
382 @override
383 Future<void> chooseWorkingDevice() async {}
384
385 @override
386 Future<void> chooseWorkingDeviceById(String deviceId) async {}
387
388 @override
389 Future<List<String>> discoverDevices() async {
390 return <String>['linux'];
391 }
392
393 @override
394 Future<void> performPreflightTasks() async {}
395
396 @override
397 Future<Device> get workingDevice async => _device;
398}
399
400class MacosDeviceDiscovery implements DeviceDiscovery {
401 factory MacosDeviceDiscovery() {
402 return _instance ??= MacosDeviceDiscovery._();
403 }
404
405 MacosDeviceDiscovery._();
406
407 static MacosDeviceDiscovery? _instance;
408
409 static const MacosDevice _device = MacosDevice();
410
411 @override
412 Future<Map<String, HealthCheckResult>> checkDevices() async {
413 return <String, HealthCheckResult>{};
414 }
415
416 @override
417 Future<void> chooseWorkingDevice() async {}
418
419 @override
420 Future<void> chooseWorkingDeviceById(String deviceId) async {}
421
422 @override
423 Future<List<String>> discoverDevices() async {
424 return <String>['macos'];
425 }
426
427 @override
428 Future<void> performPreflightTasks() async {}
429
430 @override
431 Future<Device> get workingDevice async => _device;
432}
433
434class WindowsDeviceDiscovery implements DeviceDiscovery {
435 factory WindowsDeviceDiscovery() {
436 return _instance ??= WindowsDeviceDiscovery._();
437 }
438
439 WindowsDeviceDiscovery._();
440
441 static WindowsDeviceDiscovery? _instance;
442
443 static const WindowsDevice _device = WindowsDevice();
444
445 @override
446 Future<Map<String, HealthCheckResult>> checkDevices() async {
447 return <String, HealthCheckResult>{};
448 }
449
450 @override
451 Future<void> chooseWorkingDevice() async {}
452
453 @override
454 Future<void> chooseWorkingDeviceById(String deviceId) async {}
455
456 @override
457 Future<List<String>> discoverDevices() async {
458 return <String>['windows'];
459 }
460
461 @override
462 Future<void> performPreflightTasks() async {}
463
464 @override
465 Future<Device> get workingDevice async => _device;
466}
467
468class FuchsiaDeviceDiscovery implements DeviceDiscovery {
469 factory FuchsiaDeviceDiscovery() {
470 return _instance ??= FuchsiaDeviceDiscovery._();
471 }
472
473 FuchsiaDeviceDiscovery._();
474
475 static FuchsiaDeviceDiscovery? _instance;
476
477 FuchsiaDevice? _workingDevice;
478
479 String get _ffx {
480 final String ffx = path.join(getArtifactPath(), 'fuchsia', 'tools', 'x64', 'ffx');
481 if (!File(ffx).existsSync()) {
482 throw FileSystemException("Couldn't find ffx at location $ffx");
483 }
484 return ffx;
485 }
486
487 @override
488 Future<FuchsiaDevice> get workingDevice async {
489 if (_workingDevice == null) {
490 if (Platform.environment.containsKey(DeviceIdEnvName)) {
491 final String deviceId = Platform.environment[DeviceIdEnvName]!;
492 await chooseWorkingDeviceById(deviceId);
493 return _workingDevice!;
494 }
495 await chooseWorkingDevice();
496 }
497 return _workingDevice!;
498 }
499
500 /// Picks the first connected Fuchsia device.
501 @override
502 Future<void> chooseWorkingDevice() async {
503 final List<FuchsiaDevice> allDevices = (await discoverDevices())
504 .map<FuchsiaDevice>((String id) => FuchsiaDevice(deviceId: id))
505 .toList();
506
507 if (allDevices.isEmpty) {
508 throw const DeviceException('No Fuchsia devices detected');
509 }
510 _workingDevice = allDevices.first;
511 print('Device chosen: $_workingDevice');
512 }
513
514 @override
515 Future<void> chooseWorkingDeviceById(String deviceId) async {
516 final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
517 if (matchedId != null) {
518 _workingDevice = FuchsiaDevice(deviceId: matchedId);
519 print('Choose device by ID: $matchedId');
520 return;
521 }
522 throw DeviceException(
523 'Device with ID $deviceId is not found for operating system: '
524 '$deviceOperatingSystem',
525 );
526 }
527
528 @override
529 Future<List<String>> discoverDevices() async {
530 final List<String> output = (await eval(_ffx, <String>[
531 'target',
532 'list',
533 '-f',
534 's',
535 ])).trim().split('\n');
536
537 final List<String> devices = <String>[];
538 for (final String line in output) {
539 final List<String> parts = line.split(' ');
540 assert(parts.length == 2);
541 devices.add(parts.last); // The device id.
542 }
543 return devices;
544 }
545
546 @override
547 Future<Map<String, HealthCheckResult>> checkDevices() async {
548 final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
549 for (final String deviceId in await discoverDevices()) {
550 try {
551 final int resolveResult = await exec(_ffx, <String>['target', 'list', '-f', 'a', deviceId]);
552 if (resolveResult == 0) {
553 results['fuchsia-device-$deviceId'] = HealthCheckResult.success();
554 } else {
555 results['fuchsia-device-$deviceId'] = HealthCheckResult.failure(
556 'Cannot resolve device $deviceId',
557 );
558 }
559 } on Exception catch (error, stacktrace) {
560 results['fuchsia-device-$deviceId'] = HealthCheckResult.error(error, stacktrace);
561 }
562 }
563 return results;
564 }
565
566 @override
567 Future<void> performPreflightTasks() async {}
568}
569
570class AndroidDevice extends Device {
571 AndroidDevice({required this.deviceId}) {
572 _updateDeviceInfo();
573 }
574
575 @override
576 final String deviceId;
577 String deviceInfo = '';
578 int apiLevel = 0;
579
580 @override
581 Future<void> toggleFixedPerformanceMode(bool enable) async {
582 await shellExec('cmd', <String>[
583 'power',
584 'set-fixed-performance-mode-enabled',
585 if (enable) 'true' else 'false',
586 ]);
587 }
588
589 /// Whether the device is awake.
590 @override
591 Future<bool> isAwake() async {
592 return await _getWakefulness() == 'Awake';
593 }
594
595 /// Whether the device is asleep.
596 @override
597 Future<bool> isAsleep() async {
598 return await _getWakefulness() == 'Asleep';
599 }
600
601 /// Wake up the device if it is not awake using [togglePower].
602 @override
603 Future<void> wakeUp() async {
604 if (!(await isAwake())) {
605 await togglePower();
606 }
607 }
608
609 /// Send the device to sleep mode if it is not asleep using [togglePower].
610 @override
611 Future<void> sendToSleep() async {
612 if (!(await isAsleep())) {
613 await togglePower();
614 }
615 }
616
617 /// Sends `KEYCODE_HOME` (3), which causes the device to go to the home screen.
618 @override
619 Future<void> home() async {
620 await shellExec('input', const <String>['keyevent', '3']);
621 }
622
623 /// Sends `KEYCODE_POWER` (26), which causes the device to toggle its mode
624 /// between awake and asleep.
625 @override
626 Future<void> togglePower() async {
627 await shellExec('input', const <String>['keyevent', '26']);
628 }
629
630 /// Unlocks the device by sending `KEYCODE_MENU` (82).
631 ///
632 /// This only works when the device doesn't have a secure unlock pattern.
633 @override
634 Future<void> unlock() async {
635 await wakeUp();
636 await shellExec('input', const <String>['keyevent', '82']);
637 }
638
639 @override
640 Future<void> tap(int x, int y) async {
641 await shellExec('input', <String>['tap', '$x', '$y']);
642 }
643
644 /// Retrieves device's wakefulness state.
645 ///
646 /// See: https://android.googlesource.com/platform/frameworks/base/+/main/core/java/android/os/PowerManagerInternal.java
647 Future<String> _getWakefulness() async {
648 final String powerInfo = await shellEval('dumpsys', <String>['power']);
649 // A motoG4 phone returns `mWakefulness=Awake`.
650 // A Samsung phone returns `getWakefullnessLocked()=Awake`.
651 final RegExp wakefulnessRegexp = RegExp(r'.*(mWakefulness=|getWakefulnessLocked\(\)=).*');
652 final String wakefulness = grep(wakefulnessRegexp, from: powerInfo).single.split('=')[1].trim();
653 return wakefulness;
654 }
655
656 Future<bool> isArm64() async {
657 final String cpuInfo = await shellEval('getprop', const <String>['ro.product.cpu.abi']);
658 return cpuInfo.contains('arm64');
659 }
660
661 Future<bool> isArm() async {
662 final String cpuInfo = await shellEval('getprop', const <String>['ro.product.cpu.abi']);
663 return cpuInfo.contains('armeabi');
664 }
665
666 Future<void> _updateDeviceInfo() async {
667 String info;
668 try {
669 info = await shellEval('getprop', <String>[
670 'ro.bootimage.build.fingerprint',
671 ';',
672 'getprop',
673 'ro.build.version.release',
674 ';',
675 'getprop',
676 'ro.build.version.sdk',
677 ], silent: true);
678 } on IOException {
679 info = '';
680 }
681 final List<String> list = info.split('\n');
682 if (list.length == 3) {
683 apiLevel = int.parse(list[2]);
684 deviceInfo = 'fingerprint: ${list[0]} os: ${list[1]} api-level: $apiLevel';
685 } else {
686 apiLevel = 0;
687 deviceInfo = '';
688 }
689 }
690
691 /// Executes [command] on `adb shell`.
692 Future<void> shellExec(
693 String command,
694 List<String> arguments, {
695 Map<String, String>? environment,
696 bool silent = false,
697 }) async {
698 await adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
699 }
700
701 /// Executes [command] on `adb shell` and returns its standard output as a [String].
702 Future<String> shellEval(
703 String command,
704 List<String> arguments, {
705 Map<String, String>? environment,
706 bool silent = false,
707 }) {
708 return adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
709 }
710
711 /// Runs `adb` with the given [arguments], selecting this device.
712 Future<String> adb(
713 List<String> arguments, {
714 Map<String, String>? environment,
715 bool silent = false,
716 bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
717 }) {
718 return eval(
719 adbPath,
720 <String>['-s', deviceId, ...arguments],
721 environment: environment,
722 printStdout: !silent,
723 printStderr: !silent,
724 canFail: canFail,
725 );
726 }
727
728 @override
729 Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
730 final String meminfo = await shellEval('dumpsys', <String>['meminfo', packageName]);
731 final Match? match = RegExp(r'TOTAL\s+(\d+)').firstMatch(meminfo);
732 assert(match != null, 'could not parse dumpsys meminfo output');
733 return <String, dynamic>{'total_kb': int.parse(match!.group(1)!)};
734 }
735
736 @override
737 bool get canStreamLogs => true;
738
739 bool _abortedLogging = false;
740 Process? _loggingProcess;
741
742 @override
743 Future<void> startLoggingToSink(IOSink sink, {bool clear = true}) async {
744 if (clear) {
745 await adb(<String>['logcat', '--clear'], silent: true, canFail: true);
746 }
747 _loggingProcess = await startProcess(
748 adbPath,
749 // Catch the whole log.
750 <String>['-s', deviceId, 'logcat'],
751 );
752 _loggingProcess!.stdout.transform<String>(const Utf8Decoder(allowMalformed: true)).listen((
753 String line,
754 ) {
755 sink.write(line);
756 });
757 _loggingProcess!.stderr.transform<String>(const Utf8Decoder(allowMalformed: true)).listen((
758 String line,
759 ) {
760 sink.write(line);
761 });
762 unawaited(
763 _loggingProcess!.exitCode.then<void>((int exitCode) {
764 if (!_abortedLogging) {
765 sink.writeln('adb logcat failed with exit code $exitCode.\n');
766 }
767 }),
768 );
769 }
770
771 @override
772 Future<void> stopLoggingToSink() async {
773 if (_loggingProcess != null) {
774 _abortedLogging = true;
775 _loggingProcess!.kill();
776 await _loggingProcess!.exitCode;
777 }
778 }
779
780 @override
781 Future<void> clearLogs() {
782 return adb(<String>['logcat', '-c'], canFail: true);
783 }
784
785 @override
786 Stream<String> get logcat {
787 final Completer<void> stdoutDone = Completer<void>();
788 final Completer<void> stderrDone = Completer<void>();
789 final Completer<void> processDone = Completer<void>();
790 final Completer<void> abort = Completer<void>();
791 bool aborted = false;
792 late final StreamController<String> stream;
793 stream = StreamController<String>(
794 onListen: () async {
795 await clearLogs();
796 final Process process = await startProcess(
797 adbPath,
798 // Make logcat less chatty by filtering down to just ActivityManager
799 // (to let us know when app starts), flutter (needed by tests to see
800 // log output), and fatal messages (hopefully catches tombstones).
801 // For local testing, this can just be:
802 // ['-s', deviceId, 'logcat']
803 // to view the whole log, or just run logcat alongside this.
804 <String>['-s', deviceId, 'logcat', 'ActivityManager:I', 'flutter:V', '*:F'],
805 );
806 process.stdout
807 .transform<String>(utf8.decoder)
808 .transform<String>(const LineSplitter())
809 .listen(
810 (String line) {
811 print('adb logcat: $line');
812 if (!stream.isClosed) {
813 stream.sink.add(line);
814 }
815 },
816 onDone: () {
817 stdoutDone.complete();
818 },
819 );
820 process.stderr
821 .transform<String>(utf8.decoder)
822 .transform<String>(const LineSplitter())
823 .listen(
824 (String line) {
825 print('adb logcat stderr: $line');
826 },
827 onDone: () {
828 stderrDone.complete();
829 },
830 );
831 unawaited(
832 process.exitCode.then<void>((int exitCode) {
833 print('adb logcat process terminated with exit code $exitCode');
834 if (!aborted) {
835 stream.addError(BuildFailedError('adb logcat failed with exit code $exitCode.\n'));
836 processDone.complete();
837 }
838 }),
839 );
840 await Future.any<dynamic>(<Future<dynamic>>[
841 Future.wait<void>(<Future<void>>[
842 stdoutDone.future,
843 stderrDone.future,
844 processDone.future,
845 ]),
846 abort.future,
847 ]);
848 aborted = true;
849 print('terminating adb logcat');
850 process.kill();
851 print('closing logcat stream');
852 await stream.close();
853 },
854 onCancel: () {
855 if (!aborted) {
856 print('adb logcat aborted');
857 aborted = true;
858 abort.complete();
859 }
860 },
861 );
862 return stream.stream;
863 }
864
865 @override
866 Future<void> stop(String packageName) async {
867 return shellExec('am', <String>['force-stop', packageName]);
868 }
869
870 @override
871 String toString() {
872 return '$deviceId $deviceInfo';
873 }
874
875 @override
876 Future<void> reboot() {
877 return adb(<String>['reboot']);
878 }
879
880 @override
881 Future<void> awaitDevice() async {
882 print('Waiting for device.');
883 final String waitOut = await adb(<String>['wait-for-device']);
884 print(waitOut);
885 const RetryOptions retryOptions = RetryOptions(
886 delayFactor: Duration(seconds: 1),
887 maxAttempts: 10,
888 maxDelay: Duration(minutes: 1),
889 );
890 await retryOptions.retry(() async {
891 final String adbShellOut = await adb(<String>['shell', 'getprop sys.boot_completed']);
892 if (adbShellOut != '1') {
893 print('Device not ready.');
894 print(adbShellOut);
895 throw const DeviceException('Phone not ready.');
896 }
897 }, retryIf: (Exception e) => e is DeviceException);
898 print('Done waiting for device.');
899 }
900}
901
902class IosDeviceDiscovery implements DeviceDiscovery {
903 factory IosDeviceDiscovery() {
904 return _instance ??= IosDeviceDiscovery._();
905 }
906
907 IosDeviceDiscovery._();
908
909 static IosDeviceDiscovery? _instance;
910
911 IosDevice? _workingDevice;
912
913 @override
914 Future<IosDevice> get workingDevice async {
915 if (_workingDevice == null) {
916 if (Platform.environment.containsKey(DeviceIdEnvName)) {
917 final String deviceId = Platform.environment[DeviceIdEnvName]!;
918 await chooseWorkingDeviceById(deviceId);
919 return _workingDevice!;
920 }
921 await chooseWorkingDevice();
922 }
923
924 return _workingDevice!;
925 }
926
927 /// Picks a random iOS device out of connected devices and sets it as
928 /// [workingDevice].
929 @override
930 Future<void> chooseWorkingDevice() async {
931 final List<IosDevice> allDevices = (await discoverDevices())
932 .map<IosDevice>((String id) => IosDevice(deviceId: id))
933 .toList();
934
935 if (allDevices.isEmpty) {
936 throw const DeviceException('No iOS devices detected');
937 }
938
939 // TODO(yjbanov): filter out and warn about those with low battery level
940 _workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
941 print('Device chosen: $_workingDevice');
942 }
943
944 @override
945 Future<void> chooseWorkingDeviceById(String deviceId) async {
946 final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
947 if (matchedId != null) {
948 _workingDevice = IosDevice(deviceId: matchedId);
949 print('Choose device by ID: $matchedId');
950 return;
951 }
952 throw DeviceException(
953 'Device with ID $deviceId is not found for operating system: '
954 '$deviceOperatingSystem',
955 );
956 }
957
958 @override
959 Future<List<String>> discoverDevices() async {
960 final List<dynamic> results =
961 json.decode(
962 await eval(path.join(flutterDirectory.path, 'bin', 'flutter'), <String>[
963 'devices',
964 '--machine',
965 '--suppress-analytics',
966 '--device-timeout',
967 '5',
968 ]),
969 )
970 as List<dynamic>;
971
972 // [
973 // {
974 // "name": "Flutter's iPhone",
975 // "id": "00008020-00017DA80CC1002E",
976 // "isSupported": true,
977 // "targetPlatform": "ios",
978 // "emulator": false,
979 // "sdk": "iOS 13.2",
980 // "capabilities": {
981 // "hotReload": true,
982 // "hotRestart": true,
983 // "screenshot": true,
984 // "fastStart": false,
985 // "flutterExit": true,
986 // "hardwareRendering": false,
987 // "startPaused": false
988 // }
989 // }
990 // ]
991
992 final List<String> deviceIds = <String>[];
993
994 for (final dynamic result in results) {
995 final Map<String, dynamic> device = result as Map<String, dynamic>;
996 if (device['targetPlatform'] == 'ios' &&
997 device['id'] != null &&
998 device['emulator'] != true &&
999 device['isSupported'] == true) {
1000 deviceIds.add(device['id'] as String);
1001 }
1002 }
1003
1004 if (deviceIds.isEmpty) {
1005 throw const DeviceException('No connected physical iOS devices found.');
1006 }
1007 return deviceIds;
1008 }
1009
1010 @override
1011 Future<Map<String, HealthCheckResult>> checkDevices() async {
1012 final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
1013 for (final String deviceId in await discoverDevices()) {
1014 // TODO(ianh): do a more meaningful connectivity check than just recording the ID
1015 results['ios-device-$deviceId'] = HealthCheckResult.success();
1016 }
1017 return results;
1018 }
1019
1020 @override
1021 Future<void> performPreflightTasks() async {
1022 // Currently we do not have preflight tasks for iOS.
1023 }
1024}
1025
1026/// iOS device.
1027class IosDevice extends Device {
1028 IosDevice({required this.deviceId});
1029
1030 @override
1031 final String deviceId;
1032
1033 String get idevicesyslogPath {
1034 return path.join(
1035 flutterDirectory.path,
1036 'bin',
1037 'cache',
1038 'artifacts',
1039 'libimobiledevice',
1040 'idevicesyslog',
1041 );
1042 }
1043
1044 String get dyldLibraryPath {
1045 final List<String> dylibsPaths = <String>[
1046 path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libimobiledevice'),
1047 path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'openssl'),
1048 path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libusbmuxd'),
1049 path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libplist'),
1050 path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libimobiledeviceglue'),
1051 ];
1052 return dylibsPaths.join(':');
1053 }
1054
1055 @override
1056 bool get canStreamLogs => true;
1057
1058 bool _abortedLogging = false;
1059 Process? _loggingProcess;
1060
1061 @override
1062 Future<void> startLoggingToSink(IOSink sink, {bool clear = true}) async {
1063 // Clear is not supported.
1064 _loggingProcess = await startProcess(
1065 idevicesyslogPath,
1066 <String>['-u', deviceId, '--quiet'],
1067 environment: <String, String>{'DYLD_LIBRARY_PATH': dyldLibraryPath},
1068 );
1069 _loggingProcess!.stdout.transform<String>(const Utf8Decoder(allowMalformed: true)).listen((
1070 String line,
1071 ) {
1072 sink.write(line);
1073 });
1074 _loggingProcess!.stderr.transform<String>(const Utf8Decoder(allowMalformed: true)).listen((
1075 String line,
1076 ) {
1077 sink.write(line);
1078 });
1079 unawaited(
1080 _loggingProcess!.exitCode.then<void>((int exitCode) {
1081 if (!_abortedLogging) {
1082 sink.writeln('idevicesyslog failed with exit code $exitCode.\n');
1083 }
1084 }),
1085 );
1086 }
1087
1088 @override
1089 Future<void> stopLoggingToSink() async {
1090 if (_loggingProcess != null) {
1091 _abortedLogging = true;
1092 _loggingProcess!.kill();
1093 await _loggingProcess!.exitCode;
1094 }
1095 }
1096
1097 // The methods below are stubs for now. They will need to be expanded.
1098 // We currently do not have a way to lock/unlock iOS devices. So we assume the
1099 // devices are already unlocked. For now we'll just keep them at minimum
1100 // screen brightness so they don't drain battery too fast.
1101
1102 @override
1103 Future<bool> isAwake() async => true;
1104
1105 @override
1106 Future<bool> isAsleep() async => false;
1107
1108 @override
1109 Future<void> wakeUp() async {}
1110
1111 @override
1112 Future<void> sendToSleep() async {}
1113
1114 @override
1115 Future<void> home() async {}
1116
1117 @override
1118 Future<void> togglePower() async {}
1119
1120 @override
1121 Future<void> unlock() async {}
1122
1123 @override
1124 Future<void> tap(int x, int y) async {
1125 throw UnimplementedError();
1126 }
1127
1128 @override
1129 Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
1130 throw UnimplementedError();
1131 }
1132
1133 @override
1134 Stream<String> get logcat {
1135 throw UnimplementedError();
1136 }
1137
1138 @override
1139 Future<void> clearLogs() async {}
1140
1141 @override
1142 Future<void> stop(String packageName) async {}
1143
1144 @override
1145 Future<void> reboot() {
1146 return Process.run('idevicediagnostics', <String>['restart', '-u', deviceId]);
1147 }
1148
1149 @override
1150 Future<void> awaitDevice() async {}
1151}
1152
1153class LinuxDevice extends Device {
1154 const LinuxDevice();
1155
1156 @override
1157 String get deviceId => 'linux';
1158
1159 @override
1160 Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
1161 return <String, dynamic>{};
1162 }
1163
1164 @override
1165 Future<void> home() async {}
1166
1167 @override
1168 Future<bool> isAsleep() async {
1169 return false;
1170 }
1171
1172 @override
1173 Future<bool> isAwake() async {
1174 return true;
1175 }
1176
1177 @override
1178 Stream<String> get logcat => const Stream<String>.empty();
1179
1180 @override
1181 Future<void> clearLogs() async {}
1182
1183 @override
1184 Future<void> reboot() async {}
1185
1186 @override
1187 Future<void> sendToSleep() async {}
1188
1189 @override
1190 Future<void> stop(String packageName) async {}
1191
1192 @override
1193 Future<void> tap(int x, int y) async {}
1194
1195 @override
1196 Future<void> togglePower() async {}
1197
1198 @override
1199 Future<void> unlock() async {}
1200
1201 @override
1202 Future<void> wakeUp() async {}
1203
1204 @override
1205 Future<void> awaitDevice() async {}
1206}
1207
1208class MacosDevice extends Device {
1209 const MacosDevice();
1210
1211 @override
1212 String get deviceId => 'macos';
1213
1214 @override
1215 Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
1216 return <String, dynamic>{};
1217 }
1218
1219 @override
1220 Future<void> home() async {}
1221
1222 @override
1223 Future<bool> isAsleep() async {
1224 return false;
1225 }
1226
1227 @override
1228 Future<bool> isAwake() async {
1229 return true;
1230 }
1231
1232 @override
1233 Stream<String> get logcat => const Stream<String>.empty();
1234
1235 @override
1236 Future<void> clearLogs() async {}
1237
1238 @override
1239 Future<void> reboot() async {}
1240
1241 @override
1242 Future<void> sendToSleep() async {}
1243
1244 @override
1245 Future<void> stop(String packageName) async {}
1246
1247 @override
1248 Future<void> tap(int x, int y) async {}
1249
1250 @override
1251 Future<void> togglePower() async {}
1252
1253 @override
1254 Future<void> unlock() async {}
1255
1256 @override
1257 Future<void> wakeUp() async {}
1258
1259 @override
1260 Future<void> awaitDevice() async {}
1261}
1262
1263class WindowsDevice extends Device {
1264 const WindowsDevice();
1265
1266 @override
1267 String get deviceId => 'windows';
1268
1269 @override
1270 Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
1271 return <String, dynamic>{};
1272 }
1273
1274 @override
1275 Future<void> home() async {}
1276
1277 @override
1278 Future<bool> isAsleep() async {
1279 return false;
1280 }
1281
1282 @override
1283 Future<bool> isAwake() async {
1284 return true;
1285 }
1286
1287 @override
1288 Stream<String> get logcat => const Stream<String>.empty();
1289
1290 @override
1291 Future<void> clearLogs() async {}
1292
1293 @override
1294 Future<void> reboot() async {}
1295
1296 @override
1297 Future<void> sendToSleep() async {}
1298
1299 @override
1300 Future<void> stop(String packageName) async {}
1301
1302 @override
1303 Future<void> tap(int x, int y) async {}
1304
1305 @override
1306 Future<void> togglePower() async {}
1307
1308 @override
1309 Future<void> unlock() async {}
1310
1311 @override
1312 Future<void> wakeUp() async {}
1313
1314 @override
1315 Future<void> awaitDevice() async {}
1316}
1317
1318/// Fuchsia device.
1319class FuchsiaDevice extends Device {
1320 const FuchsiaDevice({required this.deviceId});
1321
1322 @override
1323 final String deviceId;
1324
1325 // TODO(egarciad): Implement these for Fuchsia.
1326 @override
1327 Future<bool> isAwake() async => true;
1328
1329 @override
1330 Future<bool> isAsleep() async => false;
1331
1332 @override
1333 Future<void> wakeUp() async {}
1334
1335 @override
1336 Future<void> sendToSleep() async {}
1337
1338 @override
1339 Future<void> home() async {}
1340
1341 @override
1342 Future<void> togglePower() async {}
1343
1344 @override
1345 Future<void> unlock() async {}
1346
1347 @override
1348 Future<void> tap(int x, int y) async {}
1349
1350 @override
1351 Future<void> stop(String packageName) async {}
1352
1353 @override
1354 Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
1355 throw UnimplementedError();
1356 }
1357
1358 @override
1359 Stream<String> get logcat {
1360 throw UnimplementedError();
1361 }
1362
1363 @override
1364 Future<void> clearLogs() async {}
1365
1366 @override
1367 Future<void> reboot() async {
1368 // Unsupported.
1369 }
1370
1371 @override
1372 Future<void> awaitDevice() async {}
1373}
1374
1375/// Path to the `adb` executable.
1376String get adbPath {
1377 final String? androidHome =
1378 Platform.environment['ANDROID_HOME'] ?? Platform.environment['ANDROID_SDK_ROOT'];
1379
1380 if (androidHome == null) {
1381 throw const DeviceException(
1382 'The ANDROID_HOME environment variable is '
1383 'missing. The variable must point to the Android '
1384 'SDK directory containing platform-tools.',
1385 );
1386 }
1387
1388 final String adbPath = path.join(androidHome, 'platform-tools/adb');
1389
1390 if (!canRun(adbPath)) {
1391 throw DeviceException('adb not found at: $adbPath');
1392 }
1393
1394 return path.absolute(adbPath);
1395}
1396
1397class FakeDevice extends Device {
1398 const FakeDevice({required this.deviceId});
1399
1400 @override
1401 final String deviceId;
1402
1403 @override
1404 Future<bool> isAwake() async => true;
1405
1406 @override
1407 Future<bool> isAsleep() async => false;
1408
1409 @override
1410 Future<void> wakeUp() async {}
1411
1412 @override
1413 Future<void> sendToSleep() async {}
1414
1415 @override
1416 Future<void> home() async {}
1417
1418 @override
1419 Future<void> togglePower() async {}
1420
1421 @override
1422 Future<void> unlock() async {}
1423
1424 @override
1425 Future<void> tap(int x, int y) async {
1426 throw UnimplementedError();
1427 }
1428
1429 @override
1430 Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
1431 throw UnimplementedError();
1432 }
1433
1434 @override
1435 Stream<String> get logcat {
1436 throw UnimplementedError();
1437 }
1438
1439 @override
1440 Future<void> clearLogs() async {}
1441
1442 @override
1443 Future<void> stop(String packageName) async {}
1444
1445 @override
1446 Future<void> reboot() async {
1447 // Unsupported.
1448 }
1449
1450 @override
1451 Future<void> awaitDevice() async {}
1452}
1453
1454class FakeDeviceDiscovery implements DeviceDiscovery {
1455 factory FakeDeviceDiscovery() {
1456 return _instance ??= FakeDeviceDiscovery._();
1457 }
1458
1459 FakeDeviceDiscovery._();
1460
1461 static FakeDeviceDiscovery? _instance;
1462
1463 FakeDevice? _workingDevice;
1464
1465 @override
1466 Future<FakeDevice> get workingDevice async {
1467 if (_workingDevice == null) {
1468 if (Platform.environment.containsKey(DeviceIdEnvName)) {
1469 final String deviceId = Platform.environment[DeviceIdEnvName]!;
1470 await chooseWorkingDeviceById(deviceId);
1471 return _workingDevice!;
1472 }
1473 await chooseWorkingDevice();
1474 }
1475
1476 return _workingDevice!;
1477 }
1478
1479 /// The Fake is only available for by ID device discovery.
1480 @override
1481 Future<void> chooseWorkingDevice() async {
1482 throw const DeviceException('No fake devices detected');
1483 }
1484
1485 @override
1486 Future<void> chooseWorkingDeviceById(String deviceId) async {
1487 final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
1488 if (matchedId != null) {
1489 _workingDevice = FakeDevice(deviceId: matchedId);
1490 print('Choose device by ID: $matchedId');
1491 return;
1492 }
1493 throw DeviceException(
1494 'Device with ID $deviceId is not found for operating system: '
1495 '$deviceOperatingSystem',
1496 );
1497 }
1498
1499 @override
1500 Future<List<String>> discoverDevices() async {
1501 return <String>['FAKE_SUCCESS', 'THIS_IS_A_FAKE'];
1502 }
1503
1504 @override
1505 Future<Map<String, HealthCheckResult>> checkDevices() async {
1506 final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
1507 for (final String deviceId in await discoverDevices()) {
1508 results['fake-device-$deviceId'] = HealthCheckResult.success();
1509 }
1510 return results;
1511 }
1512
1513 @override
1514 Future<void> performPreflightTasks() async {}
1515}
1516