android_device.dart 22.2 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 '../android/android_sdk.dart';
9
import '../application_package.dart';
10
import '../base/file_system.dart';
11
import '../base/io.dart';
12
import '../base/logger.dart';
13
import '../base/os.dart';
14
import '../base/process.dart';
15
import '../base/process_manager.dart';
16
import '../build_info.dart';
17
import '../commands/build_apk.dart';
18
import '../device.dart';
19
import '../globals.dart';
20
import '../protocol_discovery.dart';
21

22
import 'adb.dart';
23
import 'android.dart';
24
import 'android_sdk.dart';
25

26 27
const String _defaultAdbPath = 'adb';

28 29
class AndroidDevices extends PollingDeviceDiscovery {
  AndroidDevices() : super('AndroidDevices');
30

31
  @override
32
  bool get supportsPlatform => true;
33 34

  @override
35
  List<Device> pollingGetDevices() => getAdbDevices();
36 37
}

38
class AndroidDevice extends Device {
39 40 41 42
  AndroidDevice(
    String id, {
    this.productID,
    this.modelID,
43 44
    this.deviceCodeName
  }) : super(id);
45

46 47 48 49
  final String productID;
  final String modelID;
  final String deviceCodeName;

50
  Map<String, String> _properties;
51
  bool _isLocalEmulator;
52 53 54 55 56
  TargetPlatform _platform;

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

58 59
      List<String> propCommand = adbCommandForDevice(<String>['shell', 'getprop']);
      printTrace(propCommand.join(' '));
60 61 62 63

      try {
        // We pass an encoding of LATIN1 so that we don't try and interpret the
        // `adb shell getprop` result as UTF8.
64
        ProcessResult result = processManager.runSync(
65 66 67 68 69 70 71 72 73 74 75
          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');
76 77
      }
    }
78

79 80
    return _properties[name];
  }
81

82
  @override
83 84
  bool get isLocalEmulator {
    if (_isLocalEmulator == null) {
85 86 87 88 89 90 91 92 93
      String characteristics = _getProperty('ro.build.characteristics');
      _isLocalEmulator = characteristics != null && characteristics.contains('emulator');
    }
    return _isLocalEmulator;
  }

  @override
  TargetPlatform get platform {
    if (_platform == null) {
94
      // http://developer.android.com/ndk/guides/abis.html (x86, armeabi-v7a, ...)
95 96 97 98 99 100 101 102 103 104
      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;
105
      }
106 107
    }

108
    return _platform;
109
  }
110

111 112 113 114 115 116 117
  @override
  String get sdkNameAndVersion => 'Android $_sdkVersion (API $_apiVersion)';

  String get _sdkVersion => _getProperty('ro.build.version.release');

  String get _apiVersion => _getProperty('ro.build.version.sdk');

118
  _AdbLogReader _logReader;
119
  _AndroidDevicePortForwarder _portForwarder;
120

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

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

148 149 150 151
  bool _checkForSupportedAdbVersion() {
    if (androidSdk == null)
      return false;

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

161 162 163 164 165 166 167 168 169
    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 *
170
      runCheckedSync(<String>[getAdbPath(androidSdk), 'start-server']);
171 172

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

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

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

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

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

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

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

209
  @override
210 211 212 213
  String get name => modelID;

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

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

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

231 232 233
    if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion())
      return false;

234
    Status status = logger.startProgress('Installing ${apk.apkPath}...');
235
    String installOut = runCheckedSync(adbCommandForDevice(<String>['install', '-r', apk.apkPath]));
Devon Carew's avatar
Devon Carew committed
236
    status.stop();
237 238 239 240 241 242 243
    RegExp failureExp = new RegExp(r'^Failure.*$', multiLine: true);
    String failure = failureExp.stringMatch(installOut);
    if (failure != null) {
      printError('Package install error: $failure');
      return false;
    }

244
    runCheckedSync(adbCommandForDevice(<String>['shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app)]));
245 246 247
    return true;
  }

248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
  @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;
  }

264 265 266 267 268
  @override
  Future<LaunchResult> startApp(
    ApplicationPackage package,
    BuildMode mode, {
    String mainPath,
269
    String route,
270 271
    DebuggingOptions debuggingOptions,
    Map<String, dynamic> platformArgs,
272 273
    bool prebuiltApplication: false,
    bool applicationNeedsRebuild: false,
274
  }) async {
275 276
    if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion())
      return new LaunchResult.failed();
277

278 279 280 281 282 283 284
    printTrace("Stopping app '${package.name}' on $name.");
    await stopApp(package);

    if (!prebuiltApplication) {
      printTrace('Building APK');
      await buildApk(platform,
          target: mainPath,
285 286
          buildMode: debuggingOptions.buildMode,
          applicationNeedsRebuild: applicationNeedsRebuild
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
      );
    }

    if (isAppInstalled(package)) {
      printStatus('Uninstalling old version...');
      if (!uninstallApp(package))
        printError('Warning: uninstalling old version failed');
    }

    printTrace('Installing APK.');
    if (!installApp(package)) {
      printTrace('Error: Failed to install APK.');
      return new LaunchResult.failed();
    }

302 303 304
    final bool traceStartup = platformArgs['trace-startup'] ?? false;
    final AndroidApk apk = package;
    printTrace('$this startApp');
305

306 307
    ProtocolDiscovery observatoryDiscovery;
    ProtocolDiscovery diagnosticDiscovery;
308

309
    if (debuggingOptions.debuggingEnabled) {
310 311 312 313 314 315
      // TODO(devoncarew): Remember the forwarding information (so we can later remove the
      // port forwarding).
      observatoryDiscovery = new ProtocolDiscovery.observatory(
        getLogReader(), portForwarder: portForwarder, hostPort: debuggingOptions.observatoryPort);
      diagnosticDiscovery = new ProtocolDiscovery.diagnosticService(
        getLogReader(), portForwarder: portForwarder, hostPort: debuggingOptions.diagnosticPort);
Devon Carew's avatar
Devon Carew committed
316
    }
317

318 319
    List<String> cmd;

320 321 322 323 324
    cmd = adbCommandForDevice(<String>[
      'shell', 'am', 'start',
      '-a', 'android.intent.action.RUN',
      '-f', '0x20000000',  // FLAG_ACTIVITY_SINGLE_TOP
      '--ez', 'enable-background-compilation', 'true',
325
      '--ez', 'enable-dart-profiling', 'true',
326
    ]);
327

328
    if (traceStartup)
329
      cmd.addAll(<String>['--ez', 'trace-startup', 'true']);
330
    if (route != null)
331
      cmd.addAll(<String>['--es', 'route', route]);
332 333
    if (debuggingOptions.debuggingEnabled) {
      if (debuggingOptions.buildMode == BuildMode.debug)
Devon Carew's avatar
Devon Carew committed
334
        cmd.addAll(<String>['--ez', 'enable-checked-mode', 'true']);
335
      if (debuggingOptions.startPaused)
Devon Carew's avatar
Devon Carew committed
336 337
        cmd.addAll(<String>['--ez', 'start-paused', 'true']);
    }
338
    cmd.add(apk.launchActivity);
339 340 341 342
    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
343
      return new LaunchResult.failed();
344
    }
345

346
    if (!debuggingOptions.debuggingEnabled) {
Devon Carew's avatar
Devon Carew committed
347
      return new LaunchResult.succeeded();
348
    }
Devon Carew's avatar
Devon Carew committed
349

350 351 352
    // 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...');
353

354 355 356 357
    // TODO(danrubel) Waiting for observatory and diagnostic services
    // can be made common across all devices.
    try {
      Uri observatoryUri, diagnosticUri;
358

359 360 361
      if (debuggingOptions.buildMode == BuildMode.debug) {
        List<Uri> deviceUris = await Future.wait(
            <Future<Uri>>[observatoryDiscovery.nextUri(), diagnosticDiscovery.nextUri()]
Devon Carew's avatar
Devon Carew committed
362
        );
363 364 365 366
        observatoryUri = deviceUris[0];
        diagnosticUri = deviceUris[1];
      } else if (debuggingOptions.buildMode == BuildMode.profile) {
        observatoryUri = await observatoryDiscovery.nextUri();
Devon Carew's avatar
Devon Carew committed
367
      }
368 369 370 371 372 373 374 375 376 377 378

      return new LaunchResult.succeeded(
          observatoryUri: observatoryUri,
          diagnosticUri: diagnosticUri,
      );
    } catch (error) {
      printError('Error waiting for a debug connection: $error');
      return new LaunchResult.failed();
    } finally {
      observatoryDiscovery.cancel();
      diagnosticDiscovery.cancel();
Devon Carew's avatar
Devon Carew committed
379
    }
380 381
  }

382 383 384
  @override
  bool get supportsHotMode => true;

385
  @override
386 387 388
  Future<bool> stopApp(ApplicationPackage app) {
    List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]);
    return runCommandAndStreamOutput(command).then((int exitCode) => exitCode == 0);
389 390
  }

391
  @override
392
  void clearLogs() {
393
    runSync(adbCommandForDevice(<String>['logcat', '-c']));
394 395
  }

396
  @override
397 398 399
  DeviceLogReader getLogReader({ApplicationPackage app}) {
    // The Android log reader isn't app-specific.
    _logReader ??= new _AdbLogReader(this);
400 401
    return _logReader;
  }
402

403
  @override
404 405 406 407 408 409 410
  DevicePortForwarder get portForwarder {
    if (_portForwarder == null)
      _portForwarder = new _AndroidDevicePortForwarder(this);

    return _portForwarder;
  }

411 412
  static RegExp _timeRegExp = new RegExp(r'^\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}', multiLine: true);

413 414
  /// 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.
415
  String get lastLogcatTimestamp {
416
    String output = runCheckedSync(adbCommandForDevice(<String>[
417
      'logcat', '-v', 'time', '-t', '1'
418
    ]));
419

420
    Match timeMatch = _timeRegExp.firstMatch(output);
421
    return timeMatch?.group(0);
422 423
  }

424
  @override
425 426
  bool isSupported() => true;

Devon Carew's avatar
Devon Carew committed
427 428 429 430 431 432 433 434 435 436 437 438 439
  @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);
  }
440 441 442 443 444

  @override
  Future<List<DiscoveredApp>> discoverApps() {
    RegExp discoverExp = new RegExp(r'DISCOVER: (.*)');
    List<DiscoveredApp> result = <DiscoveredApp>[];
445
    StreamSubscription<String> logs = getLogReader().logLines.listen((String line) {
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461
      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;
    });
  }
462
}
463

464 465 466 467 468 469 470 471
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;
}

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

475 476 477 478
/// Return the list of connected ADB devices.
///
/// [mockAdbOutput] is public for testing.
List<AndroidDevice> getAdbDevices({ String mockAdbOutput }) {
479
  List<AndroidDevice> devices = <AndroidDevice>[];
480
  String text;
481 482 483 484 485

  if (mockAdbOutput == null) {
    String adbPath = getAdbPath(androidSdk);
    if (adbPath == null)
      return <AndroidDevice>[];
486
    text = runSync(<String>[adbPath, 'devices', '-l']);
487
  } else {
488
    text = mockAdbOutput;
489
  }
490

491 492 493 494 495 496 497
  // Check for error messages from adb
  if (!text.contains('List of devices')) {
    printError(text);
    return <AndroidDevice>[];
  }

  for (String line in text.trim().split('\n')) {
498
    // Skip lines like: * daemon started successfully *
499 500 501 502 503 504
    if (line.startsWith('* daemon '))
      continue;

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

505 506 507
    if (_kDeviceRegex.hasMatch(line)) {
      Match match = _kDeviceRegex.firstMatch(line);

508
      String deviceID = match[1];
509
      String deviceState = match[2];
510 511 512 513 514 515 516 517 518 519 520 521 522 523 524
      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']);
525 526 527 528 529 530 531 532 533

      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 {
534 535 536 537 538 539
        devices.add(new AndroidDevice(
          deviceID,
          productID: info['product'],
          modelID: info['model'] ?? deviceID,
          deviceCodeName: info['device']
        ));
540
      }
541
    } else {
542
      printError(
543 544 545 546 547
        'Unexpected failure parsing device information from adb output:\n'
        '$line\n'
        'Please report a bug at https://github.com/flutter/flutter/issues/new');
    }
  }
548

549 550 551
  return devices;
}

552
/// A log reader that logs from `adb logcat`.
Devon Carew's avatar
Devon Carew committed
553
class _AdbLogReader extends DeviceLogReader {
Devon Carew's avatar
Devon Carew committed
554 555 556 557 558 559
  _AdbLogReader(this.device) {
    _linesController = new StreamController<String>.broadcast(
      onListen: _start,
      onCancel: _stop
    );
  }
Devon Carew's avatar
Devon Carew committed
560 561 562

  final AndroidDevice device;

Devon Carew's avatar
Devon Carew committed
563
  StreamController<String> _linesController;
564
  Process _process;
565

566
  @override
Devon Carew's avatar
Devon Carew committed
567
  Stream<String> get logLines => _linesController.stream;
568

569
  @override
570
  String get name => device.name;
Devon Carew's avatar
Devon Carew committed
571

572 573 574 575 576 577 578 579 580 581
  DateTime _timeOrigin;

  DateTime _adbTimestampToDateTime(String adbTimestamp) {
    // The adb timestamp format is: mm-dd hours:minutes:seconds.milliseconds
    // Dart's DateTime parse function accepts this format so long as we provide
    // the year, resulting in:
    // yyyy-mm-dd hours:minutes:seconds.milliseconds.
    return DateTime.parse('${new DateTime.now().year}-$adbTimestamp');
  }

Devon Carew's avatar
Devon Carew committed
582
  void _start() {
583
    // Start the adb logcat process.
584
    List<String> args = <String>['logcat', '-v', 'time'];
585
    String lastTimestamp = device.lastLogcatTimestamp;
586 587 588 589
    if (lastTimestamp != null)
        _timeOrigin = _adbTimestampToDateTime(lastTimestamp);
    else
        _timeOrigin = null;
590
    runCommand(device.adbCommandForDevice(args)).then<Null>((Process process) {
Devon Carew's avatar
Devon Carew committed
591 592 593
      _process = process;
      _process.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
      _process.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
594
      _process.exitCode.whenComplete(() {
Devon Carew's avatar
Devon Carew committed
595 596 597 598
        if (_linesController.hasListener)
          _linesController.close();
      });
    });
599 600
  }

601
  // 'W/ActivityManager: '
602
  static final RegExp _logFormat = new RegExp(r'^[VDIWEF]\/.{8,}:\s');
603 604 605

  static final List<RegExp> _whitelistedTags = <RegExp>[
    new RegExp(r'^[VDIWEF]\/flutter[^:]*:\s+', caseSensitive: false),
606
    new RegExp(r'^[IE]\/DartVM[^:]*:\s+'),
607
    new RegExp(r'^[WEF]\/AndroidRuntime:\s+'),
608
    new RegExp(r'^[WEF]\/ActivityManager:\s+.*(\bflutter\b|\bdomokit\b|\bsky\b)'),
609 610 611 612
    new RegExp(r'^[WEF]\/System\.err:\s+'),
    new RegExp(r'^[F]\/[\S^:]+:\s+')
  ];

613 614 615
  // we default to true in case none of the log lines match
  bool _acceptedLastLine = true;

616 617 618
  // The format of the line is controlled by the '-v' parameter passed to
  // adb logcat. We are currently passing 'time', which has the format:
  // mm-dd hh:mm:ss.milliseconds Priority/Tag( PID): ....
619
  void _onLine(String line) {
620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636
    final Match timeMatch = AndroidDevice._timeRegExp.firstMatch(line);
    if (timeMatch == null) {
      return;
    }
    if (_timeOrigin != null) {
      final String timestamp = timeMatch.group(0);
      DateTime time = _adbTimestampToDateTime(timestamp);
      if (time.isBefore(_timeOrigin)) {
        // Ignore log messages before the origin.
        return;
      }
    }
    if (line.length == timeMatch.end) {
      return;
    }
    // Chop off the time.
    line = line.substring(timeMatch.end + 1);
637 638 639 640
    if (_logFormat.hasMatch(line)) {
      // Filter on approved names and levels.
      for (RegExp regex in _whitelistedTags) {
        if (regex.hasMatch(line)) {
641
          _acceptedLastLine = true;
642 643 644 645
          _linesController.add(line);
          return;
        }
      }
646 647 648 649 650
      _acceptedLastLine = false;
    } else if (line == '--------- beginning of system' ||
               line == '--------- beginning of main' ) {
      // hide the ugly adb logcat log boundaries at the start
      _acceptedLastLine = false;
651
    } else {
652 653 654
      // If it doesn't match the log pattern at all, then pass it through if we
      // passed the last matching line through. It might be a multiline message.
      if (_acceptedLastLine) {
655
        _linesController.add(line);
656 657
        return;
      }
658
    }
Devon Carew's avatar
Devon Carew committed
659 660
  }

Devon Carew's avatar
Devon Carew committed
661 662
  void _stop() {
    // TODO(devoncarew): We should remove adb port forwarding here.
Devon Carew's avatar
Devon Carew committed
663

Devon Carew's avatar
Devon Carew committed
664
    _process?.kill();
Devon Carew's avatar
Devon Carew committed
665 666
  }
}
667 668 669 670 671 672 673 674 675 676

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

  final AndroidDevice device;

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

677
  @override
678 679 680
  List<ForwardedPort> get forwardedPorts {
    final List<ForwardedPort> ports = <ForwardedPort>[];

681 682 683
    String stdout = runCheckedSync(device.adbCommandForDevice(
      <String>['forward', '--list']
    ));
684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708

    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;
  }

709
  @override
710
  Future<int> forward(int devicePort, { int hostPort }) async {
711 712 713 714 715
    if ((hostPort == null) || (hostPort == 0)) {
      // Auto select host port.
      hostPort = await findAvailablePort();
    }

716 717 718
    runCheckedSync(device.adbCommandForDevice(
      <String>['forward', 'tcp:$hostPort', 'tcp:$devicePort']
    ));
719 720 721 722

    return hostPort;
  }

723
  @override
Ian Hickson's avatar
Ian Hickson committed
724
  Future<Null> unforward(ForwardedPort forwardedPort) async {
725 726 727
    runCheckedSync(device.adbCommandForDevice(
      <String>['forward', '--remove', 'tcp:${forwardedPort.hostPort}']
    ));
728 729
  }
}