android_device.dart 22.9 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 '../observatory.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 65 66
      List<String> propCommand = adbCommandForDevice(<String>['shell', 'getprop']);
      printTrace(propCommand.join(' '));
      ProcessResult result = Process.runSync(propCommand.first, propCommand.sublist(1));
      if (result.exitCode == 0) {
67
        RegExp propertyExp = new RegExp(r'\[(.*?)\]: \[(.*?)\]');
68 69 70 71
        for (Match match in propertyExp.allMatches(result.stdout))
          _properties[match.group(1)] = match.group(2);
      } else {
        printError('Error retrieving device properties for $name.');
72 73
      }
    }
74

75 76
    return _properties[name];
  }
77

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

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

104
    return _platform;
105
  }
106

107
  _AdbLogReader _logReader;
108
  _AndroidDevicePortForwarder _portForwarder;
109

110
  List<String> adbCommandForDevice(List<String> args) {
111
    return <String>[getAdbPath(androidSdk), '-s', id]..addAll(args);
112 113 114 115
  }

  bool _isValidAdbVersion(String adbVersion) {
    // Sample output: 'Android Debug Bridge version 1.0.31'
116
    Match versionFields = new RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
    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;
    }
132
    printError(
133 134 135 136
        'Unrecognized adb version string $adbVersion. Skipping version check.');
    return true;
  }

137 138 139 140
  bool _checkForSupportedAdbVersion() {
    if (androidSdk == null)
      return false;

141
    try {
142
      String adbVersion = runCheckedSync(<String>[getAdbPath(androidSdk), 'version']);
143
      if (_isValidAdbVersion(adbVersion))
144
        return true;
145
      printError('The ADB at "${getAdbPath(androidSdk)}" is too old; please install version 1.0.32 or later.');
146 147
    } catch (error, trace) {
      printError('Error running ADB: $error', trace);
148
    }
149

150 151 152 153 154 155 156 157 158
    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 *
159
      runCheckedSync(<String>[getAdbPath(androidSdk), 'start-server']);
160 161

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

164
      int sdkVersionParsed = int.parse(sdkVersion, onError: (String source) => null);
165
      if (sdkVersionParsed == null) {
166
        printError('Unexpected response from getprop: "$sdkVersion"');
167 168
        return false;
      }
169

170
      if (sdkVersionParsed < minApiLevel) {
171
        printError(
172 173 174 175
          'The Android version ($sdkVersion) on the target device is too old. Please '
          'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.');
        return false;
      }
176

177 178
      return true;
    } catch (e) {
179
      printError('Unexpected failure from adb: $e');
180
      return false;
181 182 183 184 185 186 187 188
    }
  }

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

  String _getDeviceApkSha1(ApplicationPackage app) {
189
    return runCheckedSync(adbCommandForDevice(<String>['shell', 'cat', _getDeviceSha1Path(app)]));
190 191 192
  }

  String _getSourceSha1(ApplicationPackage app) {
193 194
    AndroidApk apk = app;
    File shaFile = new File('${apk.apkPath}.sha1');
Devon Carew's avatar
Devon Carew committed
195
    return shaFile.existsSync() ? shaFile.readAsStringSync() : '';
196 197
  }

198
  @override
199 200 201 202
  String get name => modelID;

  @override
  bool isAppInstalled(ApplicationPackage app) {
203
    // This call takes 400ms - 600ms.
204 205
    String listOut = runCheckedSync(adbCommandForDevice(<String>['shell', 'pm', 'list', 'packages', app.id]));
    if (!LineSplitter.split(listOut).contains("package:${app.id}"))
206 207 208
      return false;

    // Check the application SHA.
Devon Carew's avatar
Devon Carew committed
209
    return _getDeviceApkSha1(app) == _getSourceSha1(app);
210 211 212 213
  }

  @override
  bool installApp(ApplicationPackage app) {
214 215 216
    AndroidApk apk = app;
    if (!FileSystemEntity.isFileSync(apk.apkPath)) {
      printError('"${apk.apkPath}" does not exist.');
217 218 219
      return false;
    }

220 221 222
    if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion())
      return false;

223
    String installOut = runCheckedSync(adbCommandForDevice(<String>['install', '-r', apk.apkPath]));
224 225 226 227 228 229 230
    RegExp failureExp = new RegExp(r'^Failure.*$', multiLine: true);
    String failure = failureExp.stringMatch(installOut);
    if (failure != null) {
      printError('Package install error: $failure');
      return false;
    }

231
    runCheckedSync(adbCommandForDevice(<String>['shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app)]));
232 233 234
    return true;
  }

235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
  @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;
  }

251
  Future<Null> _forwardPort(String service, int devicePort, int port) async {
252
    try {
253
      // Set up port forwarding for observatory.
Devon Carew's avatar
Devon Carew committed
254 255
      port = await portForwarder.forward(devicePort, hostPort: port);
      printStatus('$service listening on http://127.0.0.1:$port');
256
    } catch (e) {
257
      printError('Unable to forward port $port: $e');
258 259 260
    }
  }

Devon Carew's avatar
Devon Carew committed
261
  Future<LaunchResult> startBundle(AndroidApk apk, String bundlePath, {
262 263
    bool traceStartup: false,
    String route,
Devon Carew's avatar
Devon Carew committed
264
    DebuggingOptions options
265
  }) async {
266
    printTrace('$this startBundle');
267 268

    if (!FileSystemEntity.isFileSync(bundlePath)) {
269
      printError('Cannot find $bundlePath');
Devon Carew's avatar
Devon Carew committed
270
      return new LaunchResult.failed();
271 272
    }

273
    runCheckedSync(adbCommandForDevice(<String>['push', bundlePath, _deviceBundlePath]));
274

275 276
    ProtocolDiscovery observatoryDiscovery;
    ProtocolDiscovery diagnosticDiscovery;
277

Devon Carew's avatar
Devon Carew committed
278
    if (options.debuggingEnabled) {
279 280
      observatoryDiscovery = new ProtocolDiscovery(logReader, ProtocolDiscovery.kObservatoryService);
      diagnosticDiscovery = new ProtocolDiscovery(logReader, ProtocolDiscovery.kDiagnosticService);
Devon Carew's avatar
Devon Carew committed
281
    }
282

283
    List<String> cmd = adbCommandForDevice(<String>[
284 285
      'shell', 'am', 'start',
      '-a', 'android.intent.action.RUN',
286
      '-d', _deviceBundlePath,
287
      '-f', '0x20000000',  // FLAG_ACTIVITY_SINGLE_TOP
288
      '--ez', 'enable-background-compilation', 'true',
289 290
    ]);
    if (traceStartup)
291
      cmd.addAll(<String>['--ez', 'trace-startup', 'true']);
292
    if (route != null)
293
      cmd.addAll(<String>['--es', 'route', route]);
Devon Carew's avatar
Devon Carew committed
294
    if (options.debuggingEnabled) {
295
      if (options.buildMode == BuildMode.debug)
Devon Carew's avatar
Devon Carew committed
296 297 298 299
        cmd.addAll(<String>['--ez', 'enable-checked-mode', 'true']);
      if (options.startPaused)
        cmd.addAll(<String>['--ez', 'start-paused', 'true']);
    }
300
    cmd.add(apk.launchActivity);
301 302 303 304
    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
305
      return new LaunchResult.failed();
306
    }
307

Devon Carew's avatar
Devon Carew committed
308 309 310 311 312 313
    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
314

Devon Carew's avatar
Devon Carew committed
315
      try {
316 317 318 319 320 321 322 323 324 325 326 327 328
        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
329 330 331 332
        printTrace('observatory port = $observatoryDevicePort');
        int observatoryLocalPort = await options.findBestObservatoryPort();
        // TODO(devoncarew): Remember the forwarding information (so we can later remove the
        // port forwarding).
333
        await _forwardPort(ProtocolDiscovery.kObservatoryService, observatoryDevicePort, observatoryLocalPort);
334 335 336 337 338 339 340 341

        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
342 343 344 345 346 347 348 349 350 351 352 353 354 355
        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
356
    }
357 358 359
  }

  @override
Devon Carew's avatar
Devon Carew committed
360
  Future<LaunchResult> startApp(
361 362
    ApplicationPackage package,
    BuildMode mode, {
363 364
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
365
    DebuggingOptions debuggingOptions,
366
    Map<String, dynamic> platformArgs
367
  }) async {
368
    if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion())
Devon Carew's avatar
Devon Carew committed
369
      return new LaunchResult.failed();
370

Devon Carew's avatar
Devon Carew committed
371
    String localBundlePath = await flx.buildFlx(
372
      mainPath: mainPath,
373
      precompiledSnapshot: isAotBuildMode(debuggingOptions.buildMode),
374
      includeRobotoFonts: false
375
    );
376 377 378

    if (localBundlePath == null)
      return new LaunchResult.failed();
379 380 381

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

Devon Carew's avatar
Devon Carew committed
382
    return startBundle(
Devon Carew's avatar
Devon Carew committed
383 384 385 386
      package,
      localBundlePath,
      traceStartup: platformArgs['trace-startup'],
      route: route,
Devon Carew's avatar
Devon Carew committed
387 388
      options: debuggingOptions
    );
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 415 416 417 418 419 420 421 422 423
  @override
  Future<bool> restartApp(
    ApplicationPackage package,
    LaunchResult result, {
    String mainPath,
    Observatory observatory
  }) async {
    Directory tempDir = await Directory.systemTemp.createTemp('flutter_tools');

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

424
  @override
425 426 427
  Future<bool> stopApp(ApplicationPackage app) {
    List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]);
    return runCommandAndStreamOutput(command).then((int exitCode) => exitCode == 0);
428 429
  }

430
  @override
431
  void clearLogs() {
432
    runSync(adbCommandForDevice(<String>['logcat', '-c']));
433 434
  }

435
  @override
436 437 438 439 440
  DeviceLogReader get logReader {
    if (_logReader == null)
      _logReader = new _AdbLogReader(this);
    return _logReader;
  }
441

442
  @override
443 444 445 446 447 448 449
  DevicePortForwarder get portForwarder {
    if (_portForwarder == null)
      _portForwarder = new _AndroidDevicePortForwarder(this);

    return _portForwarder;
  }

450 451
  /// 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.
452
  String get lastLogcatTimestamp {
453
    String output = runCheckedSync(adbCommandForDevice(<String>[
454
      'logcat', '-v', 'time', '-t', '1'
455
    ]));
456 457 458

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

462
  @override
463 464
  bool isSupported() => true;

465
  Future<bool> refreshSnapshot(String activity, String snapshotPath) async {
466 467 468 469 470
    if (!FileSystemEntity.isFileSync(snapshotPath)) {
      printError('Cannot find $snapshotPath');
      return false;
    }

471 472 473 474 475 476 477
    RunResult result = await runAsync(
      adbCommandForDevice(<String>['push', snapshotPath, _deviceSnapshotPath])
    );
    if (result.exitCode != 0) {
      printStatus(result.toString());
      return false;
    }
478 479 480 481 482 483 484

    List<String> cmd = adbCommandForDevice(<String>[
      'shell', 'am', 'start',
      '-a', 'android.intent.action.RUN',
      '-d', _deviceBundlePath,
      '-f', '0x20000000',  // FLAG_ACTIVITY_SINGLE_TOP
      '--es', 'snapshot', _deviceSnapshotPath,
485
      activity,
486
    ]);
487 488 489 490 491
    result = await runAsync(cmd);
    if (result.exitCode != 0) {
      printStatus(result.toString());
      return false;
    }
492

493 494
    final RegExp errorRegExp = new RegExp(r'^Error: .*$', multiLine: true);
    Match errorMatch = errorRegExp.firstMatch(result.processResult.stdout);
495 496 497 498 499
    if (errorMatch != null) {
      printError(errorMatch.group(0));
      return false;
    }

500 501
    return true;
  }
Devon Carew's avatar
Devon Carew committed
502 503 504 505 506 507 508 509 510 511 512 513 514 515

  @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);
  }
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537

  @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;
    });
  }
538
}
539

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

543 544 545 546
/// Return the list of connected ADB devices.
///
/// [mockAdbOutput] is public for testing.
List<AndroidDevice> getAdbDevices({ String mockAdbOutput }) {
547
  List<AndroidDevice> devices = <AndroidDevice>[];
548 549 550 551 552 553 554 555 556 557
  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');
  }
558

559 560
  for (String line in output) {
    // Skip lines like: * daemon started successfully *
561 562 563 564 565 566
    if (line.startsWith('* daemon '))
      continue;

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

567 568 569
    if (_kDeviceRegex.hasMatch(line)) {
      Match match = _kDeviceRegex.firstMatch(line);

570
      String deviceID = match[1];
571
      String deviceState = match[2];
572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
      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']);
587 588 589 590 591 592 593 594 595

      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 {
596 597 598 599 600 601
        devices.add(new AndroidDevice(
          deviceID,
          productID: info['product'],
          modelID: info['model'] ?? deviceID,
          deviceCodeName: info['device']
        ));
602
      }
603
    } else {
604
      printError(
605 606 607 608 609
        'Unexpected failure parsing device information from adb output:\n'
        '$line\n'
        'Please report a bug at https://github.com/flutter/flutter/issues/new');
    }
  }
610

611 612 613
  return devices;
}

614
/// A log reader that logs from `adb logcat`.
Devon Carew's avatar
Devon Carew committed
615
class _AdbLogReader extends DeviceLogReader {
Devon Carew's avatar
Devon Carew committed
616 617 618 619 620 621
  _AdbLogReader(this.device) {
    _linesController = new StreamController<String>.broadcast(
      onListen: _start,
      onCancel: _stop
    );
  }
Devon Carew's avatar
Devon Carew committed
622 623 624

  final AndroidDevice device;

625 626
  bool _lastWasFiltered = false;

Devon Carew's avatar
Devon Carew committed
627
  StreamController<String> _linesController;
628 629
  Process _process;

630
  @override
Devon Carew's avatar
Devon Carew committed
631
  Stream<String> get logLines => _linesController.stream;
632

633
  @override
634
  String get name => device.name;
Devon Carew's avatar
Devon Carew committed
635

Devon Carew's avatar
Devon Carew committed
636
  void _start() {
637
    // Start the adb logcat process.
638
    List<String> args = <String>['logcat', '-v', 'tag'];
639 640 641
    String lastTimestamp = device.lastLogcatTimestamp;
    if (lastTimestamp != null)
      args.addAll(<String>['-T', lastTimestamp]);
Devon Carew's avatar
Devon Carew committed
642 643 644 645 646 647 648 649 650 651
    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();
      });
    });
652 653
  }

654 655 656 657 658 659 660 661 662 663 664
  // '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),
    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+')
  ];

665
  void _onLine(String line) {
666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685
    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
686 687
  }

Devon Carew's avatar
Devon Carew committed
688 689
  void _stop() {
    // TODO(devoncarew): We should remove adb port forwarding here.
Devon Carew's avatar
Devon Carew committed
690

Devon Carew's avatar
Devon Carew committed
691
    _process?.kill();
Devon Carew's avatar
Devon Carew committed
692 693
  }
}
694 695 696 697 698 699 700 701 702 703

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

  final AndroidDevice device;

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

704
  @override
705 706 707
  List<ForwardedPort> get forwardedPorts {
    final List<ForwardedPort> ports = <ForwardedPort>[];

708 709 710
    String stdout = runCheckedSync(device.adbCommandForDevice(
      <String>['forward', '--list']
    ));
711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735

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

736
  @override
737
  Future<int> forward(int devicePort, { int hostPort }) async {
738 739 740 741 742
    if ((hostPort == null) || (hostPort == 0)) {
      // Auto select host port.
      hostPort = await findAvailablePort();
    }

743 744 745
    runCheckedSync(device.adbCommandForDevice(
      <String>['forward', 'tcp:$hostPort', 'tcp:$devicePort']
    ));
746 747 748 749

    return hostPort;
  }

750
  @override
Ian Hickson's avatar
Ian Hickson committed
751
  Future<Null> unforward(ForwardedPort forwardedPort) async {
752 753 754
    runCheckedSync(device.adbCommandForDevice(
      <String>['forward', '--remove', 'tcp:${forwardedPort.hostPort}']
    ));
755 756
  }
}