adb.dart 26 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
import 'dart:io';
import 'dart:math' as math;

10
import 'package:meta/meta.dart';
11 12 13 14
import 'package:path/path.dart' as path;

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

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, ios, fuchsia, fake }
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.ios:
67
        return IosDeviceDiscovery();
68 69
      case DeviceOperatingSystem.fuchsia:
        return FuchsiaDeviceDiscovery();
70 71 72 73
      case DeviceOperatingSystem.fake:
        print('Looking for fake devices!'
              'You should not see this in release builds.');
        return FakeDeviceDiscovery();
74
      default:
75
        throw DeviceException('Unsupported device operating system: $deviceOperatingSystem');
76 77
    }
  }
78

79 80 81 82 83
  /// 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].
84
  Future<void> chooseWorkingDevice();
85

86 87 88
  /// Select the device with ID strati with deviceId, return the device.
  Future<void> chooseWorkingDeviceById(String deviceId);

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

96 97
  /// Lists all available devices' IDs.
  Future<List<String>> discoverDevices();
98

99 100
  /// Checks the health of the available devices.
  Future<Map<String, HealthCheckResult>> checkDevices();
101

102
  /// Prepares the system to run tasks.
103
  Future<void> performPreflightTasks();
104 105
}

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

111 112
  /// A unique device identifier.
  String get deviceId;
113

114 115
  /// Whether the device is awake.
  Future<bool> isAwake();
116

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

120
  /// Wake up the device if it is not awake.
121
  Future<void> wakeUp();
122

123
  /// Send the device to sleep mode.
124
  Future<void> sendToSleep();
125

126
  /// Emulates pressing the power button, toggling the device's on/off state.
127
  Future<void> togglePower();
128

129 130 131
  /// Unlocks the device.
  ///
  /// Assumes the device doesn't have a secure unlock pattern.
132
  Future<void> unlock();
133

134
  /// Emulate a tap on the touch screen.
135
  Future<void> tap(int x, int y);
136

137 138 139
  /// Read memory statistics for a process.
  Future<Map<String, dynamic>> getMemoryStats(String packageName);

140 141 142 143 144 145
  /// Stream the system log from the device.
  ///
  /// Flutter applications' `print` statements end up in this log
  /// with some prefix.
  Stream<String> get logcat;

146
  /// Stop a process.
147
  Future<void> stop(String packageName);
148 149 150 151 152

  @override
  String toString() {
    return 'device: $deviceId';
  }
153
}
154

155
class AndroidDeviceDiscovery implements DeviceDiscovery {
156 157 158 159 160 161
  factory AndroidDeviceDiscovery() {
    return _instance ??= AndroidDeviceDiscovery._();
  }

  AndroidDeviceDiscovery._();

162 163 164
  // Parses information about a device. Example:
  //
  // 015d172c98400a03       device usb:340787200X product:nakasi model:Nexus_7 device:grouper
165
  static final RegExp _kDeviceRegex = RegExp(r'^(\S+)\s+(\S+)(.*)');
166

167 168 169 170 171 172 173
  static AndroidDeviceDiscovery _instance;

  AndroidDevice _workingDevice;

  @override
  Future<AndroidDevice> get workingDevice async {
    if (_workingDevice == null) {
174 175 176 177 178
      if (Platform.environment.containsKey(DeviceIdEnvName)) {
        final String deviceId = Platform.environment[DeviceIdEnvName];
        await chooseWorkingDeviceById(deviceId);
        return _workingDevice;
      }
179
      await chooseWorkingDevice();
180
    }
181 182

    return _workingDevice;
183 184
  }

185 186 187
  /// Picks a random Android device out of connected devices and sets it as
  /// [workingDevice].
  @override
188
  Future<void> chooseWorkingDevice() async {
189 190
    final List<AndroidDevice> allDevices = (await discoverDevices())
      .map<AndroidDevice>((String id) => AndroidDevice(deviceId: id))
191 192 193
      .toList();

    if (allDevices.isEmpty)
194
      throw const DeviceException('No Android devices detected');
195 196

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

201 202 203 204 205 206 207 208 209 210 211 212 213 214
  @override
  Future<void> chooseWorkingDeviceById(String deviceId) async {
    final String matchedId = _findMatchId(await discoverDevices(), deviceId);
    if (matchedId != null) {
      _workingDevice = AndroidDevice(deviceId: matchedId);
      print('Choose device by ID: $matchedId');
      return;
    }
    throw DeviceException(
      'Device with ID $deviceId is not found for operating system: '
      '$deviceOperatingSystem'
      );
  }

215 216
  @override
  Future<List<String>> discoverDevices() async {
217
    final List<String> output = (await eval(adbPath, <String>['devices', '-l'], canFail: false))
218
        .trim().split('\n');
219
    final List<String> results = <String>[];
220
    for (final String line in output) {
221
      // Skip lines like: * daemon started successfully *
222 223
      if (line.startsWith('* daemon '))
        continue;
224

225 226
      if (line.startsWith('List of devices'))
        continue;
227 228

      if (_kDeviceRegex.hasMatch(line)) {
229
        final Match match = _kDeviceRegex.firstMatch(line);
230

231 232
        final String deviceID = match[1];
        final String deviceState = match[2];
233 234 235 236 237

        if (!const <String>['unauthorized', 'offline'].contains(deviceState)) {
          results.add(deviceID);
        }
      } else {
238
        throw FormatException('Failed to parse device from adb output: "$line"');
239 240 241 242 243 244
      }
    }

    return results;
  }

245 246
  @override
  Future<Map<String, HealthCheckResult>> checkDevices() async {
247
    final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
248
    for (final String deviceId in await discoverDevices()) {
249
      try {
250
        final AndroidDevice device = AndroidDevice(deviceId: deviceId);
251 252 253
        // Just a smoke test that we can read wakefulness state
        // TODO(yjbanov): check battery level
        await device._getWakefulness();
254
        results['android-device-$deviceId'] = HealthCheckResult.success();
255
      } on Exception catch (e, s) {
256
        results['android-device-$deviceId'] = HealthCheckResult.error(e, s);
257 258 259 260 261 262
      }
    }
    return results;
  }

  @override
263
  Future<void> performPreflightTasks() async {
264 265 266 267 268 269 270 271 272 273
    // Kills the `adb` server causing it to start a new instance upon next
    // command.
    //
    // Restarting `adb` helps with keeping device connections alive. When `adb`
    // runs non-stop for too long it loses connections to devices. There may be
    // a better method, but so far that's the best one I've found.
    await exec(adbPath, <String>['kill-server'], canFail: false);
  }
}

274 275 276 277 278 279 280 281 282 283 284 285
class FuchsiaDeviceDiscovery implements DeviceDiscovery {
  factory FuchsiaDeviceDiscovery() {
    return _instance ??= FuchsiaDeviceDiscovery._();
  }

  FuchsiaDeviceDiscovery._();

  static FuchsiaDeviceDiscovery _instance;

 FuchsiaDevice _workingDevice;

 String get _devFinder {
286
    final String devFinder = path.join(getArtifactPath(), 'fuchsia', 'tools', 'device-finder');
287
    if (!File(devFinder).existsSync()) {
288
      throw FileSystemException("Couldn't find device-finder at location $devFinder");
289 290 291 292 293 294 295
    }
    return devFinder;
 }

  @override
  Future<FuchsiaDevice> get workingDevice async {
    if (_workingDevice == null) {
296 297 298 299 300
      if (Platform.environment.containsKey(DeviceIdEnvName)) {
        final String deviceId = Platform.environment[DeviceIdEnvName];
        await chooseWorkingDeviceById(deviceId);
        return _workingDevice;
      }
301 302 303 304 305 306 307 308 309 310 311 312 313
      await chooseWorkingDevice();
    }
    return _workingDevice;
  }

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

    if (allDevices.isEmpty) {
314
      throw const DeviceException('No Fuchsia devices detected');
315 316
    }
    _workingDevice = allDevices.first;
317
    print('Device chosen: $_workingDevice');
318 319
  }

320 321 322 323 324 325 326 327 328 329 330 331 332 333
  @override
  Future<void> chooseWorkingDeviceById(String deviceId) async {
    final String matchedId = _findMatchId(await discoverDevices(), deviceId);
    if (deviceId != null) {
      _workingDevice = FuchsiaDevice(deviceId: matchedId);
      print('Choose device by ID: $matchedId');
      return;
    }
    throw DeviceException(
      'Device with ID $deviceId is not found for operating system: '
      '$deviceOperatingSystem'
      );
  }

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
  @override
  Future<List<String>> discoverDevices() async {
    final List<String> output = (await eval(_devFinder, <String>['list', '-full']))
      .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(
          _devFinder,
          <String>[
            'resolve',
            '-device-limit',
            '1',
            deviceId,
          ]
        );
        if (resolveResult == 0) {
          results['fuchsia-device-$deviceId'] = HealthCheckResult.success();
        } else {
          results['fuchsia-device-$deviceId'] = HealthCheckResult.failure('Cannot resolve device $deviceId');
        }
368
      } on Exception catch (error, stacktrace) {
369 370 371 372 373 374 375 376 377 378
        results['fuchsia-device-$deviceId'] = HealthCheckResult.error(error, stacktrace);
      }
    }
    return results;
  }

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

379 380 381 382
class AndroidDevice extends Device {
  AndroidDevice({@required this.deviceId}) {
    _updateDeviceInfo();
  }
383 384 385

  @override
  final String deviceId;
386
  String deviceInfo = '';
387

388
  /// Whether the device is awake.
389
  @override
390 391 392 393 394
  Future<bool> isAwake() async {
    return await _getWakefulness() == 'Awake';
  }

  /// Whether the device is asleep.
395
  @override
396 397 398 399 400
  Future<bool> isAsleep() async {
    return await _getWakefulness() == 'Asleep';
  }

  /// Wake up the device if it is not awake using [togglePower].
401
  @override
402
  Future<void> wakeUp() async {
403 404
    if (!(await isAwake()))
      await togglePower();
405 406 407
  }

  /// Send the device to sleep mode if it is not asleep using [togglePower].
408
  @override
409
  Future<void> sendToSleep() async {
410 411
    if (!(await isAsleep()))
      await togglePower();
412 413 414 415
  }

  /// Sends `KEYCODE_POWER` (26), which causes the device to toggle its mode
  /// between awake and asleep.
416
  @override
417
  Future<void> togglePower() async {
418 419 420 421 422 423
    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.
424
  @override
425
  Future<void> unlock() async {
426 427 428 429
    await wakeUp();
    await shellExec('input', const <String>['keyevent', '82']);
  }

430
  @override
431
  Future<void> tap(int x, int y) async {
432 433 434
    await shellExec('input', <String>['tap', '$x', '$y']);
  }

435 436 437 438
  /// Retrieves device's wakefulness state.
  ///
  /// See: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/PowerManagerInternal.java
  Future<String> _getWakefulness() async {
439 440
    final String powerInfo = await shellEval('dumpsys', <String>['power']);
    final String wakefulness = grep('mWakefulness=', from: powerInfo).single.split('=')[1].trim();
441 442 443
    return wakefulness;
  }

444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466
  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) {
      deviceInfo = 'fingerprint: ${list[0]} os: ${list[1]}  api-level: ${list[2]}';
    } else {
      deviceInfo = '';
    }
  }

467
  /// Executes [command] on `adb shell` and returns its exit code.
468 469
  Future<void> shellExec(String command, List<String> arguments, { Map<String, String> environment, bool silent = false }) async {
    await adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
470 471 472
  }

  /// Executes [command] on `adb shell` and returns its standard output as a [String].
473 474
  Future<String> shellEval(String command, List<String> arguments, { Map<String, String> environment, bool silent = false }) {
    return adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
475 476 477
  }

  /// Runs `adb` with the given [arguments], selecting this device.
478 479 480 481 482 483 484 485 486 487 488 489 490
  Future<String> adb(
      List<String> arguments, {
      Map<String, String> environment,
      bool silent = false,
    }) {
    return eval(
      adbPath,
      <String>['-s', deviceId, ...arguments],
      environment: environment,
      canFail: false,
      printStdout: !silent,
      printStderr: !silent,
    );
491
  }
492 493 494

  @override
  Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
495
    final String meminfo = await shellEval('dumpsys', <String>['meminfo', packageName]);
496
    final Match match = RegExp(r'TOTAL\s+(\d+)').firstMatch(meminfo);
497
    assert(match != null, 'could not parse dumpsys meminfo output');
498 499 500 501 502
    return <String, dynamic>{
      'total_kb': int.parse(match.group(1)),
    };
  }

503 504
  @override
  Stream<String> get logcat {
505 506 507 508
    final Completer<void> stdoutDone = Completer<void>();
    final Completer<void> stderrDone = Completer<void>();
    final Completer<void> processDone = Completer<void>();
    final Completer<void> abort = Completer<void>();
509 510
    bool aborted = false;
    StreamController<String> stream;
511
    stream = StreamController<String>(
512 513
      onListen: () async {
        await adb(<String>['logcat', '--clear']);
514 515 516 517 518 519 520 521 522 523
        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'],
        );
524
        process.stdout
525 526
          .transform<String>(utf8.decoder)
          .transform<String>(const LineSplitter())
527 528 529 530 531
          .listen((String line) {
            print('adb logcat: $line');
            stream.sink.add(line);
          }, onDone: () { stdoutDone.complete(); });
        process.stderr
532 533
          .transform<String>(utf8.decoder)
          .transform<String>(const LineSplitter())
534 535 536
          .listen((String line) {
            print('adb logcat stderr: $line');
          }, onDone: () { stderrDone.complete(); });
537
        process.exitCode.then<void>((int exitCode) {
538 539
          print('adb logcat process terminated with exit code $exitCode');
          if (!aborted) {
540
            stream.addError(BuildFailedError('adb logcat failed with exit code $exitCode.\n'));
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568
            processDone.complete();
          }
        });
        await Future.any<dynamic>(<Future<dynamic>>[
          Future.wait<void>(<Future<void>>[
            stdoutDone.future,
            stderrDone.future,
            processDone.future,
          ]),
          abort.future,
        ]);
        aborted = true;
        print('terminating adb logcat');
        process.kill();
        print('closing logcat stream');
        await stream.close();
      },
      onCancel: () {
        if (!aborted) {
          print('adb logcat aborted');
          aborted = true;
          abort.complete();
        }
      },
    );
    return stream.stream;
  }

569
  @override
570
  Future<void> stop(String packageName) async {
571 572
    return shellExec('am', <String>['force-stop', packageName]);
  }
573 574 575 576 577

  @override
  String toString() {
    return '$deviceId $deviceInfo';
  }
578 579 580 581
}

class IosDeviceDiscovery implements DeviceDiscovery {
  factory IosDeviceDiscovery() {
582
    return _instance ??= IosDeviceDiscovery._();
583 584 585 586
  }

  IosDeviceDiscovery._();

587 588
  static IosDeviceDiscovery _instance;

589 590 591 592 593
  IosDevice _workingDevice;

  @override
  Future<IosDevice> get workingDevice async {
    if (_workingDevice == null) {
594 595 596 597 598
      if (Platform.environment.containsKey(DeviceIdEnvName)) {
        final String deviceId = Platform.environment[DeviceIdEnvName];
        await chooseWorkingDeviceById(deviceId);
        return _workingDevice;
      }
599 600 601 602 603 604 605 606 607
      await chooseWorkingDevice();
    }

    return _workingDevice;
  }

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

613
    if (allDevices.isEmpty)
614
      throw const DeviceException('No iOS devices detected');
615 616

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

621 622 623 624 625 626 627 628 629 630 631 632 633 634
  @override
  Future<void> chooseWorkingDeviceById(String deviceId) async {
    final String matchedId = _findMatchId(await discoverDevices(), deviceId);
    if (matchedId != null) {
      _workingDevice = IosDevice(deviceId: matchedId);
      print('Choose device by ID: $matchedId');
      return;
    }
    throw DeviceException(
      'Device with ID $deviceId is not found for operating system: '
      '$deviceOperatingSystem'
      );
  }

635 636
  @override
  Future<List<String>> discoverDevices() async {
637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674
    final List<dynamic> results = json.decode(await eval(
      path.join(flutterDirectory.path, 'bin', 'flutter'),
      <String>['devices', '--machine', '--suppress-analytics'],
    )) 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) {
675
      throw const DeviceException('No connected iOS devices found.');
676 677
    }
    return deviceIds;
678
  }
679 680 681

  @override
  Future<Map<String, HealthCheckResult>> checkDevices() async {
682
    final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
683
    for (final String deviceId in await discoverDevices()) {
684
      // TODO(ianh): do a more meaningful connectivity check than just recording the ID
685
      results['ios-device-$deviceId'] = HealthCheckResult.success();
686 687 688 689 690
    }
    return results;
  }

  @override
691
  Future<void> performPreflightTasks() async {
692 693 694 695 696
    // Currently we do not have preflight tasks for iOS.
  }
}

/// iOS device.
697
class IosDevice extends Device {
698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714
  const IosDevice({ @required this.deviceId });

  @override
  final String deviceId;

  // 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
715
  Future<void> wakeUp() async {}
716 717

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

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

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

726
  @override
727
  Future<void> tap(int x, int y) async {
728
    throw UnimplementedError();
729 730
  }

731 732
  @override
  Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
733
    throw UnimplementedError();
734 735
  }

736 737
  @override
  Stream<String> get logcat {
738
    throw UnimplementedError();
739 740
  }

741
  @override
742
  Future<void> stop(String packageName) async {}
743 744
}

745
/// Fuchsia device.
746
class FuchsiaDevice extends Device {
747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778
  const FuchsiaDevice({ @required this.deviceId });

  @override
  final String deviceId;

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

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

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

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

  @override
  Future<void> 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 {
779
    throw UnimplementedError();
780 781 782 783
  }

  @override
  Stream<String> get logcat {
784
    throw UnimplementedError();
785 786 787
  }
}

788 789
/// Path to the `adb` executable.
String get adbPath {
790
  final String androidHome = Platform.environment['ANDROID_HOME'] ?? Platform.environment['ANDROID_SDK_ROOT'];
791

792 793
  if (androidHome == null) {
    throw const DeviceException(
794 795
      'The ANDROID_SDK_ROOT environment variable is '
      'missing. The variable must point to the Android '
796 797 798
      'SDK directory containing platform-tools.'
    );
  }
799

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

802
  if (!canRun(adbPath))
803
    throw DeviceException('adb not found at: $adbPath');
804

805
  return path.absolute(adbPath);
806
}
807 808 809 810 811 812 813 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 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913

class FakeDevice extends Device {
  const FakeDevice({ @required this.deviceId });

  @override
  final String deviceId;

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

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

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

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

  @override
  Future<void> 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 {}
}

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

  FakeDeviceDiscovery._();

  static FakeDeviceDiscovery _instance;

  FakeDevice _workingDevice;

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

    return _workingDevice;
  }

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

  @override
  Future<void> chooseWorkingDeviceById(String deviceId) async {
    final String matchedId = _findMatchId(await discoverDevices(), deviceId);
    if (matchedId != null) {
      _workingDevice = FakeDevice(deviceId: matchedId);
      print('Choose device by ID: $matchedId');
      return;
    }
    throw DeviceException(
      'Device with ID $deviceId is not found for operating system: '
      '$deviceOperatingSystem'
      );
  }

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

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

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