android_device.dart 24.3 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 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
      package,
      localBundlePath,
385
      traceStartup: platformArgs['trace-startup'] ?? false,
Devon Carew's avatar
Devon Carew committed
386
      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
  @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;
  }

416 417 418
  @override
  bool get supportsRestart => true;

419 420 421 422 423
  @override
  Future<bool> restartApp(
    ApplicationPackage package,
    LaunchResult result, {
    String mainPath,
424
    VMService observatory
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
  }) 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);
    }
  }

452
  @override
453 454 455
  Future<bool> stopApp(ApplicationPackage app) {
    List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]);
    return runCommandAndStreamOutput(command).then((int exitCode) => exitCode == 0);
456 457
  }

458
  @override
459
  void clearLogs() {
460
    runSync(adbCommandForDevice(<String>['logcat', '-c']));
461 462
  }

463
  @override
464 465 466 467 468
  DeviceLogReader get logReader {
    if (_logReader == null)
      _logReader = new _AdbLogReader(this);
    return _logReader;
  }
469

470
  @override
471 472 473 474 475 476 477
  DevicePortForwarder get portForwarder {
    if (_portForwarder == null)
      _portForwarder = new _AndroidDevicePortForwarder(this);

    return _portForwarder;
  }

478 479
  /// 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.
480
  String get lastLogcatTimestamp {
481
    String output = runCheckedSync(adbCommandForDevice(<String>[
482
      'logcat', '-v', 'time', '-t', '1'
483
    ]));
484 485 486

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

490
  @override
491 492
  bool isSupported() => true;

493
  Future<bool> refreshSnapshot(String activity, String snapshotPath) async {
494 495 496 497 498
    if (!FileSystemEntity.isFileSync(snapshotPath)) {
      printError('Cannot find $snapshotPath');
      return false;
    }

499 500 501 502 503 504 505
    RunResult result = await runAsync(
      adbCommandForDevice(<String>['push', snapshotPath, _deviceSnapshotPath])
    );
    if (result.exitCode != 0) {
      printStatus(result.toString());
      return false;
    }
506 507 508 509 510 511 512

    List<String> cmd = adbCommandForDevice(<String>[
      'shell', 'am', 'start',
      '-a', 'android.intent.action.RUN',
      '-d', _deviceBundlePath,
      '-f', '0x20000000',  // FLAG_ACTIVITY_SINGLE_TOP
      '--es', 'snapshot', _deviceSnapshotPath,
513
      activity,
514
    ]);
515 516 517 518 519
    result = await runAsync(cmd);
    if (result.exitCode != 0) {
      printStatus(result.toString());
      return false;
    }
520

521 522
    final RegExp errorRegExp = new RegExp(r'^Error: .*$', multiLine: true);
    Match errorMatch = errorRegExp.firstMatch(result.processResult.stdout);
523 524 525 526 527
    if (errorMatch != null) {
      printError(errorMatch.group(0));
      return false;
    }

528 529
    return true;
  }
Devon Carew's avatar
Devon Carew committed
530 531 532 533 534 535 536 537 538 539 540 541 542 543

  @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);
  }
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565

  @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;
    });
  }
566
}
567

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

571 572 573 574
/// Return the list of connected ADB devices.
///
/// [mockAdbOutput] is public for testing.
List<AndroidDevice> getAdbDevices({ String mockAdbOutput }) {
575
  List<AndroidDevice> devices = <AndroidDevice>[];
576 577 578 579 580 581 582 583 584 585
  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');
  }
586

587 588
  for (String line in output) {
    // Skip lines like: * daemon started successfully *
589 590 591 592 593 594
    if (line.startsWith('* daemon '))
      continue;

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

595 596 597
    if (_kDeviceRegex.hasMatch(line)) {
      Match match = _kDeviceRegex.firstMatch(line);

598
      String deviceID = match[1];
599
      String deviceState = match[2];
600 601 602 603 604 605 606 607 608 609 610 611 612 613 614
      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']);
615 616 617 618 619 620 621 622 623

      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 {
624 625 626 627 628 629
        devices.add(new AndroidDevice(
          deviceID,
          productID: info['product'],
          modelID: info['model'] ?? deviceID,
          deviceCodeName: info['device']
        ));
630
      }
631
    } else {
632
      printError(
633 634 635 636 637
        'Unexpected failure parsing device information from adb output:\n'
        '$line\n'
        'Please report a bug at https://github.com/flutter/flutter/issues/new');
    }
  }
638

639 640 641
  return devices;
}

642
/// A log reader that logs from `adb logcat`.
Devon Carew's avatar
Devon Carew committed
643
class _AdbLogReader extends DeviceLogReader {
Devon Carew's avatar
Devon Carew committed
644 645 646 647 648 649
  _AdbLogReader(this.device) {
    _linesController = new StreamController<String>.broadcast(
      onListen: _start,
      onCancel: _stop
    );
  }
Devon Carew's avatar
Devon Carew committed
650 651 652

  final AndroidDevice device;

653 654
  bool _lastWasFiltered = false;

Devon Carew's avatar
Devon Carew committed
655
  StreamController<String> _linesController;
656 657
  Process _process;

658
  @override
Devon Carew's avatar
Devon Carew committed
659
  Stream<String> get logLines => _linesController.stream;
660

661
  @override
662
  String get name => device.name;
Devon Carew's avatar
Devon Carew committed
663

Devon Carew's avatar
Devon Carew committed
664
  void _start() {
665
    // Start the adb logcat process.
666
    List<String> args = <String>['logcat', '-v', 'tag'];
667
    String lastTimestamp = device.lastLogcatTimestamp;
668 669 670 671 672 673 674 675 676 677 678 679 680 681
    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
682 683 684 685 686 687 688 689 690 691
    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();
      });
    });
692 693
  }

694 695 696 697 698
  // '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),
699
    new RegExp(r'^[IE]\/DartVM[^:]*:\s+'),
700 701 702 703 704 705
    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+')
  ];

706
  void _onLine(String line) {
707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726
    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
727 728
  }

Devon Carew's avatar
Devon Carew committed
729 730
  void _stop() {
    // TODO(devoncarew): We should remove adb port forwarding here.
Devon Carew's avatar
Devon Carew committed
731

Devon Carew's avatar
Devon Carew committed
732
    _process?.kill();
Devon Carew's avatar
Devon Carew committed
733 734
  }
}
735 736 737 738 739 740 741 742 743 744

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

  final AndroidDevice device;

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

745
  @override
746 747 748
  List<ForwardedPort> get forwardedPorts {
    final List<ForwardedPort> ports = <ForwardedPort>[];

749 750 751
    String stdout = runCheckedSync(device.adbCommandForDevice(
      <String>['forward', '--list']
    ));
752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776

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

777
  @override
778
  Future<int> forward(int devicePort, { int hostPort }) async {
779 780 781 782 783
    if ((hostPort == null) || (hostPort == 0)) {
      // Auto select host port.
      hostPort = await findAvailablePort();
    }

784 785 786
    runCheckedSync(device.adbCommandForDevice(
      <String>['forward', 'tcp:$hostPort', 'tcp:$devicePort']
    ));
787 788 789 790

    return hostPort;
  }

791
  @override
Ian Hickson's avatar
Ian Hickson committed
792
  Future<Null> unforward(ForwardedPort forwardedPort) async {
793 794 795
    runCheckedSync(device.adbCommandForDevice(
      <String>['forward', '--remove', 'tcp:${forwardedPort.hostPort}']
    ));
796 797
  }
}