android_device.dart 25.4 KB
Newer Older
1 2 3 4 5
// Copyright 2016 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.

import 'dart:async';
6
import 'dart:convert';
7 8
import 'dart:io';

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

11
import '../android/android_sdk.dart';
12
import '../application_package.dart';
13
import '../base/os.dart';
14
import '../base/process.dart';
15
import '../build_info.dart';
16 17
import '../device.dart';
import '../flx.dart' as flx;
18
import '../globals.dart';
19
import '../vmservice.dart';
20
import '../protocol_discovery.dart';
21
import 'adb.dart';
22
import 'android.dart';
23
import 'android_sdk.dart';
24

25 26
const String _defaultAdbPath = 'adb';

27 28 29 30 31 32
// Path where the FLX bundle will be copied on the device.
const String _deviceBundlePath = '/data/local/tmp/dev.flx';

// Path where the snapshot will be copied on the device.
const String _deviceSnapshotPath = '/data/local/tmp/dev_snapshot.bin';

33 34
class AndroidDevices extends PollingDeviceDiscovery {
  AndroidDevices() : super('AndroidDevices');
35

36
  @override
37
  bool get supportsPlatform => true;
38 39

  @override
40
  List<Device> pollingGetDevices() => getAdbDevices();
41 42
}

43
class AndroidDevice extends Device {
44 45 46 47
  AndroidDevice(
    String id, {
    this.productID,
    this.modelID,
48 49
    this.deviceCodeName
  }) : super(id);
50

51 52 53 54
  final String productID;
  final String modelID;
  final String deviceCodeName;

55
  Map<String, String> _properties;
56
  bool _isLocalEmulator;
57 58 59 60 61
  TargetPlatform _platform;

  String _getProperty(String name) {
    if (_properties == null) {
      _properties = <String, String>{};
62

63 64
      List<String> propCommand = adbCommandForDevice(<String>['shell', 'getprop']);
      printTrace(propCommand.join(' '));
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80

      try {
        // We pass an encoding of LATIN1 so that we don't try and interpret the
        // `adb shell getprop` result as UTF8.
        ProcessResult result = Process.runSync(
          propCommand.first,
          propCommand.sublist(1),
          stdoutEncoding: LATIN1
        );
        if (result.exitCode == 0) {
          _properties = parseAdbDeviceProperties(result.stdout);
        } else {
          printError('Error retrieving device properties for $name.');
        }
      } catch (error) {
        printError('Error retrieving device properties for $name: $error');
81 82
      }
    }
83

84 85
    return _properties[name];
  }
86

87
  @override
88 89
  bool get isLocalEmulator {
    if (_isLocalEmulator == null) {
90 91 92 93 94 95 96 97 98
      String characteristics = _getProperty('ro.build.characteristics');
      _isLocalEmulator = characteristics != null && characteristics.contains('emulator');
    }
    return _isLocalEmulator;
  }

  @override
  TargetPlatform get platform {
    if (_platform == null) {
99
      // http://developer.android.com/ndk/guides/abis.html (x86, armeabi-v7a, ...)
100 101 102 103 104 105 106 107 108 109
      switch (_getProperty('ro.product.cpu.abi')) {
        case 'x86_64':
          _platform = TargetPlatform.android_x64;
          break;
        case 'x86':
          _platform = TargetPlatform.android_x86;
          break;
        default:
          _platform = TargetPlatform.android_arm;
          break;
110
      }
111 112
    }

113
    return _platform;
114
  }
115

116
  _AdbLogReader _logReader;
117
  _AndroidDevicePortForwarder _portForwarder;
118

119
  List<String> adbCommandForDevice(List<String> args) {
120
    return <String>[getAdbPath(androidSdk), '-s', id]..addAll(args);
121 122 123 124
  }

  bool _isValidAdbVersion(String adbVersion) {
    // Sample output: 'Android Debug Bridge version 1.0.31'
125
    Match versionFields = new RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
    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;
    }
141
    printError(
142 143 144 145
        'Unrecognized adb version string $adbVersion. Skipping version check.');
    return true;
  }

146 147 148 149
  bool _checkForSupportedAdbVersion() {
    if (androidSdk == null)
      return false;

150
    try {
151
      String adbVersion = runCheckedSync(<String>[getAdbPath(androidSdk), 'version']);
152
      if (_isValidAdbVersion(adbVersion))
153
        return true;
154
      printError('The ADB at "${getAdbPath(androidSdk)}" is too old; please install version 1.0.32 or later.');
155 156
    } catch (error, trace) {
      printError('Error running ADB: $error', trace);
157
    }
158

159 160 161 162 163 164 165 166 167
    return false;
  }

  bool _checkForSupportedAndroidVersion() {
    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 *
168
      runCheckedSync(<String>[getAdbPath(androidSdk), 'start-server']);
169 170

      // Sample output: '22'
171
      String sdkVersion = _getProperty('ro.build.version.sdk');
172

173
      int sdkVersionParsed = int.parse(sdkVersion, onError: (String source) => null);
174
      if (sdkVersionParsed == null) {
175
        printError('Unexpected response from getprop: "$sdkVersion"');
176 177
        return false;
      }
178

179
      if (sdkVersionParsed < minApiLevel) {
180
        printError(
181 182 183 184
          'The Android version ($sdkVersion) on the target device is too old. Please '
          'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.');
        return false;
      }
185

186 187
      return true;
    } catch (e) {
188
      printError('Unexpected failure from adb: $e');
189
      return false;
190 191 192 193 194 195 196 197
    }
  }

  String _getDeviceSha1Path(ApplicationPackage app) {
    return '/data/local/tmp/sky.${app.id}.sha1';
  }

  String _getDeviceApkSha1(ApplicationPackage app) {
198
    return runCheckedSync(adbCommandForDevice(<String>['shell', 'cat', _getDeviceSha1Path(app)]));
199 200 201
  }

  String _getSourceSha1(ApplicationPackage app) {
202 203
    AndroidApk apk = app;
    File shaFile = new File('${apk.apkPath}.sha1');
Devon Carew's avatar
Devon Carew committed
204
    return shaFile.existsSync() ? shaFile.readAsStringSync() : '';
205 206
  }

207
  @override
208 209 210 211
  String get name => modelID;

  @override
  bool isAppInstalled(ApplicationPackage app) {
212
    // This call takes 400ms - 600ms.
213 214
    String listOut = runCheckedSync(adbCommandForDevice(<String>['shell', 'pm', 'list', 'packages', app.id]));
    if (!LineSplitter.split(listOut).contains("package:${app.id}"))
215 216 217
      return false;

    // Check the application SHA.
Devon Carew's avatar
Devon Carew committed
218
    return _getDeviceApkSha1(app) == _getSourceSha1(app);
219 220 221 222
  }

  @override
  bool installApp(ApplicationPackage app) {
223 224 225
    AndroidApk apk = app;
    if (!FileSystemEntity.isFileSync(apk.apkPath)) {
      printError('"${apk.apkPath}" does not exist.');
226 227 228
      return false;
    }

229 230 231
    if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion())
      return false;

232
    String installOut = runCheckedSync(adbCommandForDevice(<String>['install', '-r', apk.apkPath]));
233 234 235 236 237 238 239
    RegExp failureExp = new RegExp(r'^Failure.*$', multiLine: true);
    String failure = failureExp.stringMatch(installOut);
    if (failure != null) {
      printError('Package install error: $failure');
      return false;
    }

240
    runCheckedSync(adbCommandForDevice(<String>['shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app)]));
241 242 243
    return true;
  }

244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
  @override
  bool uninstallApp(ApplicationPackage app) {
    if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion())
      return false;

    String uninstallOut = runCheckedSync(adbCommandForDevice(<String>['uninstall', app.id]));
    RegExp failureExp = new RegExp(r'^Failure.*$', multiLine: true);
    String failure = failureExp.stringMatch(uninstallOut);
    if (failure != null) {
      printError('Package uninstall error: $failure');
      return false;
    }

    return true;
  }

260
  Future<Null> _forwardPort(String service, int devicePort, int port) async {
261
    try {
262
      // Set up port forwarding for observatory.
Devon Carew's avatar
Devon Carew committed
263 264
      port = await portForwarder.forward(devicePort, hostPort: port);
      printStatus('$service listening on http://127.0.0.1:$port');
265
    } catch (e) {
266
      printError('Unable to forward port $port: $e');
267 268 269
    }
  }

Devon Carew's avatar
Devon Carew committed
270
  Future<LaunchResult> startBundle(AndroidApk apk, String bundlePath, {
271 272
    bool traceStartup: false,
    String route,
Devon Carew's avatar
Devon Carew committed
273
    DebuggingOptions options
274
  }) async {
275
    printTrace('$this startBundle');
276

277 278 279 280 281
    if (bundlePath != null) {
      if (!FileSystemEntity.isFileSync(bundlePath)) {
        printError('Cannot find $bundlePath');
        return new LaunchResult.failed();
      }
282

283 284 285
      runCheckedSync(
          adbCommandForDevice(<String>['push', bundlePath, _deviceBundlePath]));
    }
286

287 288
    ProtocolDiscovery observatoryDiscovery;
    ProtocolDiscovery diagnosticDiscovery;
289

Devon Carew's avatar
Devon Carew committed
290
    if (options.debuggingEnabled) {
291 292
      observatoryDiscovery = new ProtocolDiscovery(logReader, ProtocolDiscovery.kObservatoryService);
      diagnosticDiscovery = new ProtocolDiscovery(logReader, ProtocolDiscovery.kDiagnosticService);
Devon Carew's avatar
Devon Carew committed
293
    }
294

295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
    List<String> cmd;

    if (bundlePath != null) {
      // Specify in the RUN intent the path to the local bundle pushed.
      cmd = adbCommandForDevice(<String>[
        'shell', 'am', 'start',
        '-a', 'android.intent.action.RUN',
        '-d', _deviceBundlePath,
        '-f', '0x20000000',  // FLAG_ACTIVITY_SINGLE_TOP
        '--ez', 'enable-background-compilation', 'true',
      ]);
    } else {
      cmd = adbCommandForDevice(<String>[
        'shell', 'am', 'start',
        '-a', 'android.intent.action.RUN',
        '-f', '0x20000000',  // FLAG_ACTIVITY_SINGLE_TOP
        '--ez', 'enable-background-compilation', 'true',
      ]);
    }

315
    if (traceStartup)
316
      cmd.addAll(<String>['--ez', 'trace-startup', 'true']);
317
    if (route != null)
318
      cmd.addAll(<String>['--es', 'route', route]);
Devon Carew's avatar
Devon Carew committed
319
    if (options.debuggingEnabled) {
320
      if (options.buildMode == BuildMode.debug)
Devon Carew's avatar
Devon Carew committed
321 322 323 324
        cmd.addAll(<String>['--ez', 'enable-checked-mode', 'true']);
      if (options.startPaused)
        cmd.addAll(<String>['--ez', 'start-paused', 'true']);
    }
325
    cmd.add(apk.launchActivity);
326 327 328 329
    String result = runCheckedSync(cmd);
    // This invocation returns 0 even when it fails.
    if (result.contains('Error: ')) {
      printError(result.trim());
Devon Carew's avatar
Devon Carew committed
330
      return new LaunchResult.failed();
331
    }
332

Devon Carew's avatar
Devon Carew committed
333 334 335 336 337 338
    if (!options.debuggingEnabled) {
      return new LaunchResult.succeeded();
    } else {
      // Wait for the service protocol port here. This will complete once the
      // device has printed "Observatory is listening on...".
      printTrace('Waiting for observatory port to be available...');
Devon Carew's avatar
Devon Carew committed
339

Devon Carew's avatar
Devon Carew committed
340
      try {
341 342 343 344 345 346 347 348 349 350 351 352 353
        int observatoryDevicePort, diagnosticDevicePort;

        if (options.buildMode == BuildMode.debug) {
          Future<List<int>> scrapeServicePorts = Future.wait(
            <Future<int>>[observatoryDiscovery.nextPort(), diagnosticDiscovery.nextPort()]
          );
          List<int> devicePorts = await scrapeServicePorts.timeout(new Duration(seconds: 20));
          observatoryDevicePort = devicePorts[0];
          diagnosticDevicePort = devicePorts[1];
        } else {
          observatoryDevicePort = await observatoryDiscovery.nextPort().timeout(new Duration(seconds: 20));
        }

Devon Carew's avatar
Devon Carew committed
354 355 356 357
        printTrace('observatory port = $observatoryDevicePort');
        int observatoryLocalPort = await options.findBestObservatoryPort();
        // TODO(devoncarew): Remember the forwarding information (so we can later remove the
        // port forwarding).
358
        await _forwardPort(ProtocolDiscovery.kObservatoryService, observatoryDevicePort, observatoryLocalPort);
359 360 361 362 363 364 365 366

        int diagnosticLocalPort;
        if (diagnosticDevicePort != null) {
          printTrace('diagnostic port = $diagnosticDevicePort');
          diagnosticLocalPort = await options.findBestDiagnosticPort();
          await _forwardPort(ProtocolDiscovery.kDiagnosticService, diagnosticDevicePort, diagnosticLocalPort);
        }

Devon Carew's avatar
Devon Carew committed
367 368 369 370 371 372 373 374 375 376 377 378 379 380
        return new LaunchResult.succeeded(
          observatoryPort: observatoryLocalPort,
          diagnosticPort: diagnosticLocalPort
        );
      } catch (error) {
        if (error is TimeoutException)
          printError('Timed out while waiting for a debug connection.');
        else
          printError('Error waiting for a debug connection: $error');
        return new LaunchResult.failed();
      } finally {
        observatoryDiscovery.cancel();
        diagnosticDiscovery.cancel();
      }
Devon Carew's avatar
Devon Carew committed
381
    }
382 383 384
  }

  @override
Devon Carew's avatar
Devon Carew committed
385
  Future<LaunchResult> startApp(
386 387
    ApplicationPackage package,
    BuildMode mode, {
388 389
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
390
    DebuggingOptions debuggingOptions,
391 392
    Map<String, dynamic> platformArgs,
    bool prebuiltApplication: false
393
  }) async {
394
    if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion())
Devon Carew's avatar
Devon Carew committed
395
      return new LaunchResult.failed();
396

397
    String localBundlePath;
398

399 400 401 402 403 404 405 406 407
    if (!prebuiltApplication) {
      localBundlePath = await flx.buildFlx(
        mainPath: mainPath,
        precompiledSnapshot: isAotBuildMode(debuggingOptions.buildMode),
        includeRobotoFonts: false
      );
      if (localBundlePath == null)
        return new LaunchResult.failed();
    }
408 409 410

    printTrace('Starting bundle for $this.');

Devon Carew's avatar
Devon Carew committed
411
    return startBundle(
Devon Carew's avatar
Devon Carew committed
412 413
      package,
      localBundlePath,
414
      traceStartup: platformArgs['trace-startup'] ?? false,
Devon Carew's avatar
Devon Carew committed
415
      route: route,
Devon Carew's avatar
Devon Carew committed
416 417
      options: debuggingOptions
    );
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
  @override
  bool get supportsHotMode => true;

  @override
  Future<bool> runFromFile(ApplicationPackage package,
                           String scriptUri,
                           String packagesUri) async {
    AndroidApk apk = package;
    List<String> cmd = adbCommandForDevice(<String>[
      'shell', 'am', 'start',
      '-a', 'android.intent.action.RUN',
      '-d', _deviceBundlePath,
      '-f', '0x20000000',  // FLAG_ACTIVITY_SINGLE_TOP
    ]);
    cmd.addAll(<String>['--es', 'file', scriptUri]);
    cmd.addAll(<String>['--es', 'packages', packagesUri]);
    cmd.add(apk.launchActivity);
    String result = runCheckedSync(cmd);
    if (result.contains('Error: ')) {
      printError(result.trim());
      return false;
    }
    return true;
  }

445 446 447
  @override
  bool get supportsRestart => true;

448 449 450 451 452
  @override
  Future<bool> restartApp(
    ApplicationPackage package,
    LaunchResult result, {
    String mainPath,
453 454
    VMService observatory,
    bool prebuiltApplication: false
455 456 457
  }) async {
    Directory tempDir = await Directory.systemTemp.createTemp('flutter_tools');

458 459 460 461
    if (prebuiltApplication) {
      return false;
    }

462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485
    try {
      String snapshotPath = path.join(tempDir.path, 'snapshot_blob.bin');
      int result = await flx.createSnapshot(mainPath: mainPath, snapshotPath: snapshotPath);

      if (result != 0) {
        printError('Failed to run the Flutter compiler; exit code: $result');
        return false;
      }

      AndroidApk apk = package;
      String androidActivity = apk.launchActivity;
      bool success = await refreshSnapshot(androidActivity, snapshotPath);

      if (!success) {
        printError('Error refreshing snapshot on $this.');
        return false;
      }

      return true;
    } finally {
      tempDir.deleteSync(recursive: true);
    }
  }

486
  @override
487 488 489
  Future<bool> stopApp(ApplicationPackage app) {
    List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]);
    return runCommandAndStreamOutput(command).then((int exitCode) => exitCode == 0);
490 491
  }

492
  @override
493
  void clearLogs() {
494
    runSync(adbCommandForDevice(<String>['logcat', '-c']));
495 496
  }

497
  @override
498 499 500 501 502
  DeviceLogReader get logReader {
    if (_logReader == null)
      _logReader = new _AdbLogReader(this);
    return _logReader;
  }
503

504
  @override
505 506 507 508 509 510 511
  DevicePortForwarder get portForwarder {
    if (_portForwarder == null)
      _portForwarder = new _AndroidDevicePortForwarder(this);

    return _portForwarder;
  }

512 513
  /// Return the most recent timestamp in the Android log or `null` if there is
  /// no available timestamp. The format can be passed to logcat's -T option.
514
  String get lastLogcatTimestamp {
515
    String output = runCheckedSync(adbCommandForDevice(<String>[
516
      'logcat', '-v', 'time', '-t', '1'
517
    ]));
518 519 520

    RegExp timeRegExp = new RegExp(r'^\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}', multiLine: true);
    Match timeMatch = timeRegExp.firstMatch(output);
521
    return timeMatch?.group(0);
522 523
  }

524
  @override
525 526
  bool isSupported() => true;

527
  Future<bool> refreshSnapshot(String activity, String snapshotPath) async {
528 529 530 531 532
    if (!FileSystemEntity.isFileSync(snapshotPath)) {
      printError('Cannot find $snapshotPath');
      return false;
    }

533 534 535 536 537 538 539
    RunResult result = await runAsync(
      adbCommandForDevice(<String>['push', snapshotPath, _deviceSnapshotPath])
    );
    if (result.exitCode != 0) {
      printStatus(result.toString());
      return false;
    }
540 541 542 543 544 545 546

    List<String> cmd = adbCommandForDevice(<String>[
      'shell', 'am', 'start',
      '-a', 'android.intent.action.RUN',
      '-d', _deviceBundlePath,
      '-f', '0x20000000',  // FLAG_ACTIVITY_SINGLE_TOP
      '--es', 'snapshot', _deviceSnapshotPath,
547
      activity,
548
    ]);
549 550 551 552 553
    result = await runAsync(cmd);
    if (result.exitCode != 0) {
      printStatus(result.toString());
      return false;
    }
554

555 556
    final RegExp errorRegExp = new RegExp(r'^Error: .*$', multiLine: true);
    Match errorMatch = errorRegExp.firstMatch(result.processResult.stdout);
557 558 559 560 561
    if (errorMatch != null) {
      printError(errorMatch.group(0));
      return false;
    }

562 563
    return true;
  }
Devon Carew's avatar
Devon Carew committed
564 565 566 567 568 569 570 571 572 573 574 575 576 577

  @override
  bool get supportsScreenshot => true;

  @override
  Future<bool> takeScreenshot(File outputFile) {
    const String remotePath = '/data/local/tmp/flutter_screenshot.png';

    runCheckedSync(adbCommandForDevice(<String>['shell', 'screencap', '-p', remotePath]));
    runCheckedSync(adbCommandForDevice(<String>['pull', remotePath, outputFile.path]));
    runCheckedSync(adbCommandForDevice(<String>['shell', 'rm', remotePath]));

    return new Future<bool>.value(true);
  }
578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599

  @override
  Future<List<DiscoveredApp>> discoverApps() {
    RegExp discoverExp = new RegExp(r'DISCOVER: (.*)');
    List<DiscoveredApp> result = <DiscoveredApp>[];
    StreamSubscription<String> logs = logReader.logLines.listen((String line) {
      Match match = discoverExp.firstMatch(line);
      if (match != null) {
        Map<String, dynamic> app = JSON.decode(match.group(1));
        result.add(new DiscoveredApp(app['id'], app['observatoryPort']));
      }
    });

    runCheckedSync(adbCommandForDevice(<String>[
      'shell', 'am', 'broadcast', '-a', 'io.flutter.view.DISCOVER'
    ]));

    return new Future<List<DiscoveredApp>>.delayed(new Duration(seconds: 1), () {
      logs.cancel();
      return result;
    });
  }
600
}
601

602 603 604 605 606 607 608 609
Map<String, String> parseAdbDeviceProperties(String str) {
  Map<String, String> properties = <String, String>{};
  final RegExp propertyExp = new RegExp(r'\[(.*?)\]: \[(.*?)\]');
  for (Match match in propertyExp.allMatches(str))
    properties[match.group(1)] = match.group(2);
  return properties;
}

610 611 612
// 015d172c98400a03       device usb:340787200X product:nakasi model:Nexus_7 device:grouper
final RegExp _kDeviceRegex = new RegExp(r'^(\S+)\s+(\S+)(.*)');

613 614 615 616
/// Return the list of connected ADB devices.
///
/// [mockAdbOutput] is public for testing.
List<AndroidDevice> getAdbDevices({ String mockAdbOutput }) {
617
  List<AndroidDevice> devices = <AndroidDevice>[];
618 619 620 621 622 623 624 625 626 627
  List<String> output;

  if (mockAdbOutput == null) {
    String adbPath = getAdbPath(androidSdk);
    if (adbPath == null)
      return <AndroidDevice>[];
    output = runSync(<String>[adbPath, 'devices', '-l']).trim().split('\n');
  } else {
    output = mockAdbOutput.trim().split('\n');
  }
628

629 630
  for (String line in output) {
    // Skip lines like: * daemon started successfully *
631 632 633 634 635 636
    if (line.startsWith('* daemon '))
      continue;

    if (line.startsWith('List of devices'))
      continue;

637 638 639
    if (_kDeviceRegex.hasMatch(line)) {
      Match match = _kDeviceRegex.firstMatch(line);

640
      String deviceID = match[1];
641
      String deviceState = match[2];
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656
      String rest = match[3];

      Map<String, String> info = <String, String>{};
      if (rest != null && rest.isNotEmpty) {
        rest = rest.trim();
        for (String data in rest.split(' ')) {
          if (data.contains(':')) {
            List<String> fields = data.split(':');
            info[fields[0]] = fields[1];
          }
        }
      }

      if (info['model'] != null)
        info['model'] = cleanAdbDeviceName(info['model']);
657 658 659 660 661 662 663 664 665

      if (deviceState == 'unauthorized') {
        printError(
          'Device $deviceID is not authorized.\n'
          'You might need to check your device for an authorization dialog.'
        );
      } else if (deviceState == 'offline') {
        printError('Device $deviceID is offline.');
      } else {
666 667 668 669 670 671
        devices.add(new AndroidDevice(
          deviceID,
          productID: info['product'],
          modelID: info['model'] ?? deviceID,
          deviceCodeName: info['device']
        ));
672
      }
673
    } else {
674
      printError(
675 676 677 678 679
        'Unexpected failure parsing device information from adb output:\n'
        '$line\n'
        'Please report a bug at https://github.com/flutter/flutter/issues/new');
    }
  }
680

681 682 683
  return devices;
}

684
/// A log reader that logs from `adb logcat`.
Devon Carew's avatar
Devon Carew committed
685
class _AdbLogReader extends DeviceLogReader {
Devon Carew's avatar
Devon Carew committed
686 687 688 689 690 691
  _AdbLogReader(this.device) {
    _linesController = new StreamController<String>.broadcast(
      onListen: _start,
      onCancel: _stop
    );
  }
Devon Carew's avatar
Devon Carew committed
692 693 694

  final AndroidDevice device;

695 696
  bool _lastWasFiltered = false;

Devon Carew's avatar
Devon Carew committed
697
  StreamController<String> _linesController;
698 699
  Process _process;

700
  @override
Devon Carew's avatar
Devon Carew committed
701
  Stream<String> get logLines => _linesController.stream;
702

703
  @override
704
  String get name => device.name;
Devon Carew's avatar
Devon Carew committed
705

Devon Carew's avatar
Devon Carew committed
706
  void _start() {
707
    // Start the adb logcat process.
708
    List<String> args = <String>['logcat', '-v', 'tag'];
709
    String lastTimestamp = device.lastLogcatTimestamp;
710 711 712 713 714 715 716 717 718 719 720 721 722 723
    if (lastTimestamp != null) {
      bool supportsLastTimestamp = false;

      // Check to see if this copy of adb supports -T.
      try {
        // "logcat: invalid option -- T", "Unrecognized Option"
        // logcat -g will finish immediately; it will print an error to stdout if -T isn't supported.
        String result = runSync(device.adbCommandForDevice(<String>['logcat', '-g', '-T', lastTimestamp]));
        supportsLastTimestamp = !result.contains('logcat: invalid option') && !result.contains('Unrecognized Option');
      } catch (_) { }

      if (supportsLastTimestamp)
        args.addAll(<String>['-T', lastTimestamp]);
    }
Devon Carew's avatar
Devon Carew committed
724 725 726 727 728 729 730 731 732 733
    runCommand(device.adbCommandForDevice(args)).then((Process process) {
      _process = process;
      _process.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
      _process.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);

      _process.exitCode.then((int code) {
        if (_linesController.hasListener)
          _linesController.close();
      });
    });
734 735
  }

736 737 738 739 740
  // 'W/ActivityManager: '
  static final RegExp _logFormat = new RegExp(r'^[VDIWEF]\/[^:]+:\s+');

  static final List<RegExp> _whitelistedTags = <RegExp>[
    new RegExp(r'^[VDIWEF]\/flutter[^:]*:\s+', caseSensitive: false),
741
    new RegExp(r'^[IE]\/DartVM[^:]*:\s+'),
742 743 744 745 746 747
    new RegExp(r'^[WEF]\/AndroidRuntime:\s+'),
    new RegExp(r'^[WEF]\/ActivityManager:\s+'),
    new RegExp(r'^[WEF]\/System\.err:\s+'),
    new RegExp(r'^[F]\/[\S^:]+:\s+')
  ];

748
  void _onLine(String line) {
749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768
    if (_logFormat.hasMatch(line)) {
      // Filter out some noisy ActivityManager notifications.
      if (line.startsWith('W/ActivityManager: getRunningAppProcesses'))
        return;

      // Filter on approved names and levels.
      for (RegExp regex in _whitelistedTags) {
        if (regex.hasMatch(line)) {
          _lastWasFiltered = false;
          _linesController.add(line);
          return;
        }
      }

      _lastWasFiltered = true;
    } else {
      // If it doesn't match the log pattern at all, pass it through.
      if (!_lastWasFiltered)
        _linesController.add(line);
    }
Devon Carew's avatar
Devon Carew committed
769 770
  }

Devon Carew's avatar
Devon Carew committed
771 772
  void _stop() {
    // TODO(devoncarew): We should remove adb port forwarding here.
Devon Carew's avatar
Devon Carew committed
773

Devon Carew's avatar
Devon Carew committed
774
    _process?.kill();
Devon Carew's avatar
Devon Carew committed
775 776
  }
}
777 778 779 780 781 782 783 784 785 786

class _AndroidDevicePortForwarder extends DevicePortForwarder {
  _AndroidDevicePortForwarder(this.device);

  final AndroidDevice device;

  static int _extractPort(String portString) {
    return int.parse(portString.trim(), onError: (_) => null);
  }

787
  @override
788 789 790
  List<ForwardedPort> get forwardedPorts {
    final List<ForwardedPort> ports = <ForwardedPort>[];

791 792 793
    String stdout = runCheckedSync(device.adbCommandForDevice(
      <String>['forward', '--list']
    ));
794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818

    List<String> lines = LineSplitter.split(stdout).toList();
    for (String line in lines) {
      if (line.startsWith(device.id)) {
        List<String> splitLine = line.split("tcp:");

        // Sanity check splitLine.
        if (splitLine.length != 3)
          continue;

        // Attempt to extract ports.
        int hostPort = _extractPort(splitLine[1]);
        int devicePort = _extractPort(splitLine[2]);

        // Failed, skip.
        if ((hostPort == null) || (devicePort == null))
          continue;

        ports.add(new ForwardedPort(hostPort, devicePort));
      }
    }

    return ports;
  }

819
  @override
820
  Future<int> forward(int devicePort, { int hostPort }) async {
821 822 823 824 825
    if ((hostPort == null) || (hostPort == 0)) {
      // Auto select host port.
      hostPort = await findAvailablePort();
    }

826 827 828
    runCheckedSync(device.adbCommandForDevice(
      <String>['forward', 'tcp:$hostPort', 'tcp:$devicePort']
    ));
829 830 831 832

    return hostPort;
  }

833
  @override
Ian Hickson's avatar
Ian Hickson committed
834
  Future<Null> unforward(ForwardedPort forwardedPort) async {
835 836 837
    runCheckedSync(device.adbCommandForDevice(
      <String>['forward', '--remove', 'tcp:${forwardedPort.hostPort}']
    ));
838 839
  }
}