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 =
259 (await discoverDevices())
260 .map<AndroidDevice>((String id) => AndroidDevice(deviceId: id))
261 .toList();
262
263 if (allDevices.isEmpty) {
264 throw const DeviceException('No Android devices detected');
265 }
266
267 if (cpu != null) {
268 for (final AndroidDevice device in allDevices) {
269 if (await _matchesCPURequirement(device)) {
270 _workingDevice = device;
271 break;
272 }
273 }
274 } else {
275 // TODO(yjbanov): filter out and warn about those with low battery level
276 _workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
277 }
278
279 if (_workingDevice == null) {
280 throw const DeviceException('Cannot find a suitable Android device');
281 }
282
283 print('Device chosen: $_workingDevice');
284 }
285
286 @override
287 Future<void> chooseWorkingDeviceById(String deviceId) async {
288 final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
289 if (matchedId != null) {
290 _workingDevice = AndroidDevice(deviceId: matchedId);
291 if (cpu != null) {
292 if (!await _matchesCPURequirement(_workingDevice!)) {
293 throw DeviceException(
294 'The selected device $matchedId does not match the cpu requirement',
295 );
296 }
297 }
298 print('Choose device by ID: $matchedId');
299 return;
300 }
301 throw DeviceException(
302 'Device with ID $deviceId is not found for operating system: '
303 '$deviceOperatingSystem',
304 );
305 }
306
307 @override
308 Future<List<String>> discoverDevices() async {
309 final List<String> output = (await eval(adbPath, <String>['devices', '-l'])).trim().split('\n');
310 final List<String> results = <String>[];
311 for (final String line in output) {
312 // Skip lines like: * daemon started successfully *
313 if (line.startsWith('* daemon ')) {
314 continue;
315 }
316
317 if (line.startsWith('List of devices')) {
318 continue;
319 }
320
321 if (_kDeviceRegex.hasMatch(line)) {
322 final Match match = _kDeviceRegex.firstMatch(line)!;
323
324 final String deviceID = match[1]!;
325 final String deviceState = match[2]!;
326
327 if (!const <String>['unauthorized', 'offline'].contains(deviceState)) {
328 results.add(deviceID);
329 }
330 } else {
331 throw FormatException('Failed to parse device from adb output: "$line"');
332 }
333 }
334
335 return results;
336 }
337
338 @override
339 Future<Map<String, HealthCheckResult>> checkDevices() async {
340 final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
341 for (final String deviceId in await discoverDevices()) {
342 try {
343 final AndroidDevice device = AndroidDevice(deviceId: deviceId);
344 // Just a smoke test that we can read wakefulness state
345 // TODO(yjbanov): check battery level
346 await device._getWakefulness();
347 results['android-device-$deviceId'] = HealthCheckResult.success();
348 } on Exception catch (e, s) {
349 results['android-device-$deviceId'] = HealthCheckResult.error(e, s);
350 }
351 }
352 return results;
353 }
354
355 @override
356 Future<void> performPreflightTasks() async {
357 // Kills the `adb` server causing it to start a new instance upon next
358 // command.
359 //
360 // Restarting `adb` helps with keeping device connections alive. When `adb`
361 // runs non-stop for too long it loses connections to devices. There may be
362 // a better method, but so far that's the best one I've found.
363 await exec(adbPath, <String>['kill-server']);
364 }
365}
366
367class LinuxDeviceDiscovery implements DeviceDiscovery {
368 factory LinuxDeviceDiscovery() {
369 return _instance ??= LinuxDeviceDiscovery._();
370 }
371
372 LinuxDeviceDiscovery._();
373
374 static LinuxDeviceDiscovery? _instance;
375
376 static const LinuxDevice _device = LinuxDevice();
377
378 @override
379 Future<Map<String, HealthCheckResult>> checkDevices() async {
380 return <String, HealthCheckResult>{};
381 }
382
383 @override
384 Future<void> chooseWorkingDevice() async {}
385
386 @override
387 Future<void> chooseWorkingDeviceById(String deviceId) async {}
388
389 @override
390 Future<List<String>> discoverDevices() async {
391 return <String>['linux'];
392 }
393
394 @override
395 Future<void> performPreflightTasks() async {}
396
397 @override
398 Future<Device> get workingDevice async => _device;
399}
400
401class MacosDeviceDiscovery implements DeviceDiscovery {
402 factory MacosDeviceDiscovery() {
403 return _instance ??= MacosDeviceDiscovery._();
404 }
405
406 MacosDeviceDiscovery._();
407
408 static MacosDeviceDiscovery? _instance;
409
410 static const MacosDevice _device = MacosDevice();
411
412 @override
413 Future<Map<String, HealthCheckResult>> checkDevices() async {
414 return <String, HealthCheckResult>{};
415 }
416
417 @override
418 Future<void> chooseWorkingDevice() async {}
419
420 @override
421 Future<void> chooseWorkingDeviceById(String deviceId) async {}
422
423 @override
424 Future<List<String>> discoverDevices() async {
425 return <String>['macos'];
426 }
427
428 @override
429 Future<void> performPreflightTasks() async {}
430
431 @override
432 Future<Device> get workingDevice async => _device;
433}
434
435class WindowsDeviceDiscovery implements DeviceDiscovery {
436 factory WindowsDeviceDiscovery() {
437 return _instance ??= WindowsDeviceDiscovery._();
438 }
439
440 WindowsDeviceDiscovery._();
441
442 static WindowsDeviceDiscovery? _instance;
443
444 static const WindowsDevice _device = WindowsDevice();
445
446 @override
447 Future<Map<String, HealthCheckResult>> checkDevices() async {
448 return <String, HealthCheckResult>{};
449 }
450
451 @override
452 Future<void> chooseWorkingDevice() async {}
453
454 @override
455 Future<void> chooseWorkingDeviceById(String deviceId) async {}
456
457 @override
458 Future<List<String>> discoverDevices() async {
459 return <String>['windows'];
460 }
461
462 @override
463 Future<void> performPreflightTasks() async {}
464
465 @override
466 Future<Device> get workingDevice async => _device;
467}
468
469class FuchsiaDeviceDiscovery implements DeviceDiscovery {
470 factory FuchsiaDeviceDiscovery() {
471 return _instance ??= FuchsiaDeviceDiscovery._();
472 }
473
474 FuchsiaDeviceDiscovery._();
475
476 static FuchsiaDeviceDiscovery? _instance;
477
478 FuchsiaDevice? _workingDevice;
479
480 String get _ffx {
481 final String ffx = path.join(getArtifactPath(), 'fuchsia', 'tools', 'x64', 'ffx');
482 if (!File(ffx).existsSync()) {
483 throw FileSystemException("Couldn't find ffx at location $ffx");
484 }
485 return ffx;
486 }
487
488 @override
489 Future<FuchsiaDevice> get workingDevice async {
490 if (_workingDevice == null) {
491 if (Platform.environment.containsKey(DeviceIdEnvName)) {
492 final String deviceId = Platform.environment[DeviceIdEnvName]!;
493 await chooseWorkingDeviceById(deviceId);
494 return _workingDevice!;
495 }
496 await chooseWorkingDevice();
497 }
498 return _workingDevice!;
499 }
500
501 /// Picks the first connected Fuchsia device.
502 @override
503 Future<void> chooseWorkingDevice() async {
504 final List<FuchsiaDevice> allDevices =
505 (await discoverDevices())
506 .map<FuchsiaDevice>((String id) => FuchsiaDevice(deviceId: id))
507 .toList();
508
509 if (allDevices.isEmpty) {
510 throw const DeviceException('No Fuchsia devices detected');
511 }
512 _workingDevice = allDevices.first;
513 print('Device chosen: $_workingDevice');
514 }
515
516 @override
517 Future<void> chooseWorkingDeviceById(String deviceId) async {
518 final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
519 if (matchedId != null) {
520 _workingDevice = FuchsiaDevice(deviceId: matchedId);
521 print('Choose device by ID: $matchedId');
522 return;
523 }
524 throw DeviceException(
525 'Device with ID $deviceId is not found for operating system: '
526 '$deviceOperatingSystem',
527 );
528 }
529
530 @override
531 Future<List<String>> discoverDevices() async {
532 final List<String> output = (await eval(_ffx, <String>[
533 'target',
534 'list',
535 '-f',
536 's',
537 ])).trim().split('\n');
538
539 final List<String> devices = <String>[];
540 for (final String line in output) {
541 final List<String> parts = line.split(' ');
542 assert(parts.length == 2);
543 devices.add(parts.last); // The device id.
544 }
545 return devices;
546 }
547
548 @override
549 Future<Map<String, HealthCheckResult>> checkDevices() async {
550 final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
551 for (final String deviceId in await discoverDevices()) {
552 try {
553 final int resolveResult = await exec(_ffx, <String>['target', 'list', '-f', 'a', deviceId]);
554 if (resolveResult == 0) {
555 results['fuchsia-device-$deviceId'] = HealthCheckResult.success();
556 } else {
557 results['fuchsia-device-$deviceId'] = HealthCheckResult.failure(
558 'Cannot resolve device $deviceId',
559 );
560 }
561 } on Exception catch (error, stacktrace) {
562 results['fuchsia-device-$deviceId'] = HealthCheckResult.error(error, stacktrace);
563 }
564 }
565 return results;
566 }
567
568 @override
569 Future<void> performPreflightTasks() async {}
570}
571
572class AndroidDevice extends Device {
573 AndroidDevice({required this.deviceId}) {
574 _updateDeviceInfo();
575 }
576
577 @override
578 final String deviceId;
579 String deviceInfo = '';
580 int apiLevel = 0;
581
582 @override
583 Future<void> toggleFixedPerformanceMode(bool enable) async {
584 await shellExec('cmd', <String>[
585 'power',
586 'set-fixed-performance-mode-enabled',
587 if (enable) 'true' else 'false',
588 ]);
589 }
590
591 /// Whether the device is awake.
592 @override
593 Future<bool> isAwake() async {
594 return await _getWakefulness() == 'Awake';
595 }
596
597 /// Whether the device is asleep.
598 @override
599 Future<bool> isAsleep() async {
600 return await _getWakefulness() == 'Asleep';
601 }
602
603 /// Wake up the device if it is not awake using [togglePower].
604 @override
605 Future<void> wakeUp() async {
606 if (!(await isAwake())) {
607 await togglePower();
608 }
609 }
610
611 /// Send the device to sleep mode if it is not asleep using [togglePower].
612 @override
613 Future<void> sendToSleep() async {
614 if (!(await isAsleep())) {
615 await togglePower();
616 }
617 }
618
619 /// Sends `KEYCODE_HOME` (3), which causes the device to go to the home screen.
620 @override
621 Future<void> home() async {
622 await shellExec('input', const <String>['keyevent', '3']);
623 }
624
625 /// Sends `KEYCODE_POWER` (26), which causes the device to toggle its mode
626 /// between awake and asleep.
627 @override
628 Future<void> togglePower() async {
629 await shellExec('input', const <String>['keyevent', '26']);
630 }
631
632 /// Unlocks the device by sending `KEYCODE_MENU` (82).
633 ///
634 /// This only works when the device doesn't have a secure unlock pattern.
635 @override
636 Future<void> unlock() async {
637 await wakeUp();
638 await shellExec('input', const <String>['keyevent', '82']);
639 }
640
641 @override
642 Future<void> tap(int x, int y) async {
643 await shellExec('input', <String>['tap', '$x', '$y']);
644 }
645
646 /// Retrieves device's wakefulness state.
647 ///
648 /// See: https://android.googlesource.com/platform/frameworks/base/+/main/core/java/android/os/PowerManagerInternal.java
649 Future<String> _getWakefulness() async {
650 final String powerInfo = await shellEval('dumpsys', <String>['power']);
651 // A motoG4 phone returns `mWakefulness=Awake`.
652 // A Samsung phone returns `getWakefullnessLocked()=Awake`.
653 final RegExp wakefulnessRegexp = RegExp(r'.*(mWakefulness=|getWakefulnessLocked\(\)=).*');
654 final String wakefulness = grep(wakefulnessRegexp, from: powerInfo).single.split('=')[1].trim();
655 return wakefulness;
656 }
657
658 Future<bool> isArm64() async {
659 final String cpuInfo = await shellEval('getprop', const <String>['ro.product.cpu.abi']);
660 return cpuInfo.contains('arm64');
661 }
662
663 Future<bool> isArm() async {
664 final String cpuInfo = await shellEval('getprop', const <String>['ro.product.cpu.abi']);
665 return cpuInfo.contains('armeabi');
666 }
667
668 Future<void> _updateDeviceInfo() async {
669 String info;
670 try {
671 info = await shellEval('getprop', <String>[
672 'ro.bootimage.build.fingerprint',
673 ';',
674 'getprop',
675 'ro.build.version.release',
676 ';',
677 'getprop',
678 'ro.build.version.sdk',
679 ], silent: true);
680 } on IOException {
681 info = '';
682 }
683 final List<String> list = info.split('\n');
684 if (list.length == 3) {
685 apiLevel = int.parse(list[2]);
686 deviceInfo = 'fingerprint: ${list[0]} os: ${list[1]} api-level: $apiLevel';
687 } else {
688 apiLevel = 0;
689 deviceInfo = '';
690 }
691 }
692
693 /// Executes [command] on `adb shell`.
694 Future<void> shellExec(
695 String command,
696 List<String> arguments, {
697 Map<String, String>? environment,
698 bool silent = false,
699 }) async {
700 await adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
701 }
702
703 /// Executes [command] on `adb shell` and returns its standard output as a [String].
704 Future<String> shellEval(
705 String command,
706 List<String> arguments, {
707 Map<String, String>? environment,
708 bool silent = false,
709 }) {
710 return adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
711 }
712
713 /// Runs `adb` with the given [arguments], selecting this device.
714 Future<String> adb(
715 List<String> arguments, {
716 Map<String, String>? environment,
717 bool silent = false,
718 bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
719 }) {
720 return eval(
721 adbPath,
722 <String>['-s', deviceId, ...arguments],
723 environment: environment,
724 printStdout: !silent,
725 printStderr: !silent,
726 canFail: canFail,
727 );
728 }
729
730 @override
731 Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
732 final String meminfo = await shellEval('dumpsys', <String>['meminfo', packageName]);
733 final Match? match = RegExp(r'TOTAL\s+(\d+)').firstMatch(meminfo);
734 assert(match != null, 'could not parse dumpsys meminfo output');
735 return <String, dynamic>{'total_kb': int.parse(match!.group(1)!)};
736 }
737
738 @override
739 bool get canStreamLogs => true;
740
741 bool _abortedLogging = false;
742 Process? _loggingProcess;
743
744 @override
745 Future<void> startLoggingToSink(IOSink sink, {bool clear = true}) async {
746 if (clear) {
747 await adb(<String>['logcat', '--clear'], silent: true, canFail: true);
748 }
749 _loggingProcess = await startProcess(
750 adbPath,
751 // Catch the whole log.
752 <String>['-s', deviceId, 'logcat'],
753 );
754 _loggingProcess!.stdout.transform<String>(const Utf8Decoder(allowMalformed: true)).listen((
755 String line,
756 ) {
757 sink.write(line);
758 });
759 _loggingProcess!.stderr.transform<String>(const Utf8Decoder(allowMalformed: true)).listen((
760 String line,
761 ) {
762 sink.write(line);
763 });
764 unawaited(
765 _loggingProcess!.exitCode.then<void>((int exitCode) {
766 if (!_abortedLogging) {
767 sink.writeln('adb logcat failed with exit code $exitCode.\n');
768 }
769 }),
770 );
771 }
772
773 @override
774 Future<void> stopLoggingToSink() async {
775 if (_loggingProcess != null) {
776 _abortedLogging = true;
777 _loggingProcess!.kill();
778 await _loggingProcess!.exitCode;
779 }
780 }
781
782 @override
783 Future<void> clearLogs() {
784 return adb(<String>['logcat', '-c'], canFail: true);
785 }
786
787 @override
788 Stream<String> get logcat {
789 final Completer<void> stdoutDone = Completer<void>();
790 final Completer<void> stderrDone = Completer<void>();
791 final Completer<void> processDone = Completer<void>();
792 final Completer<void> abort = Completer<void>();
793 bool aborted = false;
794 late final StreamController<String> stream;
795 stream = StreamController<String>(
796 onListen: () async {
797 await clearLogs();
798 final Process process = await startProcess(
799 adbPath,
800 // Make logcat less chatty by filtering down to just ActivityManager
801 // (to let us know when app starts), flutter (needed by tests to see
802 // log output), and fatal messages (hopefully catches tombstones).
803 // For local testing, this can just be:
804 // ['-s', deviceId, 'logcat']
805 // to view the whole log, or just run logcat alongside this.
806 <String>['-s', deviceId, 'logcat', 'ActivityManager:I', 'flutter:V', '*:F'],
807 );
808 process.stdout
809 .transform<String>(utf8.decoder)
810 .transform<String>(const LineSplitter())
811 .listen(
812 (String line) {
813 print('adb logcat: $line');
814 if (!stream.isClosed) {
815 stream.sink.add(line);
816 }
817 },
818 onDone: () {
819 stdoutDone.complete();
820 },
821 );
822 process.stderr
823 .transform<String>(utf8.decoder)
824 .transform<String>(const LineSplitter())
825 .listen(
826 (String line) {
827 print('adb logcat stderr: $line');
828 },
829 onDone: () {
830 stderrDone.complete();
831 },
832 );
833 unawaited(
834 process.exitCode.then<void>((int exitCode) {
835 print('adb logcat process terminated with exit code $exitCode');
836 if (!aborted) {
837 stream.addError(BuildFailedError('adb logcat failed with exit code $exitCode.\n'));
838 processDone.complete();
839 }
840 }),
841 );
842 await Future.any<dynamic>(<Future<dynamic>>[
843 Future.wait<void>(<Future<void>>[
844 stdoutDone.future,
845 stderrDone.future,
846 processDone.future,
847 ]),
848 abort.future,
849 ]);
850 aborted = true;
851 print('terminating adb logcat');
852 process.kill();
853 print('closing logcat stream');
854 await stream.close();
855 },
856 onCancel: () {
857 if (!aborted) {
858 print('adb logcat aborted');
859 aborted = true;
860 abort.complete();
861 }
862 },
863 );
864 return stream.stream;
865 }
866
867 @override
868 Future<void> stop(String packageName) async {
869 return shellExec('am', <String>['force-stop', packageName]);
870 }
871
872 @override
873 String toString() {
874 return '$deviceId $deviceInfo';
875 }
876
877 @override
878 Future<void> reboot() {
879 return adb(<String>['reboot']);
880 }
881
882 @override
883 Future<void> awaitDevice() async {
884 print('Waiting for device.');
885 final String waitOut = await adb(<String>['wait-for-device']);
886 print(waitOut);
887 const RetryOptions retryOptions = RetryOptions(
888 delayFactor: Duration(seconds: 1),
889 maxAttempts: 10,
890 maxDelay: Duration(minutes: 1),
891 );
892 await retryOptions.retry(() async {
893 final String adbShellOut = await adb(<String>['shell', 'getprop sys.boot_completed']);
894 if (adbShellOut != '1') {
895 print('Device not ready.');
896 print(adbShellOut);
897 throw const DeviceException('Phone not ready.');
898 }
899 }, retryIf: (Exception e) => e is DeviceException);
900 print('Done waiting for device.');
901 }
902}
903
904class IosDeviceDiscovery implements DeviceDiscovery {
905 factory IosDeviceDiscovery() {
906 return _instance ??= IosDeviceDiscovery._();
907 }
908
909 IosDeviceDiscovery._();
910
911 static IosDeviceDiscovery? _instance;
912
913 IosDevice? _workingDevice;
914
915 @override
916 Future<IosDevice> get workingDevice async {
917 if (_workingDevice == null) {
918 if (Platform.environment.containsKey(DeviceIdEnvName)) {
919 final String deviceId = Platform.environment[DeviceIdEnvName]!;
920 await chooseWorkingDeviceById(deviceId);
921 return _workingDevice!;
922 }
923 await chooseWorkingDevice();
924 }
925
926 return _workingDevice!;
927 }
928
929 /// Picks a random iOS device out of connected devices and sets it as
930 /// [workingDevice].
931 @override
932 Future<void> chooseWorkingDevice() async {
933 final List<IosDevice> allDevices =
934 (await discoverDevices()).map<IosDevice>((String id) => IosDevice(deviceId: id)).toList();
935
936 if (allDevices.isEmpty) {
937 throw const DeviceException('No iOS devices detected');
938 }
939
940 // TODO(yjbanov): filter out and warn about those with low battery level
941 _workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
942 print('Device chosen: $_workingDevice');
943 }
944
945 @override
946 Future<void> chooseWorkingDeviceById(String deviceId) async {
947 final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
948 if (matchedId != null) {
949 _workingDevice = IosDevice(deviceId: matchedId);
950 print('Choose device by ID: $matchedId');
951 return;
952 }
953 throw DeviceException(
954 'Device with ID $deviceId is not found for operating system: '
955 '$deviceOperatingSystem',
956 );
957 }
958
959 @override
960 Future<List<String>> discoverDevices() async {
961 final List<dynamic> results =
962 json.decode(
963 await eval(path.join(flutterDirectory.path, 'bin', 'flutter'), <String>[
964 'devices',
965 '--machine',
966 '--suppress-analytics',
967 '--device-timeout',
968 '5',
969 ]),
970 )
971 as List<dynamic>;
972
973 // [
974 // {
975 // "name": "Flutter's iPhone",
976 // "id": "00008020-00017DA80CC1002E",
977 // "isSupported": true,
978 // "targetPlatform": "ios",
979 // "emulator": false,
980 // "sdk": "iOS 13.2",
981 // "capabilities": {
982 // "hotReload": true,
983 // "hotRestart": true,
984 // "screenshot": true,
985 // "fastStart": false,
986 // "flutterExit": true,
987 // "hardwareRendering": false,
988 // "startPaused": false
989 // }
990 // }
991 // ]
992
993 final List<String> deviceIds = <String>[];
994
995 for (final dynamic result in results) {
996 final Map<String, dynamic> device = result as Map<String, dynamic>;
997 if (device['targetPlatform'] == 'ios' &&
998 device['id'] != null &&
999 device['emulator'] != true &&
1000 device['isSupported'] == true) {
1001 deviceIds.add(device['id'] as String);
1002 }
1003 }
1004
1005 if (deviceIds.isEmpty) {
1006 throw const DeviceException('No connected physical iOS devices found.');
1007 }
1008 return deviceIds;
1009 }
1010
1011 @override
1012 Future<Map<String, HealthCheckResult>> checkDevices() async {
1013 final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
1014 for (final String deviceId in await discoverDevices()) {
1015 // TODO(ianh): do a more meaningful connectivity check than just recording the ID
1016 results['ios-device-$deviceId'] = HealthCheckResult.success();
1017 }
1018 return results;
1019 }
1020
1021 @override
1022 Future<void> performPreflightTasks() async {
1023 // Currently we do not have preflight tasks for iOS.
1024 }
1025}
1026
1027/// iOS device.
1028class IosDevice extends Device {
1029 IosDevice({required this.deviceId});
1030
1031 @override
1032 final String deviceId;
1033
1034 String get idevicesyslogPath {
1035 return path.join(
1036 flutterDirectory.path,
1037 'bin',
1038 'cache',
1039 'artifacts',
1040 'libimobiledevice',
1041 'idevicesyslog',
1042 );
1043 }
1044
1045 String get dyldLibraryPath {
1046 final List<String> dylibsPaths = <String>[
1047 path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libimobiledevice'),
1048 path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'openssl'),
1049 path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'usbmuxd'),
1050 path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libplist'),
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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com