devices.dart 33.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:convert';
7 8 9 10 11
import 'dart:io';
import 'dart:math' as math;

import 'package:path/path.dart' as path;

12
import '../common.dart';
13 14
import 'utils.dart';

15 16
const String DeviceIdEnvName = 'FLUTTER_DEVICELAB_DEVICEID';

17 18 19 20 21 22 23 24
class DeviceException implements Exception {
  const DeviceException(this.message);

  final String message;

  @override
  String toString() => message == null ? '$DeviceException' : '$DeviceException: $message';
}
25 26 27 28 29 30 31 32 33 34 35

/// Gets the artifact path relative to the current directory.
String getArtifactPath() {
  return path.normalize(
      path.join(
        path.current,
        '../../bin/cache/artifacts',
      )
    );
}

36
/// Return the item is in idList if find a match, otherwise return null
37 38
String? _findMatchId(List<String> idList, String idPattern) {
  String? candidate;
39 40 41 42 43 44 45 46 47 48 49 50
  idPattern = idPattern.toLowerCase();
  for(final String id in idList) {
    if (id.toLowerCase() == idPattern) {
      return id;
    }
    if (id.toLowerCase().startsWith(idPattern)) {
      candidate ??= id;
    }
  }
  return candidate;
}

51
/// The root of the API for controlling devices.
52
DeviceDiscovery get devices => DeviceDiscovery();
53 54

/// Device operating system the test is configured to test.
55
enum DeviceOperatingSystem { android, androidArm, androidArm64 ,ios, fuchsia, fake, windows }
56 57 58 59 60 61 62

/// Device OS to test on.
DeviceOperatingSystem deviceOperatingSystem = DeviceOperatingSystem.android;

/// Discovers available devices and chooses one to work with.
abstract class DeviceDiscovery {
  factory DeviceDiscovery() {
63
    switch (deviceOperatingSystem) {
64
      case DeviceOperatingSystem.android:
65
        return AndroidDeviceDiscovery();
66
      case DeviceOperatingSystem.androidArm:
67
        return AndroidDeviceDiscovery(cpu: AndroidCPU.arm);
68
      case DeviceOperatingSystem.androidArm64:
69
        return AndroidDeviceDiscovery(cpu: AndroidCPU.arm64);
70
      case DeviceOperatingSystem.ios:
71
        return IosDeviceDiscovery();
72 73
      case DeviceOperatingSystem.fuchsia:
        return FuchsiaDeviceDiscovery();
74 75
      case DeviceOperatingSystem.windows:
        return WindowsDeviceDiscovery();
76
      case DeviceOperatingSystem.fake:
77
        print('Looking for fake devices! You should not see this in release builds.');
78
        return FakeDeviceDiscovery();
79 80
    }
  }
81

82 83 84 85 86
  /// Selects a device to work with, load-balancing between devices if more than
  /// one are available.
  ///
  /// Calling this method does not guarantee that the same device will be
  /// returned. For such behavior see [workingDevice].
87
  Future<void> chooseWorkingDevice();
88

89
  /// Selects a device to work with by device ID.
90 91
  Future<void> chooseWorkingDeviceById(String deviceId);

92 93 94 95
  /// A device to work with.
  ///
  /// Returns the same device when called repeatedly (unlike
  /// [chooseWorkingDevice]). This is useful when you need to perform multiple
96
  /// operations on one.
97
  Future<Device> get workingDevice;
98

99 100
  /// Lists all available devices' IDs.
  Future<List<String>> discoverDevices();
101

102 103
  /// Checks the health of the available devices.
  Future<Map<String, HealthCheckResult>> checkDevices();
104

105
  /// Prepares the system to run tasks.
106
  Future<void> performPreflightTasks();
107 108
}

109 110
/// A proxy for one specific device.
abstract class Device {
111 112 113
  // Const constructor so subclasses may be const.
  const Device();

114 115
  /// A unique device identifier.
  String get deviceId;
116

117 118
  /// Whether the device is awake.
  Future<bool> isAwake();
119

120 121
  /// Whether the device is asleep.
  Future<bool> isAsleep();
122

123
  /// Wake up the device if it is not awake.
124
  Future<void> wakeUp();
125

126
  /// Send the device to sleep mode.
127
  Future<void> sendToSleep();
128

129 130 131
  /// Emulates pressing the home button.
  Future<void> home();

132
  /// Emulates pressing the power button, toggling the device's on/off state.
133
  Future<void> togglePower();
134

135 136 137
  /// Unlocks the device.
  ///
  /// Assumes the device doesn't have a secure unlock pattern.
138
  Future<void> unlock();
139

140 141 142
  /// Attempt to reboot the phone, if possible.
  Future<void> reboot();

143
  /// Emulate a tap on the touch screen.
144
  Future<void> tap(int x, int y);
145

146 147 148
  /// Read memory statistics for a process.
  Future<Map<String, dynamic>> getMemoryStats(String packageName);

149 150 151 152 153 154
  /// Stream the system log from the device.
  ///
  /// Flutter applications' `print` statements end up in this log
  /// with some prefix.
  Stream<String> get logcat;

155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
  /// Whether this device supports calls to [startLoggingToSink]
  /// and [stopLoggingToSink].
  bool get canStreamLogs => false;

  /// Starts logging to an [IOSink].
  ///
  /// If `clear` is set to true, the log will be cleared before starting. This
  /// is not supported on all platforms.
  Future<void> startLoggingToSink(IOSink sink, {bool clear = true}) {
    throw UnimplementedError();
  }

  /// Stops logging that was started by [startLoggingToSink].
  Future<void> stopLoggingToSink() {
    throw UnimplementedError();
  }

172
  /// Stop a process.
173
  Future<void> stop(String packageName);
174 175 176 177 178

  @override
  String toString() {
    return 'device: $deviceId';
  }
179
}
180

181
enum AndroidCPU {
182
  arm,
183 184 185
  arm64,
}

186
class AndroidDeviceDiscovery implements DeviceDiscovery {
187
  factory AndroidDeviceDiscovery({AndroidCPU? cpu}) {
188
    return _instance ??= AndroidDeviceDiscovery._(cpu);
189 190
  }

191 192
  AndroidDeviceDiscovery._(this.cpu);

193
  final AndroidCPU? cpu;
194

195 196 197
  // Parses information about a device. Example:
  //
  // 015d172c98400a03       device usb:340787200X product:nakasi model:Nexus_7 device:grouper
198
  static final RegExp _kDeviceRegex = RegExp(r'^(\S+)\s+(\S+)(.*)');
199

200
  static AndroidDeviceDiscovery? _instance;
201

202
  AndroidDevice? _workingDevice;
203 204 205 206

  @override
  Future<AndroidDevice> get workingDevice async {
    if (_workingDevice == null) {
207
      if (Platform.environment.containsKey(DeviceIdEnvName)) {
208
        final String deviceId = Platform.environment[DeviceIdEnvName]!;
209
        await chooseWorkingDeviceById(deviceId);
210
        return _workingDevice!;
211
      }
212
      await chooseWorkingDevice();
213
    }
214

215
    return _workingDevice!;
216 217
  }

218 219
  Future<bool> _matchesCPURequirement(AndroidDevice device) async {
    switch (cpu) {
220 221
      case null:
        return true;
222
      case AndroidCPU.arm64:
223
        return device.isArm64();
224
      case AndroidCPU.arm:
225
        return device.isArm();
226 227 228
    }
  }

229 230 231
  /// Picks a random Android device out of connected devices and sets it as
  /// [workingDevice].
  @override
232
  Future<void> chooseWorkingDevice() async {
233 234
    final List<AndroidDevice> allDevices = (await discoverDevices())
      .map<AndroidDevice>((String id) => AndroidDevice(deviceId: id))
235 236 237
      .toList();

    if (allDevices.isEmpty)
238
      throw const DeviceException('No Android devices detected');
239

240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
    if (cpu != null) {
      for (final AndroidDevice device in allDevices) {
        if (await _matchesCPURequirement(device)) {
          _workingDevice = device;
          break;
        }
      }

    } else {
      // TODO(yjbanov): filter out and warn about those with low battery level
      _workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
    }

    if (_workingDevice == null)
      throw const DeviceException('Cannot find a suitable Android device');

256
    print('Device chosen: $_workingDevice');
257 258
  }

259 260
  @override
  Future<void> chooseWorkingDeviceById(String deviceId) async {
261
    final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
262 263
    if (matchedId != null) {
      _workingDevice = AndroidDevice(deviceId: matchedId);
264
      if (cpu != null) {
265
        if (!await _matchesCPURequirement(_workingDevice!)) {
266 267 268
          throw DeviceException('The selected device $matchedId does not match the cpu requirement');
        }
      }
269 270 271 272 273 274 275 276 277
      print('Choose device by ID: $matchedId');
      return;
    }
    throw DeviceException(
      'Device with ID $deviceId is not found for operating system: '
      '$deviceOperatingSystem'
      );
  }

278 279
  @override
  Future<List<String>> discoverDevices() async {
280
    final List<String> output = (await eval(adbPath, <String>['devices', '-l']))
281
        .trim().split('\n');
282
    final List<String> results = <String>[];
283
    for (final String line in output) {
284
      // Skip lines like: * daemon started successfully *
285 286
      if (line.startsWith('* daemon '))
        continue;
287

288 289
      if (line.startsWith('List of devices'))
        continue;
290 291

      if (_kDeviceRegex.hasMatch(line)) {
292
        final Match match = _kDeviceRegex.firstMatch(line)!;
293

294 295
        final String deviceID = match[1]!;
        final String deviceState = match[2]!;
296 297 298 299 300

        if (!const <String>['unauthorized', 'offline'].contains(deviceState)) {
          results.add(deviceID);
        }
      } else {
301
        throw FormatException('Failed to parse device from adb output: "$line"');
302 303 304 305 306 307
      }
    }

    return results;
  }

308 309
  @override
  Future<Map<String, HealthCheckResult>> checkDevices() async {
310
    final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
311
    for (final String deviceId in await discoverDevices()) {
312
      try {
313
        final AndroidDevice device = AndroidDevice(deviceId: deviceId);
314 315 316
        // Just a smoke test that we can read wakefulness state
        // TODO(yjbanov): check battery level
        await device._getWakefulness();
317
        results['android-device-$deviceId'] = HealthCheckResult.success();
318
      } on Exception catch (e, s) {
319
        results['android-device-$deviceId'] = HealthCheckResult.error(e, s);
320 321 322 323 324 325
      }
    }
    return results;
  }

  @override
326
  Future<void> performPreflightTasks() async {
327 328 329 330 331 332
    // Kills the `adb` server causing it to start a new instance upon next
    // command.
    //
    // Restarting `adb` helps with keeping device connections alive. When `adb`
    // runs non-stop for too long it loses connections to devices. There may be
    // a better method, but so far that's the best one I've found.
333
    await exec(adbPath, <String>['kill-server']);
334 335 336
  }
}

337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
class WindowsDeviceDiscovery implements DeviceDiscovery {
  factory WindowsDeviceDiscovery() {
    return _instance ??= WindowsDeviceDiscovery._();
  }

  WindowsDeviceDiscovery._();

  static WindowsDeviceDiscovery? _instance;

  static const WindowsDevice _device = WindowsDevice();

  @override
  Future<Map<String, HealthCheckResult>> checkDevices() async {
    return <String, HealthCheckResult>{};
  }

  @override
  Future<void> chooseWorkingDevice() async { }

  @override
  Future<void> chooseWorkingDeviceById(String deviceId) async { }

  @override
  Future<List<String>> discoverDevices() async {
    return <String>['windows'];
  }

  @override
  Future<void> performPreflightTasks() async { }

  @override
  Future<Device> get workingDevice  async => _device;

}

372 373 374 375 376 377 378
class FuchsiaDeviceDiscovery implements DeviceDiscovery {
  factory FuchsiaDeviceDiscovery() {
    return _instance ??= FuchsiaDeviceDiscovery._();
  }

  FuchsiaDeviceDiscovery._();

379
  static FuchsiaDeviceDiscovery? _instance;
380

381
  FuchsiaDevice? _workingDevice;
382

383
  String get _ffx {
384 385 386
    final String ffx = path.join(getArtifactPath(), 'fuchsia', 'tools','x64', 'ffx');
    if (!File(ffx).existsSync()) {
      throw FileSystemException("Couldn't find ffx at location $ffx");
387
    }
388
    return ffx;
389
  }
390 391 392 393

  @override
  Future<FuchsiaDevice> get workingDevice async {
    if (_workingDevice == null) {
394
      if (Platform.environment.containsKey(DeviceIdEnvName)) {
395
        final String deviceId = Platform.environment[DeviceIdEnvName]!;
396
        await chooseWorkingDeviceById(deviceId);
397
        return _workingDevice!;
398
      }
399 400
      await chooseWorkingDevice();
    }
401
    return _workingDevice!;
402 403 404 405 406 407 408 409 410 411
  }

  /// Picks the first connected Fuchsia device.
  @override
  Future<void> chooseWorkingDevice() async {
    final List<FuchsiaDevice> allDevices = (await discoverDevices())
      .map<FuchsiaDevice>((String id) => FuchsiaDevice(deviceId: id))
      .toList();

    if (allDevices.isEmpty) {
412
      throw const DeviceException('No Fuchsia devices detected');
413 414
    }
    _workingDevice = allDevices.first;
415
    print('Device chosen: $_workingDevice');
416 417
  }

418 419
  @override
  Future<void> chooseWorkingDeviceById(String deviceId) async {
420 421
    final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
    if (matchedId != null) {
422 423 424 425 426 427 428 429 430 431
      _workingDevice = FuchsiaDevice(deviceId: matchedId);
      print('Choose device by ID: $matchedId');
      return;
    }
    throw DeviceException(
      'Device with ID $deviceId is not found for operating system: '
      '$deviceOperatingSystem'
      );
  }

432 433
  @override
  Future<List<String>> discoverDevices() async {
434
    final List<String> output = (await eval(_ffx, <String>['target', 'list', '--format', 's']))
435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452
      .trim()
      .split('\n');

    final List<String> devices = <String>[];
    for (final String line in output) {
      final List<String> parts = line.split(' ');
      assert(parts.length == 2);
      devices.add(parts.last); // The device id.
    }
    return devices;
  }

  @override
  Future<Map<String, HealthCheckResult>> checkDevices() async {
    final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
    for (final String deviceId in await discoverDevices()) {
      try {
        final int resolveResult = await exec(
453
          _ffx,
454
          <String>[
455 456 457 458
            'target',
            'list',
            '--format',
            'a',
459
            deviceId,
460
          ]
461
        );
462
        if (resolveResult == 0) {
463 464 465 466
          results['fuchsia-device-$deviceId'] = HealthCheckResult.success();
        } else {
          results['fuchsia-device-$deviceId'] = HealthCheckResult.failure('Cannot resolve device $deviceId');
        }
467
      } on Exception catch (error, stacktrace) {
468 469 470 471 472 473 474 475 476 477
        results['fuchsia-device-$deviceId'] = HealthCheckResult.error(error, stacktrace);
      }
    }
    return results;
  }

  @override
  Future<void> performPreflightTasks() async {}
}

478
class AndroidDevice extends Device {
479
  AndroidDevice({required this.deviceId}) {
480 481
    _updateDeviceInfo();
  }
482 483 484

  @override
  final String deviceId;
485
  String deviceInfo = '';
486
  int apiLevel = 0;
487

488
  /// Whether the device is awake.
489
  @override
490 491 492 493 494
  Future<bool> isAwake() async {
    return await _getWakefulness() == 'Awake';
  }

  /// Whether the device is asleep.
495
  @override
496 497 498 499 500
  Future<bool> isAsleep() async {
    return await _getWakefulness() == 'Asleep';
  }

  /// Wake up the device if it is not awake using [togglePower].
501
  @override
502
  Future<void> wakeUp() async {
503 504
    if (!(await isAwake()))
      await togglePower();
505 506 507
  }

  /// Send the device to sleep mode if it is not asleep using [togglePower].
508
  @override
509
  Future<void> sendToSleep() async {
510 511
    if (!(await isAsleep()))
      await togglePower();
512 513
  }

514 515 516 517 518 519
  /// Sends `KEYCODE_HOME` (3), which causes the device to go to the home screen.
  @override
  Future<void> home() async {
    await shellExec('input', const <String>['keyevent', '3']);
  }

520 521
  /// Sends `KEYCODE_POWER` (26), which causes the device to toggle its mode
  /// between awake and asleep.
522
  @override
523
  Future<void> togglePower() async {
524 525 526 527 528 529
    await shellExec('input', const <String>['keyevent', '26']);
  }

  /// Unlocks the device by sending `KEYCODE_MENU` (82).
  ///
  /// This only works when the device doesn't have a secure unlock pattern.
530
  @override
531
  Future<void> unlock() async {
532 533 534 535
    await wakeUp();
    await shellExec('input', const <String>['keyevent', '82']);
  }

536
  @override
537
  Future<void> tap(int x, int y) async {
538 539 540
    await shellExec('input', <String>['tap', '$x', '$y']);
  }

541 542 543 544
  /// Retrieves device's wakefulness state.
  ///
  /// See: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/PowerManagerInternal.java
  Future<String> _getWakefulness() async {
545
    final String powerInfo = await shellEval('dumpsys', <String>['power']);
546 547 548 549
    // A motoG4 phone returns `mWakefulness=Awake`.
    // A Samsung phone returns `getWakefullnessLocked()=Awake`.
    final RegExp wakefulnessRegexp = RegExp(r'.*(mWakefulness=|getWakefulnessLocked\(\)=).*');
    final String wakefulness = grep(wakefulnessRegexp, from: powerInfo).single.split('=')[1].trim();
550 551 552
    return wakefulness;
  }

553 554 555 556 557
  Future<bool> isArm64() async {
    final String cpuInfo = await shellEval('getprop', const <String>['ro.product.cpu.abi']);
    return cpuInfo.contains('arm64');
  }

558 559 560 561 562
  Future<bool> isArm() async {
    final String cpuInfo = await shellEval('getprop', const <String>['ro.product.cpu.abi']);
    return cpuInfo.contains('armeabi');
  }

563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579
  Future<void> _updateDeviceInfo() async {
    String info;
    try {
      info = await shellEval(
        'getprop',
        <String>[
          'ro.bootimage.build.fingerprint', ';',
          'getprop', 'ro.build.version.release', ';',
          'getprop', 'ro.build.version.sdk',
        ],
        silent: true,
      );
    } on IOException {
      info = '';
    }
    final List<String> list = info.split('\n');
    if (list.length == 3) {
580 581
      apiLevel = int.parse(list[2]);
      deviceInfo = 'fingerprint: ${list[0]} os: ${list[1]}  api-level: $apiLevel';
582
    } else {
583
      apiLevel = 0;
584 585 586 587
      deviceInfo = '';
    }
  }

588
  /// Executes [command] on `adb shell`.
589
  Future<void> shellExec(String command, List<String> arguments, { Map<String, String>? environment, bool silent = false }) async {
590
    await adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
591 592 593
  }

  /// Executes [command] on `adb shell` and returns its standard output as a [String].
594
  Future<String> shellEval(String command, List<String> arguments, { Map<String, String>? environment, bool silent = false }) {
595
    return adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
596 597 598
  }

  /// Runs `adb` with the given [arguments], selecting this device.
599 600
  Future<String> adb(
      List<String> arguments, {
601
      Map<String, String>? environment,
602 603 604 605 606 607 608 609 610
      bool silent = false,
    }) {
    return eval(
      adbPath,
      <String>['-s', deviceId, ...arguments],
      environment: environment,
      printStdout: !silent,
      printStderr: !silent,
    );
611
  }
612 613 614

  @override
  Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
615
    final String meminfo = await shellEval('dumpsys', <String>['meminfo', packageName]);
616
    final Match? match = RegExp(r'TOTAL\s+(\d+)').firstMatch(meminfo);
617
    assert(match != null, 'could not parse dumpsys meminfo output');
618
    return <String, dynamic>{
619
      'total_kb': int.parse(match!.group(1)!),
620 621 622
    };
  }

623 624 625
  @override
  bool get canStreamLogs => true;

626 627
  bool _abortedLogging = false;
  Process? _loggingProcess;
628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643

  @override
  Future<void> startLoggingToSink(IOSink sink, {bool clear = true}) async {
    if (clear) {
      await adb(<String>['logcat', '--clear'], silent: true);
    }
    _loggingProcess = await startProcess(
      adbPath,
      // Make logcat less chatty by filtering down to just ActivityManager
      // (to let us know when app starts), flutter (needed by tests to see
      // log output), and fatal messages (hopefully catches tombstones).
      // For local testing, this can just be:
      //   <String>['-s', deviceId, 'logcat']
      // to view the whole log, or just run logcat alongside this.
      <String>['-s', deviceId, 'logcat', 'ActivityManager:I', 'flutter:V', '*:F'],
    );
644
    _loggingProcess!.stdout
645 646 647 648
      .transform<String>(const Utf8Decoder(allowMalformed: true))
      .listen((String line) {
        sink.write(line);
      });
649
    _loggingProcess!.stderr
650 651 652 653
      .transform<String>(const Utf8Decoder(allowMalformed: true))
      .listen((String line) {
        sink.write(line);
      });
654
    unawaited(_loggingProcess!.exitCode.then<void>((int exitCode) {
655 656 657 658 659 660 661 662
      if (!_abortedLogging) {
        sink.writeln('adb logcat failed with exit code $exitCode.\n');
      }
    }));
  }

  @override
  Future<void> stopLoggingToSink() async {
663 664
    if (_loggingProcess != null) {
      _abortedLogging = true;
665 666
      _loggingProcess!.kill();
      await _loggingProcess!.exitCode;
667
    }
668 669
  }

670 671
  @override
  Stream<String> get logcat {
672 673 674 675
    final Completer<void> stdoutDone = Completer<void>();
    final Completer<void> stderrDone = Completer<void>();
    final Completer<void> processDone = Completer<void>();
    final Completer<void> abort = Completer<void>();
676
    bool aborted = false;
677
    late final StreamController<String> stream;
678
    stream = StreamController<String>(
679
      onListen: () async {
680
        await adb(<String>['logcat', '-c']);
681 682 683 684 685 686 687 688 689 690
        final Process process = await startProcess(
          adbPath,
          // Make logcat less chatty by filtering down to just ActivityManager
          // (to let us know when app starts), flutter (needed by tests to see
          // log output), and fatal messages (hopefully catches tombstones).
          // For local testing, this can just be:
          //   <String>['-s', deviceId, 'logcat']
          // to view the whole log, or just run logcat alongside this.
          <String>['-s', deviceId, 'logcat', 'ActivityManager:I', 'flutter:V', '*:F'],
        );
691
        process.stdout
692 693
          .transform<String>(utf8.decoder)
          .transform<String>(const LineSplitter())
694 695
          .listen((String line) {
            print('adb logcat: $line');
696 697 698
            if (!stream.isClosed) {
              stream.sink.add(line);
            }
699 700
          }, onDone: () { stdoutDone.complete(); });
        process.stderr
701 702
          .transform<String>(utf8.decoder)
          .transform<String>(const LineSplitter())
703 704 705
          .listen((String line) {
            print('adb logcat stderr: $line');
          }, onDone: () { stderrDone.complete(); });
706
        unawaited(process.exitCode.then<void>((int exitCode) {
707 708
          print('adb logcat process terminated with exit code $exitCode');
          if (!aborted) {
709
            stream.addError(BuildFailedError('adb logcat failed with exit code $exitCode.\n'));
710 711
            processDone.complete();
          }
712
        }));
713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737
        await Future.any<dynamic>(<Future<dynamic>>[
          Future.wait<void>(<Future<void>>[
            stdoutDone.future,
            stderrDone.future,
            processDone.future,
          ]),
          abort.future,
        ]);
        aborted = true;
        print('terminating adb logcat');
        process.kill();
        print('closing logcat stream');
        await stream.close();
      },
      onCancel: () {
        if (!aborted) {
          print('adb logcat aborted');
          aborted = true;
          abort.complete();
        }
      },
    );
    return stream.stream;
  }

738
  @override
739
  Future<void> stop(String packageName) async {
740 741
    return shellExec('am', <String>['force-stop', packageName]);
  }
742 743 744 745 746

  @override
  String toString() {
    return '$deviceId $deviceInfo';
  }
747 748 749 750 751

  @override
  Future<void> reboot() {
    return adb(<String>['reboot']);
  }
752 753 754 755
}

class IosDeviceDiscovery implements DeviceDiscovery {
  factory IosDeviceDiscovery() {
756
    return _instance ??= IosDeviceDiscovery._();
757 758 759 760
  }

  IosDeviceDiscovery._();

761
  static IosDeviceDiscovery? _instance;
762

763
  IosDevice? _workingDevice;
764 765 766 767

  @override
  Future<IosDevice> get workingDevice async {
    if (_workingDevice == null) {
768
      if (Platform.environment.containsKey(DeviceIdEnvName)) {
769
        final String deviceId = Platform.environment[DeviceIdEnvName]!;
770
        await chooseWorkingDeviceById(deviceId);
771
        return _workingDevice!;
772
      }
773 774 775
      await chooseWorkingDevice();
    }

776
    return _workingDevice!;
777 778 779 780 781
  }

  /// Picks a random iOS device out of connected devices and sets it as
  /// [workingDevice].
  @override
782
  Future<void> chooseWorkingDevice() async {
783
    final List<IosDevice> allDevices = (await discoverDevices())
784
      .map<IosDevice>((String id) => IosDevice(deviceId: id))
785 786
      .toList();

787
    if (allDevices.isEmpty)
788
      throw const DeviceException('No iOS devices detected');
789 790

    // TODO(yjbanov): filter out and warn about those with low battery level
791
    _workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
792
    print('Device chosen: $_workingDevice');
793 794
  }

795 796
  @override
  Future<void> chooseWorkingDeviceById(String deviceId) async {
797
    final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
798 799 800 801 802 803 804 805 806 807 808
    if (matchedId != null) {
      _workingDevice = IosDevice(deviceId: matchedId);
      print('Choose device by ID: $matchedId');
      return;
    }
    throw DeviceException(
      'Device with ID $deviceId is not found for operating system: '
      '$deviceOperatingSystem'
      );
  }

809 810
  @override
  Future<List<String>> discoverDevices() async {
811 812
    final List<dynamic> results = json.decode(await eval(
      path.join(flutterDirectory.path, 'bin', 'flutter'),
813
      <String>['devices', '--machine', '--suppress-analytics', '--device-timeout', '5'],
814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848
    )) as List<dynamic>;

    // [
    //   {
    //     "name": "Flutter's iPhone",
    //     "id": "00008020-00017DA80CC1002E",
    //     "isSupported": true,
    //     "targetPlatform": "ios",
    //     "emulator": false,
    //     "sdk": "iOS 13.2",
    //     "capabilities": {
    //       "hotReload": true,
    //       "hotRestart": true,
    //       "screenshot": true,
    //       "fastStart": false,
    //       "flutterExit": true,
    //       "hardwareRendering": false,
    //       "startPaused": false
    //     }
    //   }
    // ]

    final List<String> deviceIds = <String>[];

    for (final dynamic result in results) {
      final Map<String, dynamic> device = result as Map<String, dynamic>;
      if (device['targetPlatform'] == 'ios' &&
          device['id'] != null &&
          device['emulator'] != true &&
          device['isSupported'] == true) {
        deviceIds.add(device['id'] as String);
      }
    }

    if (deviceIds.isEmpty) {
849
      throw const DeviceException('No connected physical iOS devices found.');
850 851
    }
    return deviceIds;
852
  }
853 854 855

  @override
  Future<Map<String, HealthCheckResult>> checkDevices() async {
856
    final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
857
    for (final String deviceId in await discoverDevices()) {
858
      // TODO(ianh): do a more meaningful connectivity check than just recording the ID
859
      results['ios-device-$deviceId'] = HealthCheckResult.success();
860 861 862 863 864
    }
    return results;
  }

  @override
865
  Future<void> performPreflightTasks() async {
866 867 868 869 870
    // Currently we do not have preflight tasks for iOS.
  }
}

/// iOS device.
871
class IosDevice extends Device {
872
  IosDevice({ required this.deviceId });
873 874 875 876

  @override
  final String deviceId;

877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893
  String get idevicesyslogPath {
    return path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libimobiledevice', 'idevicesyslog');
  }

  String get dyldLibraryPath {
    final List<String> dylibsPaths = <String>[
      path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libimobiledevice'),
      path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'openssl'),
      path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'usbmuxd'),
      path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libplist'),
    ];
    return dylibsPaths.join(':');
  }

  @override
  bool get canStreamLogs => true;

894 895
  bool _abortedLogging = false;
  Process? _loggingProcess;
896 897 898 899 900 901 902 903 904 905 906

  @override
  Future<void> startLoggingToSink(IOSink sink, {bool clear = true}) async {
    // Clear is not supported.
    _loggingProcess = await startProcess(
      idevicesyslogPath,
      <String>['-u', deviceId, '--quiet'],
      environment: <String, String>{
        'DYLD_LIBRARY_PATH': dyldLibraryPath,
      },
    );
907
    _loggingProcess!.stdout
908 909 910 911
      .transform<String>(const Utf8Decoder(allowMalformed: true))
      .listen((String line) {
        sink.write(line);
      });
912
    _loggingProcess!.stderr
913 914 915 916
      .transform<String>(const Utf8Decoder(allowMalformed: true))
      .listen((String line) {
        sink.write(line);
      });
917
    unawaited(_loggingProcess!.exitCode.then<void>((int exitCode) {
918 919 920 921 922 923 924 925
      if (!_abortedLogging) {
        sink.writeln('idevicesyslog failed with exit code $exitCode.\n');
      }
    }));
  }

  @override
  Future<void> stopLoggingToSink() async {
926 927
    if (_loggingProcess != null) {
      _abortedLogging = true;
928 929
      _loggingProcess!.kill();
      await _loggingProcess!.exitCode;
930
    }
931 932
  }

933 934 935 936 937 938 939 940 941 942 943 944
  // The methods below are stubs for now. They will need to be expanded.
  // We currently do not have a way to lock/unlock iOS devices. So we assume the
  // devices are already unlocked. For now we'll just keep them at minimum
  // screen brightness so they don't drain battery too fast.

  @override
  Future<bool> isAwake() async => true;

  @override
  Future<bool> isAsleep() async => false;

  @override
945
  Future<void> wakeUp() async {}
946 947

  @override
948
  Future<void> sendToSleep() async {}
949

950 951 952
  @override
  Future<void> home() async {}

953
  @override
954
  Future<void> togglePower() async {}
955 956

  @override
957
  Future<void> unlock() async {}
958

959
  @override
960
  Future<void> tap(int x, int y) async {
961
    throw UnimplementedError();
962 963
  }

964 965
  @override
  Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
966
    throw UnimplementedError();
967 968
  }

969 970
  @override
  Stream<String> get logcat {
971
    throw UnimplementedError();
972 973
  }

974
  @override
975
  Future<void> stop(String packageName) async {}
976 977 978

  @override
  Future<void> reboot() {
979
    return Process.run('idevicediagnostics', <String>['restart', '-u', deviceId]);
980
  }
981 982
}

983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031
class WindowsDevice extends Device {
  const WindowsDevice();

  @override
  String get deviceId => 'windows';

  @override
  Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
    return <String, dynamic>{};
  }

  @override
  Future<void> home() async { }

  @override
  Future<bool> isAsleep() async {
    return false;
  }

  @override
  Future<bool> isAwake() async {
    return true;
  }

  @override
  Stream<String> get logcat => const Stream<String>.empty();

  @override
  Future<void> reboot() async { }

  @override
  Future<void> sendToSleep() async { }

  @override
  Future<void> stop(String packageName) async { }

  @override
  Future<void> tap(int x, int y) async { }

  @override
  Future<void> togglePower() async { }

  @override
  Future<void> unlock() async { }

  @override
  Future<void> wakeUp() async { }
}

1032
/// Fuchsia device.
1033
class FuchsiaDevice extends Device {
1034
  const FuchsiaDevice({ required this.deviceId });
1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051

  @override
  final String deviceId;

  // TODO(egarciad): Implement these for Fuchsia.
  @override
  Future<bool> isAwake() async => true;

  @override
  Future<bool> isAsleep() async => false;

  @override
  Future<void> wakeUp() async {}

  @override
  Future<void> sendToSleep() async {}

1052 1053 1054
  @override
  Future<void> home() async {}

1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068
  @override
  Future<void> togglePower() async {}

  @override
  Future<void> unlock() async {}

  @override
  Future<void> tap(int x, int y) async {}

  @override
  Future<void> stop(String packageName) async {}

  @override
  Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
1069
    throw UnimplementedError();
1070 1071 1072 1073
  }

  @override
  Stream<String> get logcat {
1074
    throw UnimplementedError();
1075
  }
1076 1077 1078 1079 1080

  @override
  Future<void> reboot() async {
    // Unsupported.
  }
1081 1082
}

1083 1084
/// Path to the `adb` executable.
String get adbPath {
1085
  final String? androidHome = Platform.environment['ANDROID_HOME'] ?? Platform.environment['ANDROID_SDK_ROOT'];
1086

1087 1088
  if (androidHome == null) {
    throw const DeviceException(
1089 1090
      'The ANDROID_SDK_ROOT environment variable is '
      'missing. The variable must point to the Android '
1091 1092 1093
      'SDK directory containing platform-tools.'
    );
  }
1094

1095
  final String adbPath = path.join(androidHome, 'platform-tools/adb');
1096

1097
  if (!canRun(adbPath))
1098
    throw DeviceException('adb not found at: $adbPath');
1099

1100
  return path.absolute(adbPath);
1101
}
1102 1103

class FakeDevice extends Device {
1104
  const FakeDevice({ required this.deviceId });
1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120

  @override
  final String deviceId;

  @override
  Future<bool> isAwake() async => true;

  @override
  Future<bool> isAsleep() async => false;

  @override
  Future<void> wakeUp() async {}

  @override
  Future<void> sendToSleep() async {}

1121 1122 1123
  @override
  Future<void> home() async {}

1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146
  @override
  Future<void> togglePower() async {}

  @override
  Future<void> unlock() async {}

  @override
  Future<void> tap(int x, int y) async {
    throw UnimplementedError();
  }

  @override
  Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
    throw UnimplementedError();
  }

  @override
  Stream<String> get logcat {
    throw UnimplementedError();
  }

  @override
  Future<void> stop(String packageName) async {}
1147 1148 1149 1150 1151

  @override
  Future<void> reboot() async {
    // Unsupported.
  }
1152 1153 1154 1155 1156 1157 1158 1159 1160
}

class FakeDeviceDiscovery implements DeviceDiscovery {
  factory FakeDeviceDiscovery() {
    return _instance ??= FakeDeviceDiscovery._();
  }

  FakeDeviceDiscovery._();

1161
  static FakeDeviceDiscovery? _instance;
1162

1163
  FakeDevice? _workingDevice;
1164 1165 1166 1167 1168

  @override
  Future<FakeDevice> get workingDevice async {
    if (_workingDevice == null) {
      if (Platform.environment.containsKey(DeviceIdEnvName)) {
1169
        final String deviceId = Platform.environment[DeviceIdEnvName]!;
1170
        await chooseWorkingDeviceById(deviceId);
1171
        return _workingDevice!;
1172 1173 1174 1175
      }
      await chooseWorkingDevice();
    }

1176
    return _workingDevice!;
1177 1178 1179 1180 1181 1182 1183 1184 1185 1186
  }

  /// The Fake is only available for by ID device discovery.
  @override
  Future<void> chooseWorkingDevice() async {
    throw const DeviceException('No fake devices detected');
  }

  @override
  Future<void> chooseWorkingDeviceById(String deviceId) async {
1187
    final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216
    if (matchedId != null) {
      _workingDevice = FakeDevice(deviceId: matchedId);
      print('Choose device by ID: $matchedId');
      return;
    }
    throw DeviceException(
      'Device with ID $deviceId is not found for operating system: '
      '$deviceOperatingSystem'
      );
  }

  @override
  Future<List<String>> discoverDevices() async {
    return <String>['FAKE_SUCCESS', 'THIS_IS_A_FAKE'];
  }

  @override
  Future<Map<String, HealthCheckResult>> checkDevices() async {
    final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
    for (final String deviceId in await discoverDevices()) {
      results['fake-device-$deviceId'] = HealthCheckResult.success();
    }
    return results;
  }

  @override
  Future<void> performPreflightTasks() async {
  }
}