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 | |
5 | import 'dart:async'; |
6 | import 'dart:convert'; |
7 | import 'dart:io'; |
8 | import 'dart:math' as math; |
9 | |
10 | import 'package:path/path.dart'as path; |
11 | import 'package:retry/retry.dart'; |
12 | |
13 | import 'utils.dart'; |
14 | |
15 | const String DeviceIdEnvName = 'FLUTTER_DEVICELAB_DEVICEID'; |
16 | |
17 | class 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. |
27 | String 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 |
32 | String? _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. |
47 | DeviceDiscovery get devices => DeviceDiscovery(); |
48 | |
49 | /// Device operating system the test is configured to test. |
50 | enum 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. |
63 | DeviceOperatingSystem deviceOperatingSystem = DeviceOperatingSystem.android; |
64 | |
65 | /// Discovers available devices and chooses one to work with. |
66 | abstract 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. |
119 | abstract 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 | |
212 | enum AndroidCPU { arm, arm64 } |
213 | |
214 | class 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 | |
367 | class 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 | |
401 | class 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 | |
435 | class 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 | |
469 | class 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 | |
572 | class 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 | // |
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 | |
904 | class 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. |
1028 | class 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 | |
1153 | class 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 | |
1208 | class 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 | |
1263 | class 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. |
1319 | class 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. |
1376 | String 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 | |
1397 | class 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 | |
1454 | class 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 |
Definitions
- DeviceIdEnvName
- DeviceException
- DeviceException
- toString
- getArtifactPath
- _findMatchId
- devices
- DeviceOperatingSystem
- deviceOperatingSystem
- DeviceDiscovery
- DeviceDiscovery
- chooseWorkingDevice
- chooseWorkingDeviceById
- workingDevice
- discoverDevices
- checkDevices
- performPreflightTasks
- Device
- Device
- deviceId
- toggleFixedPerformanceMode
- isAwake
- isAsleep
- wakeUp
- sendToSleep
- home
- togglePower
- unlock
- reboot
- tap
- getMemoryStats
- logcat
- clearLogs
- canStreamLogs
- startLoggingToSink
- stopLoggingToSink
- stop
- awaitDevice
- uninstallApp
- toString
- AndroidCPU
- AndroidDeviceDiscovery
- AndroidDeviceDiscovery
- _
- workingDevice
- _matchesCPURequirement
- chooseWorkingDevice
- chooseWorkingDeviceById
- discoverDevices
- checkDevices
- performPreflightTasks
- LinuxDeviceDiscovery
- LinuxDeviceDiscovery
- _
- checkDevices
- chooseWorkingDevice
- chooseWorkingDeviceById
- discoverDevices
- performPreflightTasks
- workingDevice
- MacosDeviceDiscovery
- MacosDeviceDiscovery
- _
- checkDevices
- chooseWorkingDevice
- chooseWorkingDeviceById
- discoverDevices
- performPreflightTasks
- workingDevice
- WindowsDeviceDiscovery
- WindowsDeviceDiscovery
- _
- checkDevices
- chooseWorkingDevice
- chooseWorkingDeviceById
- discoverDevices
- performPreflightTasks
- workingDevice
- FuchsiaDeviceDiscovery
- FuchsiaDeviceDiscovery
- _
- _ffx
- workingDevice
- chooseWorkingDevice
- chooseWorkingDeviceById
- discoverDevices
- checkDevices
- performPreflightTasks
- AndroidDevice
- AndroidDevice
- toggleFixedPerformanceMode
- isAwake
- isAsleep
- wakeUp
- sendToSleep
- home
- togglePower
- unlock
- tap
- _getWakefulness
- isArm64
- isArm
- _updateDeviceInfo
- shellExec
- shellEval
- adb
- getMemoryStats
- canStreamLogs
- startLoggingToSink
- stopLoggingToSink
- clearLogs
- logcat
- stop
- toString
- reboot
- awaitDevice
- IosDeviceDiscovery
- IosDeviceDiscovery
- _
- workingDevice
- chooseWorkingDevice
- chooseWorkingDeviceById
- discoverDevices
- checkDevices
- performPreflightTasks
- IosDevice
- IosDevice
- idevicesyslogPath
- dyldLibraryPath
- canStreamLogs
- startLoggingToSink
- stopLoggingToSink
- isAwake
- isAsleep
- wakeUp
- sendToSleep
- home
- togglePower
- unlock
- tap
- getMemoryStats
- logcat
- clearLogs
- stop
- reboot
- awaitDevice
- LinuxDevice
- LinuxDevice
- deviceId
- getMemoryStats
- home
- isAsleep
- isAwake
- logcat
- clearLogs
- reboot
- sendToSleep
- stop
- tap
- togglePower
- unlock
- wakeUp
- awaitDevice
- MacosDevice
- MacosDevice
- deviceId
- getMemoryStats
- home
- isAsleep
- isAwake
- logcat
- clearLogs
- reboot
- sendToSleep
- stop
- tap
- togglePower
- unlock
- wakeUp
- awaitDevice
- WindowsDevice
- WindowsDevice
- deviceId
- getMemoryStats
- home
- isAsleep
- isAwake
- logcat
- clearLogs
- reboot
- sendToSleep
- stop
- tap
- togglePower
- unlock
- wakeUp
- awaitDevice
- FuchsiaDevice
- FuchsiaDevice
- isAwake
- isAsleep
- wakeUp
- sendToSleep
- home
- togglePower
- unlock
- tap
- stop
- getMemoryStats
- logcat
- clearLogs
- reboot
- awaitDevice
- adbPath
- FakeDevice
- FakeDevice
- isAwake
- isAsleep
- wakeUp
- sendToSleep
- home
- togglePower
- unlock
- tap
- getMemoryStats
- logcat
- clearLogs
- stop
- reboot
- awaitDevice
- FakeDeviceDiscovery
- FakeDeviceDiscovery
- _
- workingDevice
- chooseWorkingDevice
- chooseWorkingDeviceById
- discoverDevices
- checkDevices
Learn more about Flutter for embedded and desktop on industrialflutter.com