android_device.dart 29.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 9
import 'package:meta/meta.dart';

10
import '../android/android_sdk.dart';
11
import '../android/android_workflow.dart';
12
import '../android/apk.dart';
13
import '../application_package.dart';
14
import '../base/common.dart' show throwToolExit;
15
import '../base/file_system.dart';
16
import '../base/io.dart';
17
import '../base/logger.dart';
18
import '../base/process.dart';
19
import '../base/process_manager.dart';
20
import '../base/utils.dart';
21
import '../build_info.dart';
22
import '../device.dart';
23
import '../globals.dart';
24
import '../project.dart';
25
import '../protocol_discovery.dart';
26

27
import 'adb.dart';
28
import 'android.dart';
29
import 'android_sdk.dart';
30

31 32 33
enum _HardwareType { emulator, physical }

/// Map to help our `isLocalEmulator` detection.
34
const Map<String, _HardwareType> _knownHardware = <String, _HardwareType>{
35 36 37 38
  'goldfish': _HardwareType.emulator,
  'qcom': _HardwareType.physical,
  'ranchu': _HardwareType.emulator,
  'samsungexynos7420': _HardwareType.physical,
39
  'samsungexynos7870': _HardwareType.physical,
40
  'samsungexynos8890': _HardwareType.physical,
41 42 43
  'samsungexynos8895': _HardwareType.physical,
};

44
class AndroidDevices extends PollingDeviceDiscovery {
45
  AndroidDevices() : super('Android devices');
46

47
  @override
48
  bool get supportsPlatform => true;
49

50
  @override
51
  bool get canListAnything => androidWorkflow.canListDevices;
52

53
  @override
54
  Future<List<Device>> pollingGetDevices() async => getAdbDevices();
55 56 57

  @override
  Future<List<String>> getDiagnostics() async => getAdbDeviceDiagnostics();
58 59
}

60
class AndroidDevice extends Device {
61 62 63 64
  AndroidDevice(
    String id, {
    this.productID,
    this.modelID,
65 66
    this.deviceCodeName
  }) : super(id);
67

68 69 70 71
  final String productID;
  final String modelID;
  final String deviceCodeName;

72
  Map<String, String> _properties;
73
  bool _isLocalEmulator;
74 75
  TargetPlatform _platform;

76
  Future<String> _getProperty(String name) async {
77 78
    if (_properties == null) {
      _properties = <String, String>{};
79

80
      final List<String> propCommand = adbCommandForDevice(<String>['shell', 'getprop']);
81
      printTrace(propCommand.join(' '));
82 83

      try {
84
        // We pass an encoding of latin1 so that we don't try and interpret the
85
        // `adb shell getprop` result as UTF8.
86
        final ProcessResult result = await processManager.run(
87
          propCommand,
88 89
          stdoutEncoding: latin1,
          stderrEncoding: latin1,
90
        ).timeout(const Duration(seconds: 5));
91 92 93
        if (result.exitCode == 0) {
          _properties = parseAdbDeviceProperties(result.stdout);
        } else {
94 95
          printError('Error retrieving device properties for $name:');
          printError(result.stderr);
96
        }
97 98 99
      } on TimeoutException catch (_) {
        throwToolExit('adb not responding');
      } on ProcessException catch (error) {
100
        printError('Error retrieving device properties for $name: $error');
101 102
      }
    }
103

104 105
    return _properties[name];
  }
106

107
  @override
108
  Future<bool> get isLocalEmulator async {
109
    if (_isLocalEmulator == null) {
110 111 112 113 114 115 116 117 118 119 120
      final String hardware = await _getProperty('ro.hardware');
      printTrace('ro.hardware = $hardware');
      if (_knownHardware.containsKey(hardware)) {
        // Look for known hardware models.
        _isLocalEmulator = _knownHardware[hardware] == _HardwareType.emulator;
      } else {
        // Fall back to a best-effort heuristic-based approach.
        final String characteristics = await _getProperty('ro.build.characteristics');
        printTrace('ro.build.characteristics = $characteristics');
        _isLocalEmulator = characteristics != null && characteristics.contains('emulator');
      }
121 122 123 124 125
    }
    return _isLocalEmulator;
  }

  @override
126
  Future<TargetPlatform> get targetPlatform async {
127
    if (_platform == null) {
128
      // http://developer.android.com/ndk/guides/abis.html (x86, armeabi-v7a, ...)
129
      switch (await _getProperty('ro.product.cpu.abi')) {
130 131 132
        case 'arm64-v8a':
          _platform = TargetPlatform.android_arm64;
          break;
133 134 135 136 137 138 139 140 141
        case 'x86_64':
          _platform = TargetPlatform.android_x64;
          break;
        case 'x86':
          _platform = TargetPlatform.android_x86;
          break;
        default:
          _platform = TargetPlatform.android_arm;
          break;
142
      }
143 144
    }

145
    return _platform;
146
  }
147

148
  @override
149 150
  Future<String> get sdkNameAndVersion async =>
      'Android ${await _sdkVersion} (API ${await _apiVersion})';
151

152
  Future<String> get _sdkVersion => _getProperty('ro.build.version.release');
153

154
  Future<String> get _apiVersion => _getProperty('ro.build.version.sdk');
155

156
  _AdbLogReader _logReader;
157
  _AndroidDevicePortForwarder _portForwarder;
158

159
  List<String> adbCommandForDevice(List<String> args) {
160
    return <String>[getAdbPath(androidSdk), '-s', id]..addAll(args);
161 162 163 164
  }

  bool _isValidAdbVersion(String adbVersion) {
    // Sample output: 'Android Debug Bridge version 1.0.31'
165
    final Match versionFields = new RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
166
    if (versionFields != null) {
167 168 169
      final int majorVersion = int.parse(versionFields[1]);
      final int minorVersion = int.parse(versionFields[2]);
      final int patchVersion = int.parse(versionFields[3]);
170 171 172 173 174 175 176 177 178 179 180
      if (majorVersion > 1) {
        return true;
      }
      if (majorVersion == 1 && minorVersion > 0) {
        return true;
      }
      if (majorVersion == 1 && minorVersion == 0 && patchVersion >= 32) {
        return true;
      }
      return false;
    }
181
    printError(
182 183 184 185
        'Unrecognized adb version string $adbVersion. Skipping version check.');
    return true;
  }

186
  Future<bool> _checkForSupportedAdbVersion() async {
187 188 189
    if (androidSdk == null)
      return false;

190
    try {
191 192
      final RunResult adbVersion = await runCheckedAsync(<String>[getAdbPath(androidSdk), 'version']);
      if (_isValidAdbVersion(adbVersion.stdout))
193
        return true;
194
      printError('The ADB at "${getAdbPath(androidSdk)}" is too old; please install version 1.0.32 or later.');
195
    } catch (error, trace) {
196
      printError('Error running ADB: $error', stackTrace: trace);
197
    }
198

199 200 201
    return false;
  }

202
  Future<bool> _checkForSupportedAndroidVersion() async {
203 204 205 206 207
    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 *
208
      await runCheckedAsync(<String>[getAdbPath(androidSdk), 'start-server']);
209 210

      // Sample output: '22'
211
      final String sdkVersion = await _getProperty('ro.build.version.sdk');
212

213 214

      final int sdkVersionParsed = int.tryParse(sdkVersion);
215
      if (sdkVersionParsed == null) {
216
        printError('Unexpected response from getprop: "$sdkVersion"');
217 218
        return false;
      }
219

220
      if (sdkVersionParsed < minApiLevel) {
221
        printError(
222 223 224 225
          'The Android version ($sdkVersion) on the target device is too old. Please '
          'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.');
        return false;
      }
226

227 228
      return true;
    } catch (e) {
229
      printError('Unexpected failure from adb: $e');
230
      return false;
231 232 233 234 235 236 237
    }
  }

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

238 239 240
  Future<String> _getDeviceApkSha1(ApplicationPackage app) async {
    final RunResult result = await runAsync(adbCommandForDevice(<String>['shell', 'cat', _getDeviceSha1Path(app)]));
    return result.stdout;
241 242
  }

243
  String _getSourceSha1(ApplicationPackage app) {
244
    final AndroidApk apk = app;
245
    final File shaFile = fs.file('${apk.file.path}.sha1');
Devon Carew's avatar
Devon Carew committed
246
    return shaFile.existsSync() ? shaFile.readAsStringSync() : '';
247 248
  }

249
  @override
250 251 252
  String get name => modelID;

  @override
253
  Future<bool> isAppInstalled(ApplicationPackage app) async {
254
    // This call takes 400ms - 600ms.
255 256 257 258 259 260 261
    try {
      final RunResult listOut = await runCheckedAsync(adbCommandForDevice(<String>['shell', 'pm', 'list', 'packages', app.id]));
      return LineSplitter.split(listOut.stdout).contains('package:${app.id}');
    } catch (error) {
      printTrace('$error');
      return false;
    }
262 263
  }

264
  @override
265 266
  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async {
    final String installedSha1 = await _getDeviceApkSha1(app);
267 268 269
    return installedSha1.isNotEmpty && installedSha1 == _getSourceSha1(app);
  }

270
  @override
271
  Future<bool> installApp(ApplicationPackage app) async {
272
    final AndroidApk apk = app;
273 274
    if (!apk.file.existsSync()) {
      printError('"${fs.path.relative(apk.file.path)}" does not exist.');
275 276 277
      return false;
    }

278
    if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
279 280
      return false;

281 282
    final Status status = logger.startProgress('Installing ${fs.path.relative(apk.file.path)}...', expectSlowOperation: true);
    final RunResult installResult = await runAsync(adbCommandForDevice(<String>['install', '-t', '-r', apk.file.path]));
Devon Carew's avatar
Devon Carew committed
283
    status.stop();
284 285
    // Some versions of adb exit with exit code 0 even on failure :(
    // Parsing the output to check for failures.
286
    final RegExp failureExp = new RegExp(r'^Failure.*$', multiLine: true);
287
    final String failure = failureExp.stringMatch(installResult.stdout);
288 289 290 291
    if (failure != null) {
      printError('Package install error: $failure');
      return false;
    }
292 293
    if (installResult.exitCode != 0) {
      printError('Error: ADB exited with exit code ${installResult.exitCode}');
294
      printError('$installResult');
295 296
      return false;
    }
297

298 299 300
    await runCheckedAsync(adbCommandForDevice(<String>[
      'shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app)
    ]));
301 302 303
    return true;
  }

304
  @override
305
  Future<bool> uninstallApp(ApplicationPackage app) async {
306
    if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
307 308
      return false;

309
    final String uninstallOut = (await runCheckedAsync(adbCommandForDevice(<String>['uninstall', app.id]))).stdout;
310 311
    final RegExp failureExp = new RegExp(r'^Failure.*$', multiLine: true);
    final String failure = failureExp.stringMatch(uninstallOut);
312 313 314 315 316 317 318 319
    if (failure != null) {
      printError('Package uninstall error: $failure');
      return false;
    }

    return true;
  }

320
  Future<bool> _installLatestApp(ApplicationPackage package) async {
321
    final bool wasInstalled = await isAppInstalled(package);
322
    if (wasInstalled) {
323
      if (await isLatestBuildInstalled(package)) {
324
        printTrace('Latest build already installed.');
325 326 327 328
        return true;
      }
    }
    printTrace('Installing APK.');
329
    if (!await installApp(package)) {
330 331 332
      printTrace('Warning: Failed to install APK.');
      if (wasInstalled) {
        printStatus('Uninstalling old version...');
333
        if (!await uninstallApp(package)) {
334 335 336
          printError('Error: Uninstalling old version failed.');
          return false;
        }
337
        if (!await installApp(package)) {
338 339 340 341 342
          printError('Error: Failed to install APK again.');
          return false;
        }
        return true;
      }
343 344 345 346 347
      return false;
    }
    return true;
  }

348 349
  @override
  Future<LaunchResult> startApp(
350
    ApplicationPackage package, {
351
    String mainPath,
352
    String route,
353 354
    DebuggingOptions debuggingOptions,
    Map<String, dynamic> platformArgs,
355 356 357 358
    bool prebuiltApplication = false,
    bool applicationNeedsRebuild = false,
    bool usesTerminalUi = true,
    bool ipv6 = false,
359
  }) async {
360
    if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
361
      return new LaunchResult.failed();
362

363 364 365 366
    final TargetPlatform devicePlatform = await targetPlatform;
    if (!(devicePlatform == TargetPlatform.android_arm ||
          devicePlatform == TargetPlatform.android_arm64) &&
        !debuggingOptions.buildInfo.isDebug) {
367 368 369 370
      printError('Profile and release builds are only supported on ARM targets.');
      return new LaunchResult.failed();
    }

371 372 373 374
    BuildInfo buildInfo = debuggingOptions.buildInfo;
    if (buildInfo.targetPlatform == null && devicePlatform == TargetPlatform.android_arm64)
      buildInfo = buildInfo.withTargetPlatform(TargetPlatform.android_arm64);

375 376
    if (!prebuiltApplication) {
      printTrace('Building APK');
377
      await buildApk(
378
          project: new FlutterProject(fs.currentDirectory),
379
          target: mainPath,
380
          buildInfo: buildInfo,
381
      );
382 383
      // Package has been built, so we can get the updated application ID and
      // activity name from the .apk.
384
      package = await AndroidApk.fromAndroidProject(new FlutterProject(fs.currentDirectory).android);
385 386
    }

387 388 389
    printTrace("Stopping app '${package.name}' on $name.");
    await stopApp(package);

390
    if (!await _installLatestApp(package))
391
      return new LaunchResult.failed();
392

393 394 395
    final bool traceStartup = platformArgs['trace-startup'] ?? false;
    final AndroidApk apk = package;
    printTrace('$this startApp');
396

397
    ProtocolDiscovery observatoryDiscovery;
398

399
    if (debuggingOptions.debuggingEnabled) {
400
      // TODO(devoncarew): Remember the forwarding information (so we can later remove the
401
      // port forwarding or set it up again when adb fails on us).
402
      observatoryDiscovery = new ProtocolDiscovery.observatory(
403 404 405 406 407
        getLogReader(),
        portForwarder: portForwarder,
        hostPort: debuggingOptions.observatoryPort,
        ipv6: ipv6,
      );
Devon Carew's avatar
Devon Carew committed
408
    }
409

410 411
    List<String> cmd;

412 413 414
    cmd = adbCommandForDevice(<String>[
      'shell', 'am', 'start',
      '-a', 'android.intent.action.RUN',
415
      '-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP
416
      '--ez', 'enable-background-compilation', 'true',
417
      '--ez', 'enable-dart-profiling', 'true',
418
    ]);
419

420
    if (traceStartup)
421
      cmd.addAll(<String>['--ez', 'trace-startup', 'true']);
422
    if (route != null)
423
      cmd.addAll(<String>['--es', 'route', route]);
424 425
    if (debuggingOptions.enableSoftwareRendering)
      cmd.addAll(<String>['--ez', 'enable-software-rendering', 'true']);
426 427
    if (debuggingOptions.skiaDeterministicRendering)
      cmd.addAll(<String>['--ez', 'skia-deterministic-rendering', 'true']);
428 429
    if (debuggingOptions.traceSkia)
      cmd.addAll(<String>['--ez', 'trace-skia', 'true']);
430
    if (debuggingOptions.debuggingEnabled) {
431
      if (debuggingOptions.buildInfo.isDebug)
Devon Carew's avatar
Devon Carew committed
432
        cmd.addAll(<String>['--ez', 'enable-checked-mode', 'true']);
433
      if (debuggingOptions.startPaused)
Devon Carew's avatar
Devon Carew committed
434
        cmd.addAll(<String>['--ez', 'start-paused', 'true']);
435 436
      if (debuggingOptions.useTestFonts)
        cmd.addAll(<String>['--ez', 'use-test-fonts', 'true']);
Devon Carew's avatar
Devon Carew committed
437
    }
438
    cmd.add(apk.launchActivity);
439
    final String result = (await runCheckedAsync(cmd)).stdout;
440 441 442
    // This invocation returns 0 even when it fails.
    if (result.contains('Error: ')) {
      printError(result.trim());
Devon Carew's avatar
Devon Carew committed
443
      return new LaunchResult.failed();
444
    }
445

446
    if (!debuggingOptions.debuggingEnabled)
Devon Carew's avatar
Devon Carew committed
447
      return new LaunchResult.succeeded();
Devon Carew's avatar
Devon Carew committed
448

449 450 451
    // 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...');
452

453
    // TODO(danrubel) Waiting for observatory services can be made common across all devices.
454
    try {
455 456 457
      Uri observatoryUri;

      if (debuggingOptions.buildInfo.isDebug || debuggingOptions.buildInfo.isProfile) {
458
        observatoryUri = await observatoryDiscovery.uri;
Devon Carew's avatar
Devon Carew committed
459
      }
460

461
      return new LaunchResult.succeeded(observatoryUri: observatoryUri);
462 463 464 465
    } catch (error) {
      printError('Error waiting for a debug connection: $error');
      return new LaunchResult.failed();
    } finally {
466
      await observatoryDiscovery.cancel();
Devon Carew's avatar
Devon Carew committed
467
    }
468 469
  }

470 471 472
  @override
  bool get supportsHotMode => true;

473
  @override
474
  Future<bool> stopApp(ApplicationPackage app) {
475
    final List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]);
476
    return runCommandAndStreamOutput(command).then((int exitCode) => exitCode == 0);
477 478
  }

479
  @override
480
  void clearLogs() {
481
    runSync(adbCommandForDevice(<String>['logcat', '-c']));
482 483
  }

484
  @override
485 486 487
  DeviceLogReader getLogReader({ApplicationPackage app}) {
    // The Android log reader isn't app-specific.
    _logReader ??= new _AdbLogReader(this);
488 489
    return _logReader;
  }
490

491
  @override
492
  DevicePortForwarder get portForwarder => _portForwarder ??= new _AndroidDevicePortForwarder(this);
493

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

496
  /// Return the most recent timestamp in the Android log or null if there is
497
  /// no available timestamp. The format can be passed to logcat's -T option.
498
  String get lastLogcatTimestamp {
499
    final String output = runCheckedSync(adbCommandForDevice(<String>[
500
      'logcat', '-v', 'time', '-t', '1'
501
    ]));
502

503
    final Match timeMatch = _timeRegExp.firstMatch(output);
504
    return timeMatch?.group(0);
505 506
  }

507
  @override
508 509
  bool isSupported() => true;

Devon Carew's avatar
Devon Carew committed
510 511 512 513
  @override
  bool get supportsScreenshot => true;

  @override
514
  Future<void> takeScreenshot(File outputFile) async {
Devon Carew's avatar
Devon Carew committed
515
    const String remotePath = '/data/local/tmp/flutter_screenshot.png';
516 517 518
    await runCheckedAsync(adbCommandForDevice(<String>['shell', 'screencap', '-p', remotePath]));
    await runCheckedAsync(adbCommandForDevice(<String>['pull', remotePath, outputFile.path]));
    await runCheckedAsync(adbCommandForDevice(<String>['shell', 'rm', remotePath]));
Devon Carew's avatar
Devon Carew committed
519
  }
520

521 522 523
  // TODO(dantup): discoverApps is no longer used and can possibly be removed.
  // Waiting for a response here:
  // https://github.com/flutter/flutter/pull/18873#discussion_r198862179
524
  @override
525
  Future<List<DiscoveredApp>> discoverApps() async {
526 527 528 529
    final RegExp discoverExp = new RegExp(r'DISCOVER: (.*)');
    final List<DiscoveredApp> result = <DiscoveredApp>[];
    final StreamSubscription<String> logs = getLogReader().logLines.listen((String line) {
      final Match match = discoverExp.firstMatch(line);
530
      if (match != null) {
531
        final Map<String, dynamic> app = json.decode(match.group(1));
532
        result.add(new DiscoveredApp(app['id'], app['observatoryPort']));
533 534 535
      }
    });

536
    await runCheckedAsync(adbCommandForDevice(<String>[
537 538 539
      'shell', 'am', 'broadcast', '-a', 'io.flutter.view.DISCOVER'
    ]));

540 541 542 543
    await waitGroup<Null>(<Future<Null>>[
      new Future<Null>.delayed(const Duration(seconds: 1)),
      logs.cancel(),
    ]);
544
    return result;
545
  }
546
}
547

548
Map<String, String> parseAdbDeviceProperties(String str) {
549
  final Map<String, String> properties = <String, String>{};
550 551 552 553 554 555
  final RegExp propertyExp = new RegExp(r'\[(.*?)\]: \[(.*?)\]');
  for (Match match in propertyExp.allMatches(str))
    properties[match.group(1)] = match.group(2);
  return properties;
}

556
/// Return the list of connected ADB devices.
557 558 559 560 561
List<AndroidDevice> getAdbDevices() {
  final String adbPath = getAdbPath(androidSdk);
  if (adbPath == null)
    return <AndroidDevice>[];
  final String text = runSync(<String>[adbPath, 'devices', '-l']);
562
  final List<AndroidDevice> devices = <AndroidDevice>[];
563 564 565 566 567 568 569 570 571 572 573 574 575
  parseADBDeviceOutput(text, devices: devices);
  return devices;
}

/// Get diagnostics about issues with any connected devices.
Future<List<String>> getAdbDeviceDiagnostics() async {
  final String adbPath = getAdbPath(androidSdk);
  if (adbPath == null)
    return <String>[];

  final RunResult result = await runAsync(<String>[adbPath, 'devices', '-l']);
  if (result.exitCode != 0) {
    return <String>[];
576
  } else {
577 578 579 580
    final String text = result.stdout;
    final List<String> diagnostics = <String>[];
    parseADBDeviceOutput(text, diagnostics: diagnostics);
    return diagnostics;
581
  }
582 583 584 585
}

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

587 588 589 590 591 592 593 594
/// Parse the given `adb devices` output in [text], and fill out the given list
/// of devices and possible device issue diagnostics. Either argument can be null,
/// in which case information for that parameter won't be populated.
@visibleForTesting
void parseADBDeviceOutput(String text, {
  List<AndroidDevice> devices,
  List<String> diagnostics
}) {
595 596
  // Check for error messages from adb
  if (!text.contains('List of devices')) {
597 598
    diagnostics?.add(text);
    return;
599 600 601
  }

  for (String line in text.trim().split('\n')) {
602
    // Skip lines like: * daemon started successfully *
603 604 605
    if (line.startsWith('* daemon '))
      continue;

606
    // Skip lines about adb server and client version not matching
607
    if (line.startsWith(new RegExp(r'adb server (version|is out of date)'))) {
608
      diagnostics?.add(line);
609 610 611
      continue;
    }

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

615
    if (_kDeviceRegex.hasMatch(line)) {
616
      final Match match = _kDeviceRegex.firstMatch(line);
617

618 619
      final String deviceID = match[1];
      final String deviceState = match[2];
620 621
      String rest = match[3];

622
      final Map<String, String> info = <String, String>{};
623 624 625 626
      if (rest != null && rest.isNotEmpty) {
        rest = rest.trim();
        for (String data in rest.split(' ')) {
          if (data.contains(':')) {
627
            final List<String> fields = data.split(':');
628 629 630 631 632 633 634
            info[fields[0]] = fields[1];
          }
        }
      }

      if (info['model'] != null)
        info['model'] = cleanAdbDeviceName(info['model']);
635 636

      if (deviceState == 'unauthorized') {
637
        diagnostics?.add(
638 639 640 641
          'Device $deviceID is not authorized.\n'
          'You might need to check your device for an authorization dialog.'
        );
      } else if (deviceState == 'offline') {
642
        diagnostics?.add('Device $deviceID is offline.');
643
      } else {
644
        devices?.add(new AndroidDevice(
645 646 647 648 649
          deviceID,
          productID: info['product'],
          modelID: info['model'] ?? deviceID,
          deviceCodeName: info['device']
        ));
650
      }
651
    } else {
652
      diagnostics?.add(
653 654 655 656 657 658 659
        'Unexpected failure parsing device information from adb output:\n'
        '$line\n'
        'Please report a bug at https://github.com/flutter/flutter/issues/new');
    }
  }
}

660
/// A log reader that logs from `adb logcat`.
Devon Carew's avatar
Devon Carew committed
661
class _AdbLogReader extends DeviceLogReader {
Devon Carew's avatar
Devon Carew committed
662 663 664 665 666 667
  _AdbLogReader(this.device) {
    _linesController = new StreamController<String>.broadcast(
      onListen: _start,
      onCancel: _stop
    );
  }
Devon Carew's avatar
Devon Carew committed
668 669 670

  final AndroidDevice device;

Devon Carew's avatar
Devon Carew committed
671
  StreamController<String> _linesController;
672
  Process _process;
673

674
  @override
Devon Carew's avatar
Devon Carew committed
675
  Stream<String> get logLines => _linesController.stream;
676

677
  @override
678
  String get name => device.name;
Devon Carew's avatar
Devon Carew committed
679

680 681 682 683 684 685 686 687 688 689
  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
690
  void _start() {
691
    // Start the adb logcat process.
692 693
    final List<String> args = <String>['logcat', '-v', 'time'];
    final String lastTimestamp = device.lastLogcatTimestamp;
694 695 696 697
    if (lastTimestamp != null)
        _timeOrigin = _adbTimestampToDateTime(lastTimestamp);
    else
        _timeOrigin = null;
698
    runCommand(device.adbCommandForDevice(args)).then<Null>((Process process) {
Devon Carew's avatar
Devon Carew committed
699
      _process = process;
700
      const Utf8Decoder decoder = Utf8Decoder(allowMalformed: true);
701 702
      _process.stdout.transform(decoder).transform(const LineSplitter()).listen(_onLine);
      _process.stderr.transform(decoder).transform(const LineSplitter()).listen(_onLine);
703
      _process.exitCode.whenComplete(() {
Devon Carew's avatar
Devon Carew committed
704 705 706 707
        if (_linesController.hasListener)
          _linesController.close();
      });
    });
708 709
  }

710 711
  // 'W/ActivityManager(pid): '
  static final RegExp _logFormat = new RegExp(r'^[VDIWEF]\/.*?\(\s*(\d+)\):\s');
712 713 714

  static final List<RegExp> _whitelistedTags = <RegExp>[
    new RegExp(r'^[VDIWEF]\/flutter[^:]*:\s+', caseSensitive: false),
715
    new RegExp(r'^[IE]\/DartVM[^:]*:\s+'),
716
    new RegExp(r'^[WEF]\/AndroidRuntime:\s+'),
717
    new RegExp(r'^[WEF]\/ActivityManager:\s+.*(\bflutter\b|\bdomokit\b|\bsky\b)'),
718 719 720 721
    new RegExp(r'^[WEF]\/System\.err:\s+'),
    new RegExp(r'^[F]\/[\S^:]+:\s+')
  ];

722 723 724 725 726 727 728 729 730
  // 'F/libc(pid): Fatal signal 11'
  static final RegExp _fatalLog = new RegExp(r'^F\/libc\s*\(\s*\d+\):\sFatal signal (\d+)');

  // 'I/DEBUG(pid): ...'
  static final RegExp _tombstoneLine = new RegExp(r'^[IF]\/DEBUG\s*\(\s*\d+\):\s(.+)$');

  // 'I/DEBUG(pid): Tombstone written to: '
  static final RegExp _tombstoneTerminator = new RegExp(r'^Tombstone written to:\s');

731 732 733
  // we default to true in case none of the log lines match
  bool _acceptedLastLine = true;

734 735 736 737 738
  // Whether a fatal crash is happening or not.
  // During a fatal crash only lines from the crash are accepted, the rest are
  // dropped.
  bool _fatalCrash = false;

739 740 741
  // 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): ....
742
  void _onLine(String line) {
743 744 745 746 747 748
    final Match timeMatch = AndroidDevice._timeRegExp.firstMatch(line);
    if (timeMatch == null) {
      return;
    }
    if (_timeOrigin != null) {
      final String timestamp = timeMatch.group(0);
749
      final DateTime time = _adbTimestampToDateTime(timestamp);
750
      if (!time.isAfter(_timeOrigin)) {
751 752 753 754 755 756 757 758 759
        // Ignore log messages before the origin.
        return;
      }
    }
    if (line.length == timeMatch.end) {
      return;
    }
    // Chop off the time.
    line = line.substring(timeMatch.end + 1);
760 761 762
    final Match logMatch = _logFormat.firstMatch(line);
    if (logMatch != null) {
      bool acceptLine = false;
763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780

      if (_fatalCrash) {
        // While a fatal crash is going on, only accept lines from the crash
        // Otherwise the crash log in the console may get interrupted

        final Match fatalMatch = _tombstoneLine.firstMatch(line);

        if (fatalMatch != null) {
          acceptLine = true;

          line = fatalMatch[1];

          if (_tombstoneTerminator.hasMatch(fatalMatch[1])) {
            // Hit crash terminator, stop logging the crash info
            _fatalCrash = false;
          }
        }
      } else if (appPid != null && int.parse(logMatch.group(1)) == appPid) {
781
        acceptLine = true;
782 783 784 785 786

        if (_fatalLog.hasMatch(line)) {
          // Hit fatal signal, app is now crashing
          _fatalCrash = true;
        }
787 788 789 790
      } else {
        // Filter on approved names and levels.
        acceptLine = _whitelistedTags.any((RegExp re) => re.hasMatch(line));
      }
791

792 793 794 795
      if (acceptLine) {
        _acceptedLastLine = true;
        _linesController.add(line);
        return;
796
      }
797 798 799 800 801
      _acceptedLastLine = false;
    } else if (line == '--------- beginning of system' ||
               line == '--------- beginning of main' ) {
      // hide the ugly adb logcat log boundaries at the start
      _acceptedLastLine = false;
802
    } else {
803 804 805
      // 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) {
806
        _linesController.add(line);
807 808
        return;
      }
809
    }
Devon Carew's avatar
Devon Carew committed
810 811
  }

Devon Carew's avatar
Devon Carew committed
812 813
  void _stop() {
    // TODO(devoncarew): We should remove adb port forwarding here.
Devon Carew's avatar
Devon Carew committed
814

Devon Carew's avatar
Devon Carew committed
815
    _process?.kill();
Devon Carew's avatar
Devon Carew committed
816 817
  }
}
818 819 820 821 822 823 824

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

  final AndroidDevice device;

  static int _extractPort(String portString) {
825 826

    return int.tryParse(portString.trim());
827 828
  }

829
  @override
830 831 832
  List<ForwardedPort> get forwardedPorts {
    final List<ForwardedPort> ports = <ForwardedPort>[];

833
    final String stdout = runCheckedSync(device.adbCommandForDevice(
834 835
      <String>['forward', '--list']
    ));
836

837
    final List<String> lines = LineSplitter.split(stdout).toList();
838 839
    for (String line in lines) {
      if (line.startsWith(device.id)) {
840
        final List<String> splitLine = line.split('tcp:');
841 842 843 844 845 846

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

        // Attempt to extract ports.
847 848
        final int hostPort = _extractPort(splitLine[1]);
        final int devicePort = _extractPort(splitLine[2]);
849 850

        // Failed, skip.
851
        if (hostPort == null || devicePort == null)
852 853 854 855 856 857 858 859 860
          continue;

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

    return ports;
  }

861
  @override
862 863 864
  Future<int> forward(int devicePort, {int hostPort}) async {
    hostPort ??= 0;
    final RunResult process = await runCheckedAsync(device.adbCommandForDevice(
865 866
      <String>['forward', 'tcp:$hostPort', 'tcp:$devicePort']
    ));
867

868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885
    if (process.stderr.isNotEmpty)
      process.throwException('adb returned error:\n${process.stderr}');

    if (process.exitCode != 0) {
      if (process.stdout.isNotEmpty)
        process.throwException('adb returned error:\n${process.stdout}');
      process.throwException('adb failed without a message');
    }

    if (hostPort == 0) {
      if (process.stdout.isEmpty)
        process.throwException('adb did not report forwarded port');
      hostPort = int.tryParse(process.stdout) ?? (throw 'adb returned invalid port number:\n${process.stdout}');
    } else {
      if (process.stdout.isNotEmpty)
        process.throwException('adb returned error:\n${process.stdout}');
    }

886 887 888
    return hostPort;
  }

889
  @override
Ian Hickson's avatar
Ian Hickson committed
890
  Future<Null> unforward(ForwardedPort forwardedPort) async {
891
    await runCheckedAsync(device.adbCommandForDevice(
892 893
      <String>['forward', '--remove', 'tcp:${forwardedPort.hostPort}']
    ));
894 895
  }
}