device.dart 27.6 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6 7
import 'dart:io';

8
import 'package:crypto/crypto.dart';
9 10 11
import 'package:logging/logging.dart';
import 'package:path/path.dart' as path;

12
import 'application_package.dart';
13
import 'build_configuration.dart';
14
import 'process.dart';
15

16
final Logger _logging = new Logger('flutter_tools.device');
17

18
abstract class Device {
19
  final String id;
20
  static Map<String, Device> _deviceCache = {};
21

22
  factory Device._unique(String className, [String id = null]) {
23 24 25
    if (id == null) {
      if (className == AndroidDevice.className) {
        id = AndroidDevice.defaultDeviceID;
26 27
      } else if (className == IOSDevice.className) {
        id = IOSDevice.defaultDeviceID;
28 29
      } else if (className == IOSSimulator.className) {
        id = IOSSimulator.defaultDeviceID;
30 31 32 33 34 35 36 37 38 39
      } else {
        throw 'Attempted to create a Device of unknown type $className';
      }
    }

    return _deviceCache.putIfAbsent(id, () {
      if (className == AndroidDevice.className) {
        final device = new AndroidDevice._(id);
        _deviceCache[id] = device;
        return device;
40 41 42 43
      } else if (className == IOSDevice.className) {
        final device = new IOSDevice._(id);
        _deviceCache[id] = device;
        return device;
44 45 46 47
      } else if (className == IOSSimulator.className) {
        final device = new IOSSimulator._(id);
        _deviceCache[id] = device;
        return device;
48 49 50 51 52 53
      } else {
        throw 'Attempted to create a Device of unknown type $className';
      }
    });
  }

54
  Device._(this.id);
55 56

  /// Install an app package on the current device
57
  bool installApp(ApplicationPackage app);
58 59 60

  /// Check if the device is currently connected
  bool isConnected();
61 62

  /// Check if the current version of the given app is already installed
63
  bool isAppInstalled(ApplicationPackage app);
64

65
  TargetPlatform get platform;
66 67 68

  Future<int> logs({bool clear: false});

69 70 71 72 73
  /// Start an app package on the current device
  Future<bool> startApp(ApplicationPackage app);

  /// Stop an app package on the current device
  Future<bool> stopApp(ApplicationPackage app);
74 75
}

76
class IOSDevice extends Device {
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
  static const String className = 'IOSDevice';
  static final String defaultDeviceID = 'default_ios_id';

  static const String _macInstructions =
      'To work with iOS devices, please install ideviceinstaller. '
      'If you use homebrew, you can install it with '
      '"\$ brew install ideviceinstaller".';
  static const String _linuxInstructions =
      'To work with iOS devices, please install ideviceinstaller. '
      'On Ubuntu or Debian, you can install it with '
      '"\$ apt-get install ideviceinstaller".';

  String _installerPath;
  String get installerPath => _installerPath;

  String _listerPath;
  String get listerPath => _listerPath;

  String _informerPath;
  String get informerPath => _informerPath;

98 99 100
  String _debuggerPath;
  String get debuggerPath => _debuggerPath;

101 102 103
  String _loggerPath;
  String get loggerPath => _loggerPath;

104 105 106
  String _pusherPath;
  String get pusherPath => _pusherPath;

107 108 109 110
  String _name;
  String get name => _name;

  factory IOSDevice({String id, String name}) {
111
    IOSDevice device = new Device._unique(className, id);
112 113 114 115 116 117 118 119
    device._name = name;
    return device;
  }

  IOSDevice._(String id) : super._(id) {
    _installerPath = _checkForCommand('ideviceinstaller');
    _listerPath = _checkForCommand('idevice_id');
    _informerPath = _checkForCommand('ideviceinfo');
120
    _debuggerPath = _checkForCommand('idevicedebug');
121
    _loggerPath = _checkForCommand('idevicesyslog');
122 123 124 125 126 127 128
    _pusherPath = _checkForCommand(
        'ios-deploy',
        'To copy files to iOS devices, please install ios-deploy. '
        'You can do this using homebrew as follows:\n'
        '\$ brew tap flutter/flutter\n'
        '\$ brew install ios-deploy',
        'Copying files to iOS devices is not currently supported on Linux.');
129 130 131 132 133 134 135 136 137 138 139
  }

  static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
    List<IOSDevice> devices = [];
    for (String id in _getAttachedDeviceIDs(mockIOS)) {
      String name = _getDeviceName(id, mockIOS);
      devices.add(new IOSDevice(id: id, name: name));
    }
    return devices;
  }

140
  static Iterable<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) {
141 142
    String listerPath =
        (mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id');
143 144 145 146 147 148 149 150 151
    String output;
    try {
      output = runSync([listerPath, '-l']);
    } catch (e) {
      return [];
    }
    return output.trim()
                 .split('\n')
                 .where((String s) => s != null && s.length > 0);
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
  }

  static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) {
    String informerPath = (mockIOS != null)
        ? mockIOS.informerPath
        : _checkForCommand('ideviceinfo');
    return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]);
  }

  static final Map<String, String> _commandMap = {};
  static String _checkForCommand(String command,
      [String macInstructions = _macInstructions,
      String linuxInstructions = _linuxInstructions]) {
    return _commandMap.putIfAbsent(command, () {
      try {
        command = runCheckedSync(['which', command]).trim();
      } catch (e) {
        if (Platform.isMacOS) {
          _logging.severe(macInstructions);
        } else if (Platform.isLinux) {
          _logging.severe(linuxInstructions);
        } else {
          _logging.severe('$command is not available on your platform.');
        }
      }
      return command;
    });
  }

  @override
  bool installApp(ApplicationPackage app) {
183 184
    try {
      if (id == defaultDeviceID) {
185
        runCheckedSync([installerPath, '-i', app.localPath]);
186
      } else {
187
        runCheckedSync([installerPath, '-u', id, '-i', app.localPath]);
188 189 190 191
      }
      return true;
    } catch (e) {
      return false;
192 193 194 195 196 197
    }
    return false;
  }

  @override
  bool isConnected() {
198
    Iterable<String> ids = _getAttachedDeviceIDs();
199 200 201 202 203 204 205 206 207 208
    for (String id in ids) {
      if (id == this.id || this.id == defaultDeviceID) {
        return true;
      }
    }
    return false;
  }

  @override
  bool isAppInstalled(ApplicationPackage app) {
209 210
    try {
      String apps = runCheckedSync([installerPath, '-l']);
211
      if (new RegExp(app.id, multiLine: true).hasMatch(apps)) {
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
        return true;
      }
    } catch (e) {
      return false;
    }
    return false;
  }

  @override
  Future<bool> startApp(ApplicationPackage app) async {
    if (!isAppInstalled(app)) {
      return false;
    }
    // idevicedebug hangs forever after launching the app, so kill it after
    // giving it plenty of time to send the launch command.
    return runAndKill(
228
        [debuggerPath, 'run', app.id], new Duration(seconds: 3)).then(
229 230 231 232 233 234 235 236 237 238 239
        (_) {
      return true;
    }, onError: (e) {
      _logging.info('Failure running $debuggerPath: ', e);
      return false;
    });
  }

  @override
  Future<bool> stopApp(ApplicationPackage app) async {
    // Currently we don't have a way to stop an app running on iOS.
240 241
    return false;
  }
242

243 244 245 246 247 248 249 250
  Future<bool> pushFile(
      ApplicationPackage app, String localFile, String targetFile) async {
    if (Platform.isMacOS) {
      runSync([
        pusherPath,
        '-t',
        '1',
        '--bundle_id',
251
        app.id,
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
        '--upload',
        localFile,
        '--to',
        targetFile
      ]);
      return true;
    } else {
      // TODO(iansf): It may be possible to make this work on Linux. Since this
      //              functionality appears to be the only that prevents us from
      //              supporting iOS on Linux, it may be worth putting some time
      //              into investigating this.
      //              See https://bbs.archlinux.org/viewtopic.php?id=192655
      return false;
    }
    return false;
  }

269
  @override
270
  TargetPlatform get platform => TargetPlatform.iOS;
271

272
  /// Note that clear is not supported on iOS at this time.
273 274 275 276
  Future<int> logs({bool clear: false}) async {
    if (!isConnected()) {
      return 2;
    }
277
    return runCommandAndStreamOutput([loggerPath],
278
        prefix: 'iOS dev: ', filter: new RegExp(r'.*SkyShell.*'));
279
  }
280 281
}

282
class IOSSimulator extends Device {
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
  static const String className = 'IOSSimulator';
  static final String defaultDeviceID = 'default_ios_sim_id';

  static const String _macInstructions =
      'To work with iOS devices, please install ideviceinstaller. '
      'If you use homebrew, you can install it with '
      '"\$ brew install ideviceinstaller".';

  static String _xcrunPath = path.join('/usr', 'bin', 'xcrun');

  String _iOSSimPath;
  String get iOSSimPath => _iOSSimPath;

  String get xcrunPath => _xcrunPath;

  String _name;
  String get name => _name;

  factory IOSSimulator({String id, String name, String iOSSimulatorPath}) {
302
    IOSSimulator device = new Device._unique(className, id);
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 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 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
    device._name = name;
    if (iOSSimulatorPath == null) {
      iOSSimulatorPath = path.join('/Applications', 'iOS Simulator.app',
          'Contents', 'MacOS', 'iOS Simulator');
    }
    device._iOSSimPath = iOSSimulatorPath;
    return device;
  }

  IOSSimulator._(String id) : super._(id) {}

  static String _getRunningSimulatorID([IOSSimulator mockIOS]) {
    String xcrunPath = mockIOS != null ? mockIOS.xcrunPath : _xcrunPath;
    String output = runCheckedSync([xcrunPath, 'simctl', 'list', 'devices']);

    Match match;
    Iterable<Match> matches = new RegExp(r'[^\(]+\(([^\)]+)\) \(Booted\)',
        multiLine: true).allMatches(output);
    if (matches.length > 1) {
      // More than one simulator is listed as booted, which is not allowed but
      // sometimes happens erroneously.  Kill them all because we don't know
      // which one is actually running.
      _logging.warning('Multiple running simulators were detected, '
          'which is not supposed to happen.');
      for (Match m in matches) {
        if (m.groupCount > 0) {
          _logging.warning('Killing simulator ${m.group(1)}');
          runSync([xcrunPath, 'simctl', 'shutdown', m.group(1)]);
        }
      }
    } else if (matches.length == 1) {
      match = matches.first;
    }

    if (match != null && match.groupCount > 0) {
      return match.group(1);
    } else {
      _logging.info('No running simulators found');
      return null;
    }
  }

  String _getSimulatorPath() {
    String deviceID = id == defaultDeviceID ? _getRunningSimulatorID() : id;
    String homeDirectory = path.absolute(Platform.environment['HOME']);
    if (deviceID == null) {
      return null;
    }
    return path.join(homeDirectory, 'Library', 'Developer', 'CoreSimulator',
        'Devices', deviceID);
  }

  String _getSimulatorAppHomeDirectory(ApplicationPackage app) {
    String simulatorPath = _getSimulatorPath();
    if (simulatorPath == null) {
      return null;
    }
    return path.join(simulatorPath, 'data');
  }

  static List<IOSSimulator> getAttachedDevices([IOSSimulator mockIOS]) {
    List<IOSSimulator> devices = [];
    String id = _getRunningSimulatorID(mockIOS);
    if (id != null) {
      // TODO(iansf): get the simulator's name
      // String name = _getDeviceName(id, mockIOS);
      devices.add(new IOSSimulator(id: id));
    }
    return devices;
  }

  Future<bool> boot() async {
    if (!Platform.isMacOS) {
      return false;
    }
    if (isConnected()) {
      return true;
    }
    if (id == defaultDeviceID) {
      runDetached([iOSSimPath]);

      Future<bool> checkConnection([int attempts = 20]) async {
        if (attempts == 0) {
          _logging.info('Timed out waiting for iOS Simulator $id to boot.');
          return false;
        }
        if (!isConnected()) {
          _logging.info('Waiting for iOS Simulator $id to boot...');
          return new Future.delayed(new Duration(milliseconds: 500),
              () => checkConnection(attempts - 1));
        }
        return true;
      }
      return checkConnection();
    } else {
      try {
        runCheckedSync([xcrunPath, 'simctl', 'boot', id]);
      } catch (e) {
        _logging.warning('Unable to boot iOS Simulator $id: ', e);
        return false;
      }
    }
    return false;
  }

  @override
  bool installApp(ApplicationPackage app) {
    if (!isConnected()) {
      return false;
    }
    try {
      if (id == defaultDeviceID) {
415
        runCheckedSync([xcrunPath, 'simctl', 'install', 'booted', app.localPath]);
416
      } else {
417
        runCheckedSync([xcrunPath, 'simctl', 'install', id, app.localPath]);
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457
      }
      return true;
    } catch (e) {
      return false;
    }
  }

  @override
  bool isConnected() {
    if (!Platform.isMacOS) {
      return false;
    }
    String simulatorID = _getRunningSimulatorID();
    if (simulatorID == null) {
      return false;
    } else if (id == defaultDeviceID) {
      return true;
    } else {
      return _getRunningSimulatorID() == id;
    }
  }

  @override
  bool isAppInstalled(ApplicationPackage app) {
    try {
      String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
      return FileSystemEntity.isDirectorySync(simulatorHomeDirectory);
    } catch (e) {
      return false;
    }
  }

  @override
  Future<bool> startApp(ApplicationPackage app) async {
    if (!isAppInstalled(app)) {
      return false;
    }
    try {
      if (id == defaultDeviceID) {
        runCheckedSync(
458
            [xcrunPath, 'simctl', 'launch', 'booted', app.id]);
459
      } else {
460
        runCheckedSync([xcrunPath, 'simctl', 'launch', id, app.id]);
461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484
      }
      return true;
    } catch (e) {
      return false;
    }
  }

  @override
  Future<bool> stopApp(ApplicationPackage app) async {
    // Currently we don't have a way to stop an app running on iOS.
    return false;
  }

  Future<bool> pushFile(
      ApplicationPackage app, String localFile, String targetFile) async {
    if (Platform.isMacOS) {
      String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
      runCheckedSync(
          ['cp', localFile, path.join(simulatorHomeDirectory, targetFile)]);
      return true;
    }
    return false;
  }

485
  @override
486
  TargetPlatform get platform => TargetPlatform.iOSSimulator;
487

488 489 490 491 492 493 494 495 496 497 498 499
  Future<int> logs({bool clear: false}) async {
    if (!isConnected()) {
      return 2;
    }
    String homeDirectory = path.absolute(Platform.environment['HOME']);
    String simulatorDeviceID = _getRunningSimulatorID();
    String logFilePath = path.join(homeDirectory, 'Library', 'Logs',
        'CoreSimulator', simulatorDeviceID, 'system.log');
    if (clear) {
      runSync(['rm', logFilePath]);
    }
    return runCommandAndStreamOutput(['tail', '-f', logFilePath],
500
        prefix: 'iOS sim: ', filter: new RegExp(r'.*SkyShell.*'));
501 502 503
  }
}

504
class AndroidDevice extends Device {
505
  static const String _ADB_PATH = 'adb';
506
  static const int _observatoryPort = 8181;
507

508
  static const String className = 'AndroidDevice';
509
  static final String defaultDeviceID = 'default_android_device';
510

511 512 513 514
  String productID;
  String modelID;
  String deviceCodeName;

515 516
  String _adbPath;
  String get adbPath => _adbPath;
517 518
  bool _hasAdb = false;
  bool _hasValidAndroid = false;
519

520 521 522 523 524
  factory AndroidDevice(
      {String id: null,
      String productID: null,
      String modelID: null,
      String deviceCodeName: null}) {
525
    AndroidDevice device = new Device._unique(className, id);
526 527 528 529 530 531 532 533 534 535
    device.productID = productID;
    device.modelID = modelID;
    device.deviceCodeName = deviceCodeName;
    return device;
  }

  /// mockAndroid argument is only to facilitate testing with mocks, so that
  /// we don't have to rely on the test setup having adb available to it.
  static List<AndroidDevice> getAttachedDevices([AndroidDevice mockAndroid]) {
    List<AndroidDevice> devices = [];
536 537 538 539 540
    String adbPath = (mockAndroid != null) ? mockAndroid.adbPath : _getAdbPath();
    List<String> output = runSync([adbPath, 'devices', '-l']).trim().split('\n');

    // 015d172c98400a03       device usb:340787200X product:nakasi model:Nexus_7 device:grouper
    RegExp deviceRegex1 = new RegExp(
541
        r'^(\S+)\s+device\s+\S+\s+product:(\S+)\s+model:(\S+)\s+device:(\S+)$');
542 543 544 545

    // 0149947A0D01500C       device usb:340787200X
    RegExp deviceRegex2 = new RegExp(r'^(\S+)\s+device\s+\S+$');

546 547
    // Skip first line, which is always 'List of devices attached'.
    for (String line in output.skip(1)) {
548 549
      if (deviceRegex1.hasMatch(line)) {
        Match match = deviceRegex1.firstMatch(line);
550 551 552 553 554 555 556 557 558
        String deviceID = match[1];
        String productID = match[2];
        String modelID = match[3];
        String deviceCodeName = match[4];

        devices.add(new AndroidDevice(
            id: deviceID,
            productID: productID,
            modelID: modelID,
559 560 561 562 563 564
            deviceCodeName: deviceCodeName
        ));
      } else if (deviceRegex2.hasMatch(line)) {
        Match match = deviceRegex2.firstMatch(line);
        String deviceID = match[1];
        devices.add(new AndroidDevice(id: deviceID));
565 566 567 568 569 570 571
      } else {
        _logging.warning('Unexpected failure parsing device information '
            'from adb output:\n$line\n'
            'Please report a bug at http://flutter.io/');
      }
    }
    return devices;
572 573
  }

574
  AndroidDevice._(id) : super._(id) {
575 576
    _adbPath = _getAdbPath();
    _hasAdb = _checkForAdb();
577

578
    // Checking for Jelly Bean only needs to be done if we are starting an
579 580
    // app, but it has an important side effect, which is to discard any
    // progress messages if the adb server is restarted.
581
    _hasValidAndroid = _checkForSupportedAndroidVersion();
582

583
    if (!_hasAdb || !_hasValidAndroid) {
584
      _logging.warning('Unable to run on Android.');
585 586
    }
  }
587

588
  static String _getAdbPath() {
589 590 591 592 593 594
    if (Platform.environment.containsKey('ANDROID_HOME')) {
      String androidHomeDir = Platform.environment['ANDROID_HOME'];
      String adbPath1 =
          path.join(androidHomeDir, 'sdk', 'platform-tools', 'adb');
      String adbPath2 = path.join(androidHomeDir, 'platform-tools', 'adb');
      if (FileSystemEntity.isFileSync(adbPath1)) {
595
        return adbPath1;
596
      } else if (FileSystemEntity.isFileSync(adbPath2)) {
597
        return adbPath2;
598 599 600
      } else {
        _logging.info('"adb" not found at\n  "$adbPath1" or\n  "$adbPath2"\n' +
            'using default path "$_ADB_PATH"');
601
        return _ADB_PATH;
602 603
      }
    } else {
604
      return _ADB_PATH;
605 606 607
    }
  }

608 609 610 611 612 613 614 615 616
  List<String> adbCommandForDevice(List<String> args) {
    List<String> result = <String>[adbPath];
    if (id != defaultDeviceID) {
      result.addAll(['-s', id]);
    }
    result.addAll(args);
    return result;
  }

617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 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
  bool _isValidAdbVersion(String adbVersion) {
    // Sample output: 'Android Debug Bridge version 1.0.31'
    Match versionFields =
        new RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
    if (versionFields != null) {
      int majorVersion = int.parse(versionFields[1]);
      int minorVersion = int.parse(versionFields[2]);
      int patchVersion = int.parse(versionFields[3]);
      if (majorVersion > 1) {
        return true;
      }
      if (majorVersion == 1 && minorVersion > 0) {
        return true;
      }
      if (majorVersion == 1 && minorVersion == 0 && patchVersion >= 32) {
        return true;
      }
      return false;
    }
    _logging.warning(
        'Unrecognized adb version string $adbVersion. Skipping version check.');
    return true;
  }

  bool _checkForAdb() {
    try {
      String adbVersion = runCheckedSync([adbPath, 'version']);
      if (_isValidAdbVersion(adbVersion)) {
        return true;
      }

      String locatedAdbPath = runCheckedSync(['which', 'adb']);
      _logging.severe('"$locatedAdbPath" is too old. '
          'Please install version 1.0.32 or later.\n'
          'Try setting ANDROID_HOME to the path to your Android SDK install. '
          'Android builds are unavailable.');
    } catch (e, stack) {
      _logging.severe('"adb" not found in \$PATH. '
          'Please install the Android SDK or set ANDROID_HOME '
          'to the path of your Android SDK install.');
      _logging.info(e);
      _logging.info(stack);
    }
    return false;
  }

663
  bool _checkForSupportedAndroidVersion() {
664 665 666 667 668
    try {
      // If the server is automatically restarted, then we get irrelevant
      // output lines like this, which we want to ignore:
      //   adb server is out of date.  killing..
      //   * daemon started successfully *
669
      runCheckedSync(adbCommandForDevice(['start-server']));
670

671
      String ready = runSync(adbCommandForDevice(['shell', 'echo', 'ready']));
672 673 674 675 676
      if (ready.trim() != 'ready') {
        _logging.info('Android device not found.');
        return false;
      }

677 678
      // Sample output: '22'
      String sdkVersion =
679
          runCheckedSync(adbCommandForDevice(['shell', 'getprop', 'ro.build.version.sdk']))
680 681 682 683 684 685 686 687
              .trimRight();

      int sdkVersionParsed =
          int.parse(sdkVersion, onError: (String source) => null);
      if (sdkVersionParsed == null) {
        _logging.severe('Unexpected response from getprop: "$sdkVersion"');
        return false;
      }
688 689 690
      if (sdkVersionParsed < 16) {
        _logging.severe('The Android version ($sdkVersion) on the target device '
            'is too old. Please use a Jelly Bean (version 16 / 4.1.x) device or later.');
691 692 693
        return false;
      }
      return true;
694 695
    } catch (e) {
      _logging.severe('Unexpected failure from adb: ', e);
696 697 698
    }
    return false;
  }
699

700
  String _getDeviceSha1Path(ApplicationPackage app) {
Jeff R. Allen's avatar
Jeff R. Allen committed
701
    return '/data/local/tmp/sky.${app.id}.sha1';
702 703
  }

704
  String _getDeviceApkSha1(ApplicationPackage app) {
705
    return runCheckedSync(adbCommandForDevice(['shell', 'cat', _getDeviceSha1Path(app)]));
706 707
  }

708
  String _getSourceSha1(ApplicationPackage app) {
709 710 711 712
    var sha1 = new SHA1();
    var file = new File(app.localPath);
    sha1.add(file.readAsBytesSync());
    return CryptoUtils.bytesToHex(sha1.close());
713 714 715
  }

  @override
716
  bool isAppInstalled(ApplicationPackage app) {
717 718 719
    if (!isConnected()) {
      return false;
    }
720
    if (runCheckedSync(adbCommandForDevice(['shell', 'pm', 'path', app.id])) ==
721
        '') {
722
      _logging.info(
723
          'TODO(iansf): move this log to the caller. ${app.name} is not on the device. Installing now...');
724 725
      return false;
    }
726
    if (_getDeviceApkSha1(app) != _getSourceSha1(app)) {
727
      _logging.info(
728
          'TODO(iansf): move this log to the caller. ${app.name} is out of date. Installing now...');
729 730 731 732 733 734
      return false;
    }
    return true;
  }

  @override
735
  bool installApp(ApplicationPackage app) {
736 737 738 739
    if (!isConnected()) {
      _logging.info('Android device not connected. Not installing.');
      return false;
    }
740 741
    if (!FileSystemEntity.isFileSync(app.localPath)) {
      _logging.severe('"${app.localPath}" does not exist.');
742 743 744
      return false;
    }

745
    print('Installing ${app.name} on device.');
746 747
    runCheckedSync(adbCommandForDevice(['install', '-r', app.localPath]));
    runCheckedSync(adbCommandForDevice(['shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app)]));
748 749 750
    return true;
  }

751 752
  void _forwardObservatoryPort() {
    // Set up port forwarding for observatory.
753
    String portString = 'tcp:$_observatoryPort';
754
    runCheckedSync(adbCommandForDevice(['forward', portString, portString]));
755 756 757 758 759 760 761 762 763 764 765 766
  }

  bool startBundle(AndroidApk apk, String bundlePath, bool poke, bool checked) {
    if (!FileSystemEntity.isFileSync(bundlePath)) {
      _logging.severe('Cannot find $bundlePath');
      return false;
    }

    if (!poke)
      _forwardObservatoryPort();

    String deviceTmpPath = '/data/local/tmp/dev.flx';
767 768
    runCheckedSync(adbCommandForDevice(['push', bundlePath, deviceTmpPath]));
    List<String> cmd = adbCommandForDevice([
769 770
      'shell', 'am', 'start',
      '-a', 'android.intent.action.RUN',
Jeff R. Allen's avatar
Jeff R. Allen committed
771
      '-d', deviceTmpPath,
772
    ]);
773 774 775 776 777 778 779
    if (checked)
      cmd.addAll(['--ez', 'enable-checked-mode', 'true']);
    cmd.add(apk.launchActivity);
    runCheckedSync(cmd);
    return true;
  }

780
  @override
781
  Future<bool> startApp(ApplicationPackage app) async {
Adam Barth's avatar
Adam Barth committed
782
    // Android currently has to be started with startBundle(...).
783 784 785 786
    assert(false);
    return false;
  }

787 788
  Future<bool> stopApp(ApplicationPackage app) async {
    final AndroidApk apk = app;
789
    runSync(adbCommandForDevice(['shell', 'am', 'force-stop', apk.id]));
790 791 792
    return true;
  }

793
  @override
794
  TargetPlatform get platform => TargetPlatform.android;
795

796
  void clearLogs() {
797
    runSync(adbCommandForDevice(['logcat', '-c']));
798 799
  }

800 801 802 803 804
  Future<int> logs({bool clear: false}) async {
    if (!isConnected()) {
      return 2;
    }

805 806 807 808
    if (clear) {
      clearLogs();
    }

809
    return runCommandAndStreamOutput(adbCommandForDevice([
810 811 812 813
      'logcat',
      '-v',
      'tag', // Only log the tag and the message
      '-s',
Devon Carew's avatar
Devon Carew committed
814 815 816 817
      'sky:V',
      'chromium:D',
      'ActivityManager:W',
      '*:F',
818
    ]), prefix: 'android: ');
819 820
  }

821
  void startTracing(AndroidApk apk) {
822
    runCheckedSync(adbCommandForDevice([
823 824 825 826
      'shell',
      'am',
      'broadcast',
      '-a',
827
      '${apk.id}.TRACING_START'
828
    ]));
829 830 831 832
  }

  String stopTracing(AndroidApk apk) {
    clearLogs();
833
    runCheckedSync(adbCommandForDevice([
834 835 836 837
      'shell',
      'am',
      'broadcast',
      '-a',
838
      '${apk.id}.TRACING_STOP'
839
    ]));
840 841 842 843 844 845 846

    RegExp traceRegExp = new RegExp(r'Saving trace to (\S+)', multiLine: true);
    RegExp completeRegExp = new RegExp(r'Trace complete', multiLine: true);

    String tracePath = null;
    bool isComplete = false;
    while (!isComplete) {
847
      String logs = runSync(adbCommandForDevice(['logcat', '-d']));
848 849 850 851 852 853 854 855
      Match fileMatch = traceRegExp.firstMatch(logs);
      if (fileMatch[1] != null) {
        tracePath = fileMatch[1];
      }
      isComplete = completeRegExp.hasMatch(logs);
    }

    if (tracePath != null) {
856 857 858
      runSync(adbCommandForDevice(['shell', 'run-as', apk.id, 'chmod', '777', tracePath]));
      runSync(adbCommandForDevice(['pull', tracePath]));
      runSync(adbCommandForDevice(['shell', 'rm', tracePath]));
859 860 861 862 863 864 865
      return path.basename(tracePath);
    }
    _logging.warning('No trace file detected. '
        'Did you remember to start the trace before stopping it?');
    return null;
  }

866 867
  @override
  bool isConnected() => _hasValidAndroid;
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

class DeviceStore {
  final AndroidDevice android;
  final IOSDevice iOS;
  final IOSSimulator iOSSimulator;

  List<Device> get all {
    List<Device> result = <Device>[];
    if (android != null)
      result.add(android);
    if (iOS != null)
      result.add(iOS);
    if (iOSSimulator != null)
      result.add(iOSSimulator);
    return result;
  }

  DeviceStore({
    this.android,
    this.iOS,
    this.iOSSimulator
  });

  factory DeviceStore.forConfigs(List<BuildConfiguration> configs) {
    AndroidDevice android;
    IOSDevice iOS;
    IOSSimulator iOSSimulator;

    for (BuildConfiguration config in configs) {
898 899
      switch (config.targetPlatform) {
        case TargetPlatform.android:
900
          assert(android == null);
901 902 903 904 905 906 907 908 909 910 911 912 913
          List<AndroidDevice> androidDevices = AndroidDevice.getAttachedDevices();
          if (config.deviceId != null) {
            android = androidDevices.firstWhere(
                (AndroidDevice dev) => (dev.id == config.deviceId),
                orElse: () => null);
            if (android == null) {
              print('Warning: Device ID ${config.deviceId} not found');
            }
          } else if (androidDevices.length == 1) {
            android = androidDevices[0];
          } else if (androidDevices.length > 1) {
            print('Warning: Multiple Android devices are connected, but no device ID was specified.');
          }
914
          break;
915
        case TargetPlatform.iOS:
916 917 918
          assert(iOS == null);
          iOS = new IOSDevice();
          break;
919
        case TargetPlatform.iOSSimulator:
920 921 922
          assert(iOSSimulator == null);
          iOSSimulator = new IOSSimulator();
          break;
Ian Hickson's avatar
Ian Hickson committed
923
        case TargetPlatform.mac:
924
        case TargetPlatform.linux:
925 926 927 928 929 930 931
          break;
      }
    }

    return new DeviceStore(android: android, iOS: iOS, iOSSimulator: iOSSimulator);
  }
}