android_device.dart 34.5 KB
Newer Older
1 2 3 4 5 6
// 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';

7 8
import 'package:meta/meta.dart';

9
import '../android/android_sdk.dart';
10
import '../android/android_workflow.dart';
11
import '../android/apk.dart';
12
import '../application_package.dart';
13
import '../base/common.dart' show throwToolExit;
14
import '../base/file_system.dart';
15
import '../base/io.dart';
16
import '../base/logger.dart';
17
import '../base/platform.dart';
18
import '../base/process.dart';
19
import '../base/process_manager.dart';
20
import '../build_info.dart';
21
import '../convert.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_console.dart';
30
import 'android_sdk.dart';
31

32 33 34
enum _HardwareType { emulator, physical }

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

47 48 49 50 51 52 53
bool allowHeapCorruptionOnWindows(int exitCode) {
  // In platform tools 29.0.0 adb.exe seems to be ending with this heap
  // corruption error code on seemingly successful termination.
  // So we ignore this error on Windows.
  return exitCode == -1073740940 && platform.isWindows;
}

54
class AndroidDevices extends PollingDeviceDiscovery {
55
  AndroidDevices() : super('Android devices');
56

57
  @override
58
  bool get supportsPlatform => true;
59

60
  @override
61
  bool get canListAnything => androidWorkflow.canListDevices;
62

63
  @override
64
  Future<List<Device>> pollingGetDevices() async => getAdbDevices();
65 66 67

  @override
  Future<List<String>> getDiagnostics() async => getAdbDeviceDiagnostics();
68 69
}

70
class AndroidDevice extends Device {
71 72 73 74
  AndroidDevice(
    String id, {
    this.productID,
    this.modelID,
75
    this.deviceCodeName,
76 77 78 79 80 81
  }) : super(
      id,
      category: Category.mobile,
      platformType: PlatformType.android,
      ephemeral: true,
  );
82

83 84 85 86
  final String productID;
  final String modelID;
  final String deviceCodeName;

87
  Map<String, String> _properties;
88
  bool _isLocalEmulator;
89 90
  TargetPlatform _platform;

91
  Future<String> _getProperty(String name) async {
92 93
    if (_properties == null) {
      _properties = <String, String>{};
94

95
      final List<String> propCommand = adbCommandForDevice(<String>['shell', 'getprop']);
96
      printTrace(propCommand.join(' '));
97 98

      try {
99
        // We pass an encoding of latin1 so that we don't try and interpret the
100
        // `adb shell getprop` result as UTF8.
101
        final ProcessResult result = await processManager.run(
102
          propCommand,
103 104
          stdoutEncoding: latin1,
          stderrEncoding: latin1,
105
        );
106
        if (result.exitCode == 0 || allowHeapCorruptionOnWindows(result.exitCode)) {
107 108
          _properties = parseAdbDeviceProperties(result.stdout);
        } else {
109
          printError('Error ${result.exitCode} retrieving device properties for $name:');
110
          printError(result.stderr);
111
        }
112
      } on ProcessException catch (error) {
113
        printError('Error retrieving device properties for $name: $error');
114 115
      }
    }
116

117 118
    return _properties[name];
  }
119

120
  @override
121
  Future<bool> get isLocalEmulator async {
122
    if (_isLocalEmulator == null) {
123 124 125 126 127 128 129 130 131 132 133
      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');
      }
134 135 136 137
    }
    return _isLocalEmulator;
  }

138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
  /// The unique identifier for the emulator that corresponds to this device, or
  /// null if it is not an emulator.
  ///
  /// The ID returned matches that in the output of `flutter emulators`. Fetching
  /// this name may require connecting to the device and if an error occurs null
  /// will be returned.
  @override
  Future<String> get emulatorId async {
    if (!(await isLocalEmulator))
      return null;

    // Emulators always have IDs in the format emulator-(port) where port is the
    // Android Console port number.
    final RegExp emulatorPortRegex = RegExp(r'emulator-(\d+)');

    final Match portMatch = emulatorPortRegex.firstMatch(id);
    if (portMatch == null || portMatch.groupCount < 1) {
      return null;
    }

    const String host = 'localhost';
    final int port = int.parse(portMatch.group(1));
    printTrace('Fetching avd name for $name via Android console on $host:$port');

    try {
      final Socket socket = await androidConsoleSocketFactory(host, port);
      final AndroidConsole console = AndroidConsole(socket);

      try {
        await console
            .connect()
            .timeout(timeoutConfiguration.fastOperation,
                onTimeout: () => throw TimeoutException('Connection timed out'));

        return await console
            .getAvdName()
            .timeout(timeoutConfiguration.fastOperation,
                onTimeout: () => throw TimeoutException('"avd name" timed out'));
      } finally {
        console.destroy();
      }
    } catch (e) {
      printTrace('Failed to fetch avd name for emulator at $host:$port: $e');
      // If we fail to connect to the device, we should not fail so just return
      // an empty name. This data is best-effort.
      return null;
    }
  }

187
  @override
188
  Future<TargetPlatform> get targetPlatform async {
189
    if (_platform == null) {
190
      // http://developer.android.com/ndk/guides/abis.html (x86, armeabi-v7a, ...)
191
      switch (await _getProperty('ro.product.cpu.abi')) {
192 193 194
        case 'arm64-v8a':
          _platform = TargetPlatform.android_arm64;
          break;
195 196 197 198 199 200 201 202 203
        case 'x86_64':
          _platform = TargetPlatform.android_x64;
          break;
        case 'x86':
          _platform = TargetPlatform.android_x86;
          break;
        default:
          _platform = TargetPlatform.android_arm;
          break;
204
      }
205 206
    }

207
    return _platform;
208
  }
209

210
  @override
211 212
  Future<String> get sdkNameAndVersion async =>
      'Android ${await _sdkVersion} (API ${await _apiVersion})';
213

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

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

218
  _AdbLogReader _logReader;
219
  _AndroidDevicePortForwarder _portForwarder;
220

221
  List<String> adbCommandForDevice(List<String> args) {
222
    return <String>[getAdbPath(androidSdk), '-s', id, ...args];
223 224
  }

225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
  String runAdbCheckedSync(
      List<String> params, {
        String workingDirectory,
        bool allowReentrantFlutter = false,
        Map<String, String> environment}) {
    return runCheckedSync(adbCommandForDevice(params), workingDirectory: workingDirectory,
        allowReentrantFlutter: allowReentrantFlutter,
        environment: environment,
        whiteListFailures: allowHeapCorruptionOnWindows
    );
  }

  Future<RunResult> runAdbCheckedAsync(
      List<String> params, {
        String workingDirectory,
        bool allowReentrantFlutter = false,
      }) async {
    return runCheckedAsync(adbCommandForDevice(params), workingDirectory: workingDirectory,
        allowReentrantFlutter: allowReentrantFlutter,
        whiteListFailures: allowHeapCorruptionOnWindows);
  }

247 248
  bool _isValidAdbVersion(String adbVersion) {
    // Sample output: 'Android Debug Bridge version 1.0.31'
249
    final Match versionFields = RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
250
    if (versionFields != null) {
251 252 253
      final int majorVersion = int.parse(versionFields[1]);
      final int minorVersion = int.parse(versionFields[2]);
      final int patchVersion = int.parse(versionFields[3]);
254 255 256 257 258 259
      if (majorVersion > 1) {
        return true;
      }
      if (majorVersion == 1 && minorVersion > 0) {
        return true;
      }
260
      if (majorVersion == 1 && minorVersion == 0 && patchVersion >= 39) {
261 262 263 264
        return true;
      }
      return false;
    }
265
    printError(
266 267 268 269
        'Unrecognized adb version string $adbVersion. Skipping version check.');
    return true;
  }

270
  Future<bool> _checkForSupportedAdbVersion() async {
271 272 273
    if (androidSdk == null)
      return false;

274
    try {
275 276
      final RunResult adbVersion = await runCheckedAsync(<String>[getAdbPath(androidSdk), 'version']);
      if (_isValidAdbVersion(adbVersion.stdout))
277
        return true;
278
      printError('The ADB at "${getAdbPath(androidSdk)}" is too old; please install version 1.0.39 or later.');
279
    } catch (error, trace) {
280
      printError('Error running ADB: $error', stackTrace: trace);
281
    }
282

283 284 285
    return false;
  }

286
  Future<bool> _checkForSupportedAndroidVersion() async {
287 288 289 290 291
    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 *
292
      await runCheckedAsync(<String>[getAdbPath(androidSdk), 'start-server']);
293 294

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

297 298

      final int sdkVersionParsed = int.tryParse(sdkVersion);
299
      if (sdkVersionParsed == null) {
300
        printError('Unexpected response from getprop: "$sdkVersion"');
301 302
        return false;
      }
303

304
      if (sdkVersionParsed < minApiLevel) {
305
        printError(
306 307 308 309
          'The Android version ($sdkVersion) on the target device is too old. Please '
          'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.');
        return false;
      }
310

311 312
      return true;
    } catch (e) {
313
      printError('Unexpected failure from adb: $e');
314
      return false;
315 316 317 318 319 320 321
    }
  }

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

322 323 324
  Future<String> _getDeviceApkSha1(ApplicationPackage app) async {
    final RunResult result = await runAsync(adbCommandForDevice(<String>['shell', 'cat', _getDeviceSha1Path(app)]));
    return result.stdout;
325 326
  }

327
  String _getSourceSha1(ApplicationPackage app) {
328
    final AndroidApk apk = app;
329
    final File shaFile = fs.file('${apk.file.path}.sha1');
Devon Carew's avatar
Devon Carew committed
330
    return shaFile.existsSync() ? shaFile.readAsStringSync() : '';
331 332
  }

333
  @override
334 335 336
  String get name => modelID;

  @override
337
  Future<bool> isAppInstalled(ApplicationPackage app) async {
338
    // This call takes 400ms - 600ms.
339
    try {
340
      final RunResult listOut = await runAdbCheckedAsync(<String>['shell', 'pm', 'list', 'packages', app.id]);
341 342 343 344 345
      return LineSplitter.split(listOut.stdout).contains('package:${app.id}');
    } catch (error) {
      printTrace('$error');
      return false;
    }
346 347
  }

348
  @override
349 350
  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async {
    final String installedSha1 = await _getDeviceApkSha1(app);
351 352 353
    return installedSha1.isNotEmpty && installedSha1 == _getSourceSha1(app);
  }

354
  @override
355
  Future<bool> installApp(ApplicationPackage app) async {
356
    final AndroidApk apk = app;
357 358
    if (!apk.file.existsSync()) {
      printError('"${fs.path.relative(apk.file.path)}" does not exist.');
359 360 361
      return false;
    }

362
    if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
363 364
      return false;

365
    final Status status = logger.startProgress('Installing ${fs.path.relative(apk.file.path)}...', timeout: timeoutConfiguration.slowOperation);
366
    final RunResult installResult = await runAsync(adbCommandForDevice(<String>['install', '-t', '-r', apk.file.path]));
Devon Carew's avatar
Devon Carew committed
367
    status.stop();
368 369
    // Some versions of adb exit with exit code 0 even on failure :(
    // Parsing the output to check for failures.
370
    final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true);
371
    final String failure = failureExp.stringMatch(installResult.stdout);
372 373 374 375
    if (failure != null) {
      printError('Package install error: $failure');
      return false;
    }
376 377
    if (installResult.exitCode != 0) {
      printError('Error: ADB exited with exit code ${installResult.exitCode}');
378
      printError('$installResult');
379 380
      return false;
    }
381 382 383 384 385 386 387 388
    try {
      await runAdbCheckedAsync(<String>[
        'shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app),
      ]);
    } on ProcessException catch (error) {
      printError('adb shell failed to write the SHA hash: $error.');
      return false;
    }
389 390 391
    return true;
  }

392
  @override
393
  Future<bool> uninstallApp(ApplicationPackage app) async {
394
    if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
395 396
      return false;

397 398 399 400 401 402 403
    String uninstallOut;
    try {
      uninstallOut = (await runCheckedAsync(adbCommandForDevice(<String>['uninstall', app.id]))).stdout;
    } catch (error) {
      printError('adb uninstall failed: $error');
      return false;
    }
404
    final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true);
405
    final String failure = failureExp.stringMatch(uninstallOut);
406 407 408 409 410 411 412 413
    if (failure != null) {
      printError('Package uninstall error: $failure');
      return false;
    }

    return true;
  }

414
  Future<bool> _installLatestApp(ApplicationPackage package) async {
415
    final bool wasInstalled = await isAppInstalled(package);
416
    if (wasInstalled) {
417
      if (await isLatestBuildInstalled(package)) {
418
        printTrace('Latest build already installed.');
419 420 421 422
        return true;
      }
    }
    printTrace('Installing APK.');
423
    if (!await installApp(package)) {
424 425 426
      printTrace('Warning: Failed to install APK.');
      if (wasInstalled) {
        printStatus('Uninstalling old version...');
427
        if (!await uninstallApp(package)) {
428 429 430
          printError('Error: Uninstalling old version failed.');
          return false;
        }
431
        if (!await installApp(package)) {
432 433 434 435 436
          printError('Error: Failed to install APK again.');
          return false;
        }
        return true;
      }
437 438 439 440 441
      return false;
    }
    return true;
  }

442 443
  @override
  Future<LaunchResult> startApp(
444
    ApplicationPackage package, {
445
    String mainPath,
446
    String route,
447 448
    DebuggingOptions debuggingOptions,
    Map<String, dynamic> platformArgs,
449 450
    bool prebuiltApplication = false,
    bool ipv6 = false,
451
    bool usesTerminalUi = true,
452
  }) async {
453
    if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
454
      return LaunchResult.failed();
455

456 457 458
    final TargetPlatform devicePlatform = await targetPlatform;
    if (!(devicePlatform == TargetPlatform.android_arm ||
          devicePlatform == TargetPlatform.android_arm64) &&
459
        !debuggingOptions.buildInfo.isDebug) {
460
      printError('Profile and release builds are only supported on ARM targets.');
461
      return LaunchResult.failed();
462 463
    }

464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481
    AndroidArch androidArch;
    switch (devicePlatform) {
      case TargetPlatform.android_arm:
        androidArch = AndroidArch.armeabi_v7a;
        break;
      case TargetPlatform.android_arm64:
        androidArch = AndroidArch.arm64_v8a;
        break;
      case TargetPlatform.android_x64:
        androidArch = AndroidArch.x86_64;
        break;
      case TargetPlatform.android_x86:
        androidArch = AndroidArch.x86;
        break;
      default:
        printError('Android platforms are only supported.');
        return LaunchResult.failed();
    }
482

483
    if (!prebuiltApplication || androidSdk.licensesAvailable && androidSdk.latestVersion == null) {
484
      printTrace('Building APK');
485
      final FlutterProject project = FlutterProject.current();
486 487 488
      await buildApk(
          project: project,
          target: mainPath,
489 490 491
          androidBuildInfo: AndroidBuildInfo(debuggingOptions.buildInfo,
            targetArchs: <AndroidArch>[androidArch]
          ),
492
      );
493 494
      // Package has been built, so we can get the updated application ID and
      // activity name from the .apk.
495
      package = await AndroidApk.fromAndroidProject(project.android);
496
    }
497 498 499 500
    // There was a failure parsing the android project information.
    if (package == null) {
      throwToolExit('Problem building Android application: see above error(s).');
    }
501

502 503 504
    printTrace("Stopping app '${package.name}' on $name.");
    await stopApp(package);

505
    if (!await _installLatestApp(package))
506
      return LaunchResult.failed();
507

508 509 510
    final bool traceStartup = platformArgs['trace-startup'] ?? false;
    final AndroidApk apk = package;
    printTrace('$this startApp');
511

512
    ProtocolDiscovery observatoryDiscovery;
513

514
    if (debuggingOptions.debuggingEnabled) {
515
      // TODO(devoncarew): Remember the forwarding information (so we can later remove the
516
      // port forwarding or set it up again when adb fails on us).
517
      observatoryDiscovery = ProtocolDiscovery.observatory(
518 519 520 521 522
        getLogReader(),
        portForwarder: portForwarder,
        hostPort: debuggingOptions.observatoryPort,
        ipv6: ipv6,
      );
Devon Carew's avatar
Devon Carew committed
523
    }
524

525 526
    List<String> cmd;

527
    cmd = <String>[
528 529
      'shell', 'am', 'start',
      '-a', 'android.intent.action.RUN',
530
      '-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP
531
      '--ez', 'enable-background-compilation', 'true',
532
      '--ez', 'enable-dart-profiling', 'true',
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565
      if (traceStartup)
        ...<String>['--ez', 'trace-startup', 'true'],
      if (route != null)
        ...<String>['--es', 'route', route],
      if (debuggingOptions.enableSoftwareRendering)
        ...<String>['--ez', 'enable-software-rendering', 'true'],
      if (debuggingOptions.skiaDeterministicRendering)
        ...<String>['--ez', 'skia-deterministic-rendering', 'true'],
      if (debuggingOptions.traceSkia)
        ...<String>['--ez', 'trace-skia', 'true'],
      if (debuggingOptions.traceSystrace)
        ...<String>['--ez', 'trace-systrace', 'true'],
      if (debuggingOptions.dumpSkpOnShaderCompilation)
        ...<String>['--ez', 'dump-skp-on-shader-compilation', 'true'],
      if (debuggingOptions.debuggingEnabled)
        ...<String>[
          if (debuggingOptions.buildInfo.isDebug)
            ...<String>[
              ...<String>['--ez', 'enable-checked-mode', 'true'],
              ...<String>['--ez', 'verify-entry-points', 'true'],
            ],
          if (debuggingOptions.startPaused)
            ...<String>['--ez', 'start-paused', 'true'],
          if (debuggingOptions.disableServiceAuthCodes)
            ...<String>['--ez', 'disable-service-auth-codes', 'true'],
          if (debuggingOptions.dartFlags.isNotEmpty)
            ...<String>['--es', 'dart-flags', debuggingOptions.dartFlags],
          if (debuggingOptions.useTestFonts)
            ...<String>['--ez', 'use-test-fonts', 'true'],
          if (debuggingOptions.verboseSystemLogs)
            ...<String>['--ez', 'verbose-logging', 'true'],
        ],
      apk.launchActivity,
566 567
    ];
    final String result = (await runAdbCheckedAsync(cmd)).stdout;
568 569
    // This invocation returns 0 even when it fails.
    if (result.contains('Error: ')) {
570
      printError(result.trim(), wrap: false);
571
      return LaunchResult.failed();
572
    }
573

574
    if (!debuggingOptions.debuggingEnabled)
575
      return LaunchResult.succeeded();
Devon Carew's avatar
Devon Carew committed
576

577 578 579
    // 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...');
580

581
    // TODO(danrubel): Waiting for observatory services can be made common across all devices.
582
    try {
583 584 585
      Uri observatoryUri;

      if (debuggingOptions.buildInfo.isDebug || debuggingOptions.buildInfo.isProfile) {
586
        observatoryUri = await observatoryDiscovery.uri;
Devon Carew's avatar
Devon Carew committed
587
      }
588

589
      return LaunchResult.succeeded(observatoryUri: observatoryUri);
590 591
    } catch (error) {
      printError('Error waiting for a debug connection: $error');
592
      return LaunchResult.failed();
593
    } finally {
594
      await observatoryDiscovery.cancel();
Devon Carew's avatar
Devon Carew committed
595
    }
596 597
  }

598
  @override
599 600 601 602
  bool get supportsHotReload => true;

  @override
  bool get supportsHotRestart => true;
603

604
  @override
605
  Future<bool> stopApp(ApplicationPackage app) {
606
    final List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]);
607 608
    return runCommandAndStreamOutput(command).then<bool>(
        (int exitCode) => exitCode == 0 || allowHeapCorruptionOnWindows(exitCode));
609 610
  }

611
  @override
612
  void clearLogs() {
613
    runSync(adbCommandForDevice(<String>['logcat', '-c']));
614 615
  }

616
  @override
617
  DeviceLogReader getLogReader({ ApplicationPackage app }) {
618
    // The Android log reader isn't app-specific.
619
    _logReader ??= _AdbLogReader(this);
620 621
    return _logReader;
  }
622

623
  @override
624
  DevicePortForwarder get portForwarder => _portForwarder ??= _AndroidDevicePortForwarder(this);
625

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

628
  /// Return the most recent timestamp in the Android log or [null] if there is
629
  /// no available timestamp. The format can be passed to logcat's -T option.
630
  String get lastLogcatTimestamp {
631 632 633 634 635 636 637 638 639
    String output;
    try {
      output = runAdbCheckedSync(<String>[
        'shell', '-x', 'logcat', '-v', 'time', '-t', '1',
      ]);
    } catch (error) {
      printError('Failed to extract the most recent timestamp from the Android log: $error.');
      return null;
    }
640
    final Match timeMatch = _timeRegExp.firstMatch(output);
641
    return timeMatch?.group(0);
642 643
  }

644
  @override
645 646
  bool isSupported() => true;

Devon Carew's avatar
Devon Carew committed
647 648 649 650
  @override
  bool get supportsScreenshot => true;

  @override
651
  Future<void> takeScreenshot(File outputFile) async {
Devon Carew's avatar
Devon Carew committed
652
    const String remotePath = '/data/local/tmp/flutter_screenshot.png';
653
    await runAdbCheckedAsync(<String>['shell', 'screencap', '-p', remotePath]);
654
    await runCheckedAsync(adbCommandForDevice(<String>['pull', remotePath, outputFile.path]));
655
    await runAdbCheckedAsync(<String>['shell', 'rm', remotePath]);
Devon Carew's avatar
Devon Carew committed
656
  }
657 658 659 660 661

  @override
  bool isSupportedForProject(FlutterProject flutterProject) {
    return flutterProject.android.existsSync();
  }
662
}
663

664
Map<String, String> parseAdbDeviceProperties(String str) {
665
  final Map<String, String> properties = <String, String>{};
666
  final RegExp propertyExp = RegExp(r'\[(.*?)\]: \[(.*?)\]');
667 668 669 670 671
  for (Match match in propertyExp.allMatches(str))
    properties[match.group(1)] = match.group(2);
  return properties;
}

672
/// Return the list of connected ADB devices.
673 674 675 676
List<AndroidDevice> getAdbDevices() {
  final String adbPath = getAdbPath(androidSdk);
  if (adbPath == null)
    return <AndroidDevice>[];
677 678 679 680
  String text;
  try {
    text = runSync(<String>[adbPath, 'devices', '-l']);
  } on ArgumentError catch (exception) {
681
    throwToolExit('Unable to find "adb", check your Android SDK installation and '
682
      'ANDROID_HOME environment variable: ${exception.message}');
683 684 685
  } on ProcessException catch (exception) {
    throwToolExit('Unable to run "adb", check your Android SDK installation and '
      'ANDROID_HOME environment variable: ${exception.executable}');
686
  }
687
  final List<AndroidDevice> devices = <AndroidDevice>[];
688 689 690 691 692 693 694 695 696 697 698 699 700
  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>[];
701
  } else {
702 703 704 705
    final String text = result.stdout;
    final List<String> diagnostics = <String>[];
    parseADBDeviceOutput(text, diagnostics: diagnostics);
    return diagnostics;
706
  }
707 708 709
}

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

712 713 714 715
/// 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
716 717
void parseADBDeviceOutput(
  String text, {
718
  List<AndroidDevice> devices,
719
  List<String> diagnostics,
720
}) {
721 722
  // Check for error messages from adb
  if (!text.contains('List of devices')) {
723 724
    diagnostics?.add(text);
    return;
725 726 727
  }

  for (String line in text.trim().split('\n')) {
728
    // Skip lines like: * daemon started successfully *
729 730 731
    if (line.startsWith('* daemon '))
      continue;

732
    // Skip lines about adb server and client version not matching
733
    if (line.startsWith(RegExp(r'adb server (version|is out of date)'))) {
734
      diagnostics?.add(line);
735 736 737
      continue;
    }

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

741
    if (_kDeviceRegex.hasMatch(line)) {
742
      final Match match = _kDeviceRegex.firstMatch(line);
743

744 745
      final String deviceID = match[1];
      final String deviceState = match[2];
746 747
      String rest = match[3];

748
      final Map<String, String> info = <String, String>{};
749 750 751 752
      if (rest != null && rest.isNotEmpty) {
        rest = rest.trim();
        for (String data in rest.split(' ')) {
          if (data.contains(':')) {
753
            final List<String> fields = data.split(':');
754 755 756 757 758 759 760
            info[fields[0]] = fields[1];
          }
        }
      }

      if (info['model'] != null)
        info['model'] = cleanAdbDeviceName(info['model']);
761 762

      if (deviceState == 'unauthorized') {
763
        diagnostics?.add(
764 765 766 767
          'Device $deviceID is not authorized.\n'
          'You might need to check your device for an authorization dialog.'
        );
      } else if (deviceState == 'offline') {
768
        diagnostics?.add('Device $deviceID is offline.');
769
      } else {
770
        devices?.add(AndroidDevice(
771 772 773
          deviceID,
          productID: info['product'],
          modelID: info['model'] ?? deviceID,
774
          deviceCodeName: info['device'],
775
        ));
776
      }
777
    } else {
778
      diagnostics?.add(
779 780
        'Unexpected failure parsing device information from adb output:\n'
        '$line\n'
781
        'Please report a bug at https://github.com/flutter/flutter/issues/new/choose');
782 783 784 785
    }
  }
}

786
/// A log reader that logs from `adb logcat`.
Devon Carew's avatar
Devon Carew committed
787
class _AdbLogReader extends DeviceLogReader {
Devon Carew's avatar
Devon Carew committed
788
  _AdbLogReader(this.device) {
789
    _linesController = StreamController<String>.broadcast(
Devon Carew's avatar
Devon Carew committed
790
      onListen: _start,
791
      onCancel: _stop,
Devon Carew's avatar
Devon Carew committed
792 793
    );
  }
Devon Carew's avatar
Devon Carew committed
794 795 796

  final AndroidDevice device;

Devon Carew's avatar
Devon Carew committed
797
  StreamController<String> _linesController;
798
  Process _process;
799

800
  @override
Devon Carew's avatar
Devon Carew committed
801
  Stream<String> get logLines => _linesController.stream;
802

803
  @override
804
  String get name => device.name;
Devon Carew's avatar
Devon Carew committed
805

806 807 808 809 810 811 812
  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.
813
    return DateTime.parse('${DateTime.now().year}-$adbTimestamp');
814 815
  }

Devon Carew's avatar
Devon Carew committed
816
  void _start() {
817
    // Start the adb logcat process.
818
    final List<String> args = <String>['shell', '-x', 'logcat', '-v', 'time'];
819
    final String lastTimestamp = device.lastLogcatTimestamp;
820
    if (lastTimestamp != null)
821
      _timeOrigin = _adbTimestampToDateTime(lastTimestamp);
822
    else
823
      _timeOrigin = null;
824
    runCommand(device.adbCommandForDevice(args)).then<void>((Process process) {
Devon Carew's avatar
Devon Carew committed
825
      _process = process;
826 827 828
      // We expect logcat streams to occasionally contain invalid utf-8,
      // see: https://github.com/flutter/flutter/pull/8864.
      const Utf8Decoder decoder = Utf8Decoder(reportErrors: false);
829 830
      _process.stdout.transform<String>(decoder).transform<String>(const LineSplitter()).listen(_onLine);
      _process.stderr.transform<String>(decoder).transform<String>(const LineSplitter()).listen(_onLine);
831
      _process.exitCode.whenComplete(() {
Devon Carew's avatar
Devon Carew committed
832 833 834 835
        if (_linesController.hasListener)
          _linesController.close();
      });
    });
836 837
  }

838
  // 'W/ActivityManager(pid): '
839
  static final RegExp _logFormat = RegExp(r'^[VDIWEF]\/.*?\(\s*(\d+)\):\s');
840 841

  static final List<RegExp> _whitelistedTags = <RegExp>[
842 843 844 845 846
    RegExp(r'^[VDIWEF]\/flutter[^:]*:\s+', caseSensitive: false),
    RegExp(r'^[IE]\/DartVM[^:]*:\s+'),
    RegExp(r'^[WEF]\/AndroidRuntime:\s+'),
    RegExp(r'^[WEF]\/ActivityManager:\s+.*(\bflutter\b|\bdomokit\b|\bsky\b)'),
    RegExp(r'^[WEF]\/System\.err:\s+'),
847
    RegExp(r'^[F]\/[\S^:]+:\s+'),
848 849
  ];

850
  // 'F/libc(pid): Fatal signal 11'
851
  static final RegExp _fatalLog = RegExp(r'^F\/libc\s*\(\s*\d+\):\sFatal signal (\d+)');
852 853

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

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

859 860 861
  // we default to true in case none of the log lines match
  bool _acceptedLastLine = true;

862 863 864 865 866
  // 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;

867 868 869
  // 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): ....
870
  void _onLine(String line) {
871 872 873 874 875 876
    final Match timeMatch = AndroidDevice._timeRegExp.firstMatch(line);
    if (timeMatch == null) {
      return;
    }
    if (_timeOrigin != null) {
      final String timestamp = timeMatch.group(0);
877
      final DateTime time = _adbTimestampToDateTime(timestamp);
878
      if (!time.isAfter(_timeOrigin)) {
879 880 881 882 883 884 885 886 887
        // Ignore log messages before the origin.
        return;
      }
    }
    if (line.length == timeMatch.end) {
      return;
    }
    // Chop off the time.
    line = line.substring(timeMatch.end + 1);
888 889 890
    final Match logMatch = _logFormat.firstMatch(line);
    if (logMatch != null) {
      bool acceptLine = false;
891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908

      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) {
909
        acceptLine = true;
910 911 912 913 914

        if (_fatalLog.hasMatch(line)) {
          // Hit fatal signal, app is now crashing
          _fatalCrash = true;
        }
915 916 917 918
      } else {
        // Filter on approved names and levels.
        acceptLine = _whitelistedTags.any((RegExp re) => re.hasMatch(line));
      }
919

920 921 922 923
      if (acceptLine) {
        _acceptedLastLine = true;
        _linesController.add(line);
        return;
924
      }
925 926
      _acceptedLastLine = false;
    } else if (line == '--------- beginning of system' ||
927
               line == '--------- beginning of main') {
928 929
      // hide the ugly adb logcat log boundaries at the start
      _acceptedLastLine = false;
930
    } else {
931 932 933
      // 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) {
934
        _linesController.add(line);
935 936
        return;
      }
937
    }
Devon Carew's avatar
Devon Carew committed
938 939
  }

Devon Carew's avatar
Devon Carew committed
940 941
  void _stop() {
    // TODO(devoncarew): We should remove adb port forwarding here.
Devon Carew's avatar
Devon Carew committed
942

Devon Carew's avatar
Devon Carew committed
943
    _process?.kill();
Devon Carew's avatar
Devon Carew committed
944 945
  }
}
946 947 948 949 950 951 952

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

  final AndroidDevice device;

  static int _extractPort(String portString) {
953 954

    return int.tryParse(portString.trim());
955 956
  }

957
  @override
958 959 960
  List<ForwardedPort> get forwardedPorts {
    final List<ForwardedPort> ports = <ForwardedPort>[];

961 962 963 964 965 966 967 968 969
    String stdout;
    try {
      stdout = runCheckedSync(device.adbCommandForDevice(
        <String>['forward', '--list']
      ));
    } catch (error) {
      printError('Failed to list forwarded ports: $error.');
      return ports;
    }
970

971
    final List<String> lines = LineSplitter.split(stdout).toList();
972 973
    for (String line in lines) {
      if (line.startsWith(device.id)) {
974
        final List<String> splitLine = line.split('tcp:');
975 976 977 978 979 980

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

        // Attempt to extract ports.
981 982
        final int hostPort = _extractPort(splitLine[1]);
        final int devicePort = _extractPort(splitLine[2]);
983 984

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

988
        ports.add(ForwardedPort(hostPort, devicePort));
989 990 991 992 993 994
      }
    }

    return ports;
  }

995
  @override
996
  Future<int> forward(int devicePort, { int hostPort }) async {
997 998
    hostPort ??= 0;
    final RunResult process = await runCheckedAsync(device.adbCommandForDevice(
999 1000
      <String>['forward', 'tcp:$hostPort', 'tcp:$devicePort']
    ));
1001

1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015
    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 {
1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029
      // stdout may be empty or the port we asked it to forward, though it's
      // not documented (or obvious) what triggers each case.
      //
      // Observations are:
      //   - On MacOS it's always empty when Flutter spawns the process, but
      //   - On MacOS it prints the port number when run from the terminal, unless
      //     the port is already forwarded, when it also prints nothing.
      //   - On ChromeOS, the port appears to be printed even when Flutter spawns
      //     the process
      //
      // To cover all cases, we accept the output being either empty or exactly
      // the port number, but treat any other output as probably being an error
      // message.
      if (process.stdout.isNotEmpty && process.stdout.trim() != '$hostPort')
1030 1031 1032
        process.throwException('adb returned error:\n${process.stdout}');
    }

1033 1034 1035
    return hostPort;
  }

1036
  @override
1037
  Future<void> unforward(ForwardedPort forwardedPort) async {
1038
    await runCheckedAsync(device.adbCommandForDevice(
1039 1040
      <String>['forward', '--remove', 'tcp:${forwardedPort.hostPort}']
    ));
1041 1042
  }
}