android_device.dart 44.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7 8
import 'dart:async';

9
import 'package:meta/meta.dart';
10
import 'package:process/process.dart';
11

12
import '../android/android_builder.dart';
13
import '../android/android_sdk.dart';
14
import '../application_package.dart';
15
import '../base/common.dart' show throwToolExit, unawaited;
16
import '../base/file_system.dart';
17
import '../base/io.dart';
18
import '../base/logger.dart';
19
import '../base/platform.dart';
20
import '../base/process.dart';
21
import '../build_info.dart';
22
import '../convert.dart';
23
import '../device.dart';
24
import '../device_port_forwarder.dart';
25
import '../project.dart';
26
import '../protocol_discovery.dart';
27

28
import 'android.dart';
29
import 'android_console.dart';
30
import 'android_sdk.dart';
31
import 'application_package.dart';
32

33 34
/// Whether the [AndroidDevice] is believed to be a physical device or an emulator.
enum HardwareType { emulator, physical }
35 36

/// Map to help our `isLocalEmulator` detection.
37 38 39 40 41 42 43 44 45 46 47 48 49 50
///
/// See [AndroidDevice] for more explanation of why this is needed.
const Map<String, HardwareType> kKnownHardware = <String, HardwareType>{
  'goldfish': HardwareType.emulator,
  'qcom': HardwareType.physical,
  'ranchu': HardwareType.emulator,
  'samsungexynos7420': HardwareType.physical,
  'samsungexynos7580': HardwareType.physical,
  'samsungexynos7870': HardwareType.physical,
  'samsungexynos7880': HardwareType.physical,
  'samsungexynos8890': HardwareType.physical,
  'samsungexynos8895': HardwareType.physical,
  'samsungexynos9810': HardwareType.physical,
  'samsungexynos7570': HardwareType.physical,
51 52
};

53 54 55
/// A physical Android device or emulator.
///
/// While [isEmulator] attempts to distinguish between the device categories,
56
/// this is a best effort process and not a guarantee; certain physical devices
57 58
/// identify as emulators. These device identifiers may be added to the [kKnownHardware]
/// map to specify that they are actually physical devices.
59
class AndroidDevice extends Device {
60 61 62 63
  AndroidDevice(
    String id, {
    this.productID,
    this.modelID,
64
    this.deviceCodeName,
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
    @required Logger logger,
    @required ProcessManager processManager,
    @required Platform platform,
    @required AndroidSdk androidSdk,
    @required FileSystem fileSystem,
    AndroidConsoleSocketFactory androidConsoleSocketFactory = kAndroidConsoleSocketFactory,
  }) : _logger = logger,
       _processManager = processManager,
       _androidSdk = androidSdk,
       _platform = platform,
       _fileSystem = fileSystem,
       _androidConsoleSocketFactory = androidConsoleSocketFactory,
       _processUtils = ProcessUtils(logger: logger, processManager: processManager),
       super(
         id,
         category: Category.mobile,
         platformType: PlatformType.android,
         ephemeral: true,
       );

  final Logger _logger;
  final ProcessManager _processManager;
  final AndroidSdk _androidSdk;
  final Platform _platform;
  final FileSystem _fileSystem;
  final ProcessUtils _processUtils;
  final AndroidConsoleSocketFactory _androidConsoleSocketFactory;
92

93 94 95 96
  final String productID;
  final String modelID;
  final String deviceCodeName;

97
  Map<String, String> _properties;
98
  bool _isLocalEmulator;
99
  TargetPlatform _applicationPlatform;
100

101
  Future<String> _getProperty(String name) async {
102 103
    if (_properties == null) {
      _properties = <String, String>{};
104

105
      final List<String> propCommand = adbCommandForDevice(<String>['shell', 'getprop']);
106
      _logger.printTrace(propCommand.join(' '));
107 108

      try {
109
        // We pass an encoding of latin1 so that we don't try and interpret the
110
        // `adb shell getprop` result as UTF8.
111
        final ProcessResult result = await _processManager.run(
112
          propCommand,
113 114
          stdoutEncoding: latin1,
          stderrEncoding: latin1,
115
        );
116
        if (result.exitCode == 0 || _allowHeapCorruptionOnWindows(result.exitCode, _platform)) {
117
          _properties = parseAdbDeviceProperties(result.stdout as String);
118
        } else {
119 120
          _logger.printError('Error ${result.exitCode} retrieving device properties for $name:');
          _logger.printError(result.stderr as String);
121
        }
122
      } on ProcessException catch (error) {
123
        _logger.printError('Error retrieving device properties for $name: $error');
124 125
      }
    }
126

127 128
    return _properties[name];
  }
129

130
  @override
131
  Future<bool> get isLocalEmulator async {
132
    if (_isLocalEmulator == null) {
133
      final String hardware = await _getProperty('ro.hardware');
134
      _logger.printTrace('ro.hardware = $hardware');
135
      if (kKnownHardware.containsKey(hardware)) {
136
        // Look for known hardware models.
137
        _isLocalEmulator = kKnownHardware[hardware] == HardwareType.emulator;
138 139 140
      } else {
        // Fall back to a best-effort heuristic-based approach.
        final String characteristics = await _getProperty('ro.build.characteristics');
141
        _logger.printTrace('ro.build.characteristics = $characteristics');
142 143
        _isLocalEmulator = characteristics != null && characteristics.contains('emulator');
      }
144 145 146 147
    }
    return _isLocalEmulator;
  }

148 149 150 151 152 153 154 155
  /// 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 {
156
    if (!(await isLocalEmulator)) {
157
      return null;
158
    }
159 160 161 162 163 164 165 166 167 168 169 170

    // 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));
171
    _logger.printTrace('Fetching avd name for $name via Android console on $host:$port');
172 173

    try {
174
      final Socket socket = await _androidConsoleSocketFactory(host, port);
175 176 177 178 179
      final AndroidConsole console = AndroidConsole(socket);

      try {
        await console
            .connect()
180
            .timeout(const Duration(seconds: 2),
181 182 183 184
                onTimeout: () => throw TimeoutException('Connection timed out'));

        return await console
            .getAvdName()
185
            .timeout(const Duration(seconds: 2),
186 187 188 189
                onTimeout: () => throw TimeoutException('"avd name" timed out'));
      } finally {
        console.destroy();
      }
190
    } on Exception catch (e) {
191
      _logger.printTrace('Failed to fetch avd name for emulator at $host:$port: $e');
192 193 194 195 196 197
      // 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;
    }
  }

198
  @override
199
  Future<TargetPlatform> get targetPlatform async {
200
    if (_applicationPlatform == null) {
201
      // http://developer.android.com/ndk/guides/abis.html (x86, armeabi-v7a, ...)
202
      switch (await _getProperty('ro.product.cpu.abi')) {
203
        case 'arm64-v8a':
204 205 206 207 208 209
          // Perform additional verification for 64 bit ABI. Some devices,
          // like the Kindle Fire 8, misreport the abilist. We might not
          // be able to retrieve this property, in which case we fall back
          // to assuming 64 bit.
          final String abilist = await _getProperty('ro.product.cpu.abilist');
          if (abilist == null || abilist.contains('arm64-v8a')) {
210
            _applicationPlatform = TargetPlatform.android_arm64;
211
          } else {
212
            _applicationPlatform = TargetPlatform.android_arm;
213
          }
214
          break;
215
        case 'x86_64':
216
          _applicationPlatform = TargetPlatform.android_x64;
217 218
          break;
        case 'x86':
219
          _applicationPlatform = TargetPlatform.android_x86;
220 221
          break;
        default:
222
          _applicationPlatform = TargetPlatform.android_arm;
223
          break;
224
      }
225 226
    }

227
    return _applicationPlatform;
228
  }
229

230 231 232 233 234 235 236 237 238 239 240 241 242 243
  @override
  Future<bool> supportsRuntimeMode(BuildMode buildMode) async {
    switch (await targetPlatform) {
      case TargetPlatform.android_arm:
      case TargetPlatform.android_arm64:
      case TargetPlatform.android_x64:
        return buildMode != BuildMode.jitRelease;
      case TargetPlatform.android_x86:
        return buildMode == BuildMode.debug;
      default:
        throw UnsupportedError('Invalid target platform for Android');
    }
  }

244
  @override
245
  Future<String> get sdkNameAndVersion async => 'Android ${await _sdkVersion} (API ${await apiVersion})';
246

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

249 250
  @visibleForTesting
  Future<String> get apiVersion => _getProperty('ro.build.version.sdk');
251

252
  AdbLogReader _logReader;
253
  AdbLogReader _pastLogReader;
254
  AndroidDevicePortForwarder _portForwarder;
255

256
  List<String> adbCommandForDevice(List<String> args) {
257
    return <String>[_androidSdk.adbPath, '-s', id, ...args];
258 259
  }

260
  Future<RunResult> runAdbCheckedAsync(
261 262 263 264
    List<String> params, {
    String workingDirectory,
    bool allowReentrantFlutter = false,
  }) async {
265
    return _processUtils.run(
266 267 268 269
      adbCommandForDevice(params),
      throwOnError: true,
      workingDirectory: workingDirectory,
      allowReentrantFlutter: allowReentrantFlutter,
270
      allowedFailures: (int value) => _allowHeapCorruptionOnWindows(value, _platform),
271
    );
272 273
  }

274 275
  bool _isValidAdbVersion(String adbVersion) {
    // Sample output: 'Android Debug Bridge version 1.0.31'
276
    final Match versionFields = RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
277
    if (versionFields != null) {
278 279 280
      final int majorVersion = int.parse(versionFields[1]);
      final int minorVersion = int.parse(versionFields[2]);
      final int patchVersion = int.parse(versionFields[3]);
281 282 283 284 285 286
      if (majorVersion > 1) {
        return true;
      }
      if (majorVersion == 1 && minorVersion > 0) {
        return true;
      }
287
      if (majorVersion == 1 && minorVersion == 0 && patchVersion >= 39) {
288 289 290 291
        return true;
      }
      return false;
    }
292
    _logger.printError(
293 294 295 296
        'Unrecognized adb version string $adbVersion. Skipping version check.');
    return true;
  }

297
  Future<bool> _checkForSupportedAdbVersion() async {
298
    if (_androidSdk == null) {
299
      return false;
300
    }
301

302
    try {
303
      final RunResult adbVersion = await _processUtils.run(
304
        <String>[_androidSdk.adbPath, 'version'],
305 306 307
        throwOnError: true,
      );
      if (_isValidAdbVersion(adbVersion.stdout)) {
308
        return true;
309
      }
310
      _logger.printError('The ADB at "${_androidSdk.adbPath}" is too old; please install version 1.0.39 or later.');
311
    } on Exception catch (error, trace) {
312
      _logger.printError('Error running ADB: $error', stackTrace: trace);
313
    }
314

315 316 317
    return false;
  }

318
  Future<bool> _checkForSupportedAndroidVersion() async {
319 320 321 322 323
    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 *
324
      await _processUtils.run(
325
        <String>[_androidSdk.adbPath, 'start-server'],
326 327
        throwOnError: true,
      );
328

329 330
      // This has been reported to return null on some devices. In this case,
      // assume the lowest supported API to still allow Flutter to run.
331
      // Sample output: '22'
332 333
      final String sdkVersion = await _getProperty('ro.build.version.sdk')
        ?? minApiLevel.toString();
334 335

      final int sdkVersionParsed = int.tryParse(sdkVersion);
336
      if (sdkVersionParsed == null) {
337
        _logger.printError('Unexpected response from getprop: "$sdkVersion"');
338 339
        return false;
      }
340

341
      if (sdkVersionParsed < minApiLevel) {
342
        _logger.printError(
343 344 345 346
          'The Android version ($sdkVersion) on the target device is too old. Please '
          'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.');
        return false;
      }
347

348
      return true;
349
    } on Exception catch (e, stacktrace) {
350 351
      _logger.printError('Unexpected failure from adb: $e');
      _logger.printError('Stacktrace: $stacktrace');
352
      return false;
353 354 355
    }
  }

356 357
  String _getDeviceSha1Path(AndroidApk apk) {
    return '/data/local/tmp/sky.${apk.id}.sha1';
358 359
  }

360
  Future<String> _getDeviceApkSha1(AndroidApk apk) async {
361
    final RunResult result = await _processUtils.run(
362
      adbCommandForDevice(<String>['shell', 'cat', _getDeviceSha1Path(apk)]));
363
    return result.stdout;
364 365
  }

366
  String _getSourceSha1(AndroidApk apk) {
367
    final File shaFile = _fileSystem.file('${apk.file.path}.sha1');
Devon Carew's avatar
Devon Carew committed
368
    return shaFile.existsSync() ? shaFile.readAsStringSync() : '';
369 370
  }

371
  @override
372 373 374
  String get name => modelID;

  @override
375 376 377 378
  Future<bool> isAppInstalled(
    AndroidApk app, {
    String userIdentifier,
  }) async {
379
    // This call takes 400ms - 600ms.
380
    try {
381 382 383 384 385 386 387 388 389
      final RunResult listOut = await runAdbCheckedAsync(<String>[
        'shell',
        'pm',
        'list',
        'packages',
        if (userIdentifier != null)
          ...<String>['--user', userIdentifier],
        app.id
      ]);
390
      return LineSplitter.split(listOut.stdout).contains('package:${app.id}');
391
    } on Exception catch (error) {
392
      _logger.printTrace('$error');
393 394
      return false;
    }
395 396
  }

397
  @override
398
  Future<bool> isLatestBuildInstalled(AndroidApk app) async {
399
    final String installedSha1 = await _getDeviceApkSha1(app);
400 401 402
    return installedSha1.isNotEmpty && installedSha1 == _getSourceSha1(app);
  }

403
  @override
404 405 406 407
  Future<bool> installApp(
    AndroidApk app, {
    String userIdentifier,
  }) async {
408 409 410 411 412 413 414 415 416 417 418 419 420 421
    if (!await _isAdbValid()) {
      return false;
    }
    final bool wasInstalled = await isAppInstalled(app, userIdentifier: userIdentifier);
    if (wasInstalled && await isLatestBuildInstalled(app)) {
      _logger.printTrace('Latest build already installed.');
      return true;
    }
    _logger.printTrace('Installing APK.');
    if (await _installApp(app, userIdentifier: userIdentifier)) {
      return true;
    }
    _logger.printTrace('Warning: Failed to install APK.');
    if (!wasInstalled) {
422 423
      return false;
    }
424 425 426 427 428 429 430 431 432 433 434
    _logger.printStatus('Uninstalling old version...');
    if (!await uninstallApp(app, userIdentifier: userIdentifier)) {
      _logger.printError('Error: Uninstalling old version failed.');
      return false;
    }
    if (!await _installApp(app, userIdentifier: userIdentifier)) {
      _logger.printError('Error: Failed to install APK again.');
      return false;
    }
    return true;
  }
435

436 437 438 439 440 441
  Future<bool> _installApp(
    AndroidApk app, {
    String userIdentifier,
  }) async {
    if (!app.file.existsSync()) {
      _logger.printError('"${_fileSystem.path.relative(app.file.path)}" does not exist.');
442
      return false;
443
    }
444

445 446 447 448
    final Status status = _logger.startProgress(
      'Installing ${_fileSystem.path.relative(app.file.path)}...',
    );
    final RunResult installResult = await _processUtils.run(
449 450 451 452 453 454 455 456
      adbCommandForDevice(<String>[
        'install',
        '-t',
        '-r',
        if (userIdentifier != null)
          ...<String>['--user', userIdentifier],
        app.file.path
      ]));
Devon Carew's avatar
Devon Carew committed
457
    status.stop();
458 459
    // Some versions of adb exit with exit code 0 even on failure :(
    // Parsing the output to check for failures.
460
    final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true);
461
    final String failure = failureExp.stringMatch(installResult.stdout);
462
    if (failure != null) {
463
      _logger.printError('Package install error: $failure');
464 465
      return false;
    }
466
    if (installResult.exitCode != 0) {
467 468 469 470 471 472
      if (installResult.stderr.contains('Bad user number')) {
        _logger.printError('Error: User "$userIdentifier" not found. Run "adb shell pm list users" to see list of available identifiers.');
      } else {
        _logger.printError('Error: ADB exited with exit code ${installResult.exitCode}');
        _logger.printError('$installResult');
      }
473 474
      return false;
    }
475 476 477 478 479
    try {
      await runAdbCheckedAsync(<String>[
        'shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app),
      ]);
    } on ProcessException catch (error) {
480
      _logger.printError('adb shell failed to write the SHA hash: $error.');
481 482
      return false;
    }
483 484 485
    return true;
  }

486
  @override
487 488 489 490
  Future<bool> uninstallApp(
    AndroidApk app, {
    String userIdentifier,
  }) async {
491
    if (!await _isAdbValid()) {
492
      return false;
493
    }
494

495 496
    String uninstallOut;
    try {
497
      final RunResult uninstallResult = await _processUtils.run(
498 499 500 501 502
        adbCommandForDevice(<String>[
          'uninstall',
          if (userIdentifier != null)
            ...<String>['--user', userIdentifier],
          app.id]),
503 504 505
        throwOnError: true,
      );
      uninstallOut = uninstallResult.stdout;
506
    } on Exception catch (error) {
507
      _logger.printError('adb uninstall failed: $error');
508 509
      return false;
    }
510
    final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true);
511
    final String failure = failureExp.stringMatch(uninstallOut);
512
    if (failure != null) {
513
      _logger.printError('Package uninstall error: $failure');
514 515 516 517 518
      return false;
    }
    return true;
  }

519 520 521 522
  // Whether the adb and Android versions are aligned.
  bool _adbIsValid;
  Future<bool> _isAdbValid() async {
    return _adbIsValid ??= await _checkForSupportedAdbVersion() && await _checkForSupportedAndroidVersion();
523 524
  }

525 526
  AndroidApk _package;

527 528
  @override
  Future<LaunchResult> startApp(
529
    AndroidApk package, {
530
    String mainPath,
531
    String route,
532 533
    DebuggingOptions debuggingOptions,
    Map<String, dynamic> platformArgs,
534 535
    bool prebuiltApplication = false,
    bool ipv6 = false,
536
    String userIdentifier,
537
  }) async {
538
    if (!await _isAdbValid()) {
539
      return LaunchResult.failed();
540
    }
541

542
    final TargetPlatform devicePlatform = await targetPlatform;
543 544
    if (devicePlatform == TargetPlatform.android_x86 &&
       !debuggingOptions.buildInfo.isDebug) {
545
      _logger.printError('Profile and release builds are only supported on ARM/x64 targets.');
546
      return LaunchResult.failed();
547 548
    }

549 550 551 552 553 554 555 556 557 558 559 560 561 562 563
    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:
564
        _logger.printError('Android platforms are only supported.');
565 566
        return LaunchResult.failed();
    }
567

568 569
    if (!prebuiltApplication || _androidSdk.licensesAvailable && _androidSdk.latestVersion == null) {
      _logger.printTrace('Building APK');
570
      final FlutterProject project = FlutterProject.current();
571
      await androidBuilder.buildApk(
572 573
          project: project,
          target: mainPath,
574 575 576
          androidBuildInfo: AndroidBuildInfo(
            debuggingOptions.buildInfo,
            targetArchs: <AndroidArch>[androidArch],
577
            fastStart: debuggingOptions.fastStart
578
          ),
579
      );
580 581
      // Package has been built, so we can get the updated application ID and
      // activity name from the .apk.
582 583
      package = await ApplicationPackageFactory.instance
        .getPackageForPlatform(devicePlatform, buildInfo: debuggingOptions.buildInfo) as AndroidApk;
584
    }
585 586 587 588
    // There was a failure parsing the android project information.
    if (package == null) {
      throwToolExit('Problem building Android application: see above error(s).');
    }
589

590
    _logger.printTrace("Stopping app '${package.name}' on $name.");
591
    await stopApp(package, userIdentifier: userIdentifier);
592

593
    if (!await installApp(package, userIdentifier: userIdentifier)) {
594
      return LaunchResult.failed();
595
    }
596

597
    final bool traceStartup = platformArgs['trace-startup'] as bool ?? false;
598
    ProtocolDiscovery observatoryDiscovery;
599

600
    if (debuggingOptions.debuggingEnabled) {
601
      observatoryDiscovery = ProtocolDiscovery.observatory(
602 603 604 605 606 607 608
        // Avoid using getLogReader, which returns a singleton instance, because the
        // observatory discovery will dipose at the end. creating a new logger here allows
        // logs to be surfaced normally during `flutter drive`.
        await AdbLogReader.createLogReader(
          this,
          _processManager,
        ),
609
        portForwarder: portForwarder,
610
        hostPort: debuggingOptions.hostVmServicePort,
611
        devicePort: debuggingOptions.deviceVmServicePort,
612
        ipv6: ipv6,
613
        logger: _logger,
614
      );
Devon Carew's avatar
Devon Carew committed
615
    }
616

617 618
    final String dartVmFlags = computeDartVmFlags(debuggingOptions);
    final List<String> cmd = <String>[
619 620
      'shell', 'am', 'start',
      '-a', 'android.intent.action.RUN',
621
      '-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP
622
      '--ez', 'enable-background-compilation', 'true',
623
      '--ez', 'enable-dart-profiling', 'true',
624 625 626 627 628 629 630 631 632 633
      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'],
634 635
      if (debuggingOptions.traceAllowlist != null)
        ...<String>['--ez', 'trace-allowlist', debuggingOptions.traceAllowlist],
636 637
      if (debuggingOptions.traceSystrace)
        ...<String>['--ez', 'trace-systrace', 'true'],
638 639
      if (debuggingOptions.endlessTraceBuffer)
        ...<String>['--ez', 'endless-trace-buffer', 'true'],
640 641
      if (debuggingOptions.dumpSkpOnShaderCompilation)
        ...<String>['--ez', 'dump-skp-on-shader-compilation', 'true'],
642 643
      if (debuggingOptions.cacheSkSL)
      ...<String>['--ez', 'cache-sksl', 'true'],
644 645
      if (debuggingOptions.purgePersistentCache)
        ...<String>['--ez', 'purge-persistent-cache', 'true'],
646 647 648 649
      if (debuggingOptions.debuggingEnabled) ...<String>[
        if (debuggingOptions.buildInfo.isDebug) ...<String>[
          ...<String>['--ez', 'enable-checked-mode', 'true'],
          ...<String>['--ez', 'verify-entry-points', 'true'],
650
        ],
651 652 653 654
        if (debuggingOptions.startPaused)
          ...<String>['--ez', 'start-paused', 'true'],
        if (debuggingOptions.disableServiceAuthCodes)
          ...<String>['--ez', 'disable-service-auth-codes', 'true'],
655 656
        if (dartVmFlags.isNotEmpty)
          ...<String>['--es', 'dart-flags', dartVmFlags],
657 658 659 660
        if (debuggingOptions.useTestFonts)
          ...<String>['--ez', 'use-test-fonts', 'true'],
        if (debuggingOptions.verboseSystemLogs)
          ...<String>['--ez', 'verbose-logging', 'true'],
661 662
        if (userIdentifier != null)
          ...<String>['--user', userIdentifier],
663
      ],
664
      package.launchActivity,
665 666
    ];
    final String result = (await runAdbCheckedAsync(cmd)).stdout;
667 668
    // This invocation returns 0 even when it fails.
    if (result.contains('Error: ')) {
669
      _logger.printError(result.trim(), wrap: false);
670
      return LaunchResult.failed();
671
    }
672

673
    _package = package;
674
    if (!debuggingOptions.debuggingEnabled) {
675
      return LaunchResult.succeeded();
676
    }
Devon Carew's avatar
Devon Carew committed
677

678 679
    // Wait for the service protocol port here. This will complete once the
    // device has printed "Observatory is listening on...".
680
    _logger.printTrace('Waiting for observatory port to be available...');
681
    try {
682 683
      Uri observatoryUri;
      if (debuggingOptions.buildInfo.isDebug || debuggingOptions.buildInfo.isProfile) {
684
        observatoryUri = await observatoryDiscovery.uri;
685
        if (observatoryUri == null) {
686
          _logger.printError(
687 688 689 690 691
            'Error waiting for a debug connection: '
            'The log reader stopped unexpectedly',
          );
          return LaunchResult.failed();
        }
Devon Carew's avatar
Devon Carew committed
692
      }
693
      return LaunchResult.succeeded(observatoryUri: observatoryUri);
694
    } on Exception catch (error) {
695
      _logger.printError('Error waiting for a debug connection: $error');
696
      return LaunchResult.failed();
697
    } finally {
698
      await observatoryDiscovery.cancel();
Devon Carew's avatar
Devon Carew committed
699
    }
700 701
  }

702
  @override
703 704 705 706
  bool get supportsHotReload => true;

  @override
  bool get supportsHotRestart => true;
707

708 709 710
  @override
  bool get supportsFastStart => true;

711
  @override
712 713 714 715
  Future<bool> stopApp(
    AndroidApk app, {
    String userIdentifier,
  }) {
716 717 718
    if (app == null) {
      return Future<bool>.value(false);
    }
719 720 721 722 723 724 725 726
    final List<String> command = adbCommandForDevice(<String>[
      'shell',
      'am',
      'force-stop',
      if (userIdentifier != null)
        ...<String>['--user', userIdentifier],
      app.id,
    ]);
727
    return _processUtils.stream(command).then<bool>(
728
        (int exitCode) => exitCode == 0 || _allowHeapCorruptionOnWindows(exitCode, _platform));
729 730
  }

731 732
  @override
  Future<MemoryInfo> queryMemoryInfo() async {
733
    final RunResult runResult = await _processUtils.run(adbCommandForDevice(<String>[
734 735 736
      'shell',
      'dumpsys',
      'meminfo',
737
      _package.id,
738 739
      '-d',
    ]));
740

741 742 743 744 745 746
    if (runResult.exitCode != 0) {
      return const MemoryInfo.empty();
    }
    return parseMeminfoDump(runResult.stdout);
  }

747
  @override
748
  void clearLogs() {
749
    _processUtils.runSync(adbCommandForDevice(<String>['logcat', '-c']));
750 751
  }

752
  @override
753 754 755 756 757 758 759 760
  FutureOr<DeviceLogReader> getLogReader({
    AndroidApk app,
    bool includePastLogs = false,
  }) async {
    // The Android log reader isn't app-specific. The `app` parameter isn't used.
    if (includePastLogs) {
      return _pastLogReader ??= await AdbLogReader.createLogReader(
        this,
761
        _processManager,
762 763 764 765 766
        includePastLogs: true,
      );
    } else {
      return _logReader ??= await AdbLogReader.createLogReader(
        this,
767
        _processManager,
768 769
      );
    }
770
  }
771

772
  @override
773
  DevicePortForwarder get portForwarder => _portForwarder ??= AndroidDevicePortForwarder(
774 775
    processManager: _processManager,
    logger: _logger,
776
    deviceId: id,
777
    adbPath: _androidSdk.adbPath,
778
  );
779

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

782
  /// Return the most recent timestamp in the Android log or [null] if there is
783
  /// no available timestamp. The format can be passed to logcat's -T option.
784 785 786
  @visibleForTesting
  Future<String> lastLogcatTimestamp() async {
    RunResult output;
787
    try {
788
      output = await runAdbCheckedAsync(<String>[
789
        'shell', '-x', 'logcat', '-v', 'time', '-t', '1'
790
      ]);
791
    } on Exception catch (error) {
792
      _logger.printError('Failed to extract the most recent timestamp from the Android log: $error.');
793 794
      return null;
    }
795
    final Match timeMatch = _timeRegExp.firstMatch(output.stdout);
796
    return timeMatch?.group(0);
797 798
  }

799
  @override
800 801
  bool isSupported() => true;

Devon Carew's avatar
Devon Carew committed
802 803 804 805
  @override
  bool get supportsScreenshot => true;

  @override
806
  Future<void> takeScreenshot(File outputFile) async {
Devon Carew's avatar
Devon Carew committed
807
    const String remotePath = '/data/local/tmp/flutter_screenshot.png';
808
    await runAdbCheckedAsync(<String>['shell', 'screencap', '-p', remotePath]);
809
    await _processUtils.run(
810 811 812
      adbCommandForDevice(<String>['pull', remotePath, outputFile.path]),
      throwOnError: true,
    );
813
    await runAdbCheckedAsync(<String>['shell', 'rm', remotePath]);
Devon Carew's avatar
Devon Carew committed
814
  }
815 816 817 818 819

  @override
  bool isSupportedForProject(FlutterProject flutterProject) {
    return flutterProject.android.existsSync();
  }
820 821 822 823

  @override
  Future<void> dispose() async {
    _logReader?._stop();
824
    _pastLogReader?._stop();
825 826
    await _portForwarder?.dispose();
  }
827
}
828

829
Map<String, String> parseAdbDeviceProperties(String str) {
830
  final Map<String, String> properties = <String, String>{};
831
  final RegExp propertyExp = RegExp(r'\[(.*?)\]: \[(.*?)\]');
832
  for (final Match match in propertyExp.allMatches(str)) {
833
    properties[match.group(1)] = match.group(2);
834
  }
835 836 837
  return properties;
}

838 839 840 841 842 843
/// Process the dumpsys info formatted in a table-like structure.
///
/// Currently this only pulls information from the  "App Summary" subsection.
///
/// Example output:
///
844
/// ```
845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895
/// Applications Memory Usage (in Kilobytes):
/// Uptime: 441088659 Realtime: 521464097
///
/// ** MEMINFO in pid 16141 [io.flutter.demo.gallery] **
///                    Pss  Private  Private  SwapPss     Heap     Heap     Heap
///                  Total    Dirty    Clean    Dirty     Size    Alloc     Free
///                 ------   ------   ------   ------   ------   ------   ------
///   Native Heap     8648     8620        0       16    20480    12403     8076
///   Dalvik Heap      547      424       40       18     2628     1092     1536
///  Dalvik Other      464      464        0        0
///         Stack      496      496        0        0
///        Ashmem        2        0        0        0
///       Gfx dev      212      204        0        0
///     Other dev       48        0       48        0
///      .so mmap    10770      708     9372       25
///     .apk mmap      240        0        0        0
///     .ttf mmap       35        0       32        0
///     .dex mmap     2205        4     1172        0
///     .oat mmap       64        0        0        0
///     .art mmap     4228     3848       24        2
///    Other mmap    20713        4    20704        0
///     GL mtrack     2380     2380        0        0
///       Unknown    43971    43968        0        1
///         TOTAL    95085    61120    31392       62    23108    13495     9612
///
///  App Summary
///                        Pss(KB)
///                         ------
///            Java Heap:     4296
///          Native Heap:     8620
///                 Code:    11288
///                Stack:      496
///             Graphics:     2584
///        Private Other:    65228
///               System:     2573
///
///                TOTAL:    95085       TOTAL SWAP PSS:       62
///
///  Objects
///                Views:        9         ViewRootImpl:        1
///          AppContexts:        3           Activities:        1
///              Assets:        4        AssetManagers:        3
///        Local Binders:       10        Proxy Binders:       18
///        Parcel memory:        6         Parcel count:       24
///     Death Recipients:        0      OpenSSL Sockets:        0
///             WebViews:        0
///
///  SQL
///          MEMORY_USED:        0
///   PAGECACHE_OVERFLOW:        0          MALLOC_SIZE:        0
/// ...
896
/// ```
897 898 899 900 901
///
/// For more information, see https://developer.android.com/studio/command-line/dumpsys.
@visibleForTesting
AndroidMemoryInfo parseMeminfoDump(String input) {
  final AndroidMemoryInfo androidMemoryInfo = AndroidMemoryInfo();
902 903 904 905 906 907 908 909 910

  final List<String> lines = input.split('\n');

  final String timelineData = lines.firstWhere((String line) =>
    line.startsWith('${AndroidMemoryInfo._kUpTimeKey}: '));
  final List<String> times = timelineData.trim().split('${AndroidMemoryInfo._kRealTimeKey}:');
  androidMemoryInfo.realTime = int.tryParse(times.last.trim()) ?? 0;

  lines
911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946
    .skipWhile((String line) => !line.contains('App Summary'))
    .takeWhile((String line) => !line.contains('TOTAL'))
    .where((String line) => line.contains(':'))
    .forEach((String line) {
      final List<String> sections = line.trim().split(':');
      final String key = sections.first.trim();
      final int value = int.tryParse(sections.last.trim()) ?? 0;
      switch (key) {
        case AndroidMemoryInfo._kJavaHeapKey:
          androidMemoryInfo.javaHeap = value;
          break;
        case AndroidMemoryInfo._kNativeHeapKey:
          androidMemoryInfo.nativeHeap = value;
          break;
        case AndroidMemoryInfo._kCodeKey:
          androidMemoryInfo.code = value;
          break;
        case AndroidMemoryInfo._kStackKey:
          androidMemoryInfo.stack = value;
          break;
        case AndroidMemoryInfo._kGraphicsKey:
          androidMemoryInfo.graphics = value;
          break;
        case AndroidMemoryInfo._kPrivateOtherKey:
          androidMemoryInfo.privateOther = value;
          break;
        case AndroidMemoryInfo._kSystemKey:
          androidMemoryInfo.system = value;
          break;
      }
  });
  return androidMemoryInfo;
}

/// Android specific implementation of memory info.
class AndroidMemoryInfo extends MemoryInfo {
947 948
  static const String _kUpTimeKey = 'Uptime';
  static const String _kRealTimeKey = 'Realtime';
949 950 951 952 953 954 955 956 957
  static const String _kJavaHeapKey = 'Java Heap';
  static const String _kNativeHeapKey = 'Native Heap';
  static const String _kCodeKey = 'Code';
  static const String _kStackKey = 'Stack';
  static const String _kGraphicsKey = 'Graphics';
  static const String _kPrivateOtherKey = 'Private Other';
  static const String _kSystemKey = 'System';
  static const String _kTotalKey = 'Total';

958 959 960 961
  // Realtime is time since the system was booted includes deep sleep. Clock
  // is monotonic, and ticks even when the CPU is in power saving modes.
  int realTime = 0;

962 963 964 965 966 967 968 969 970 971 972 973 974
  // Each measurement has KB as a unit.
  int javaHeap = 0;
  int nativeHeap = 0;
  int code = 0;
  int stack = 0;
  int graphics = 0;
  int privateOther = 0;
  int system = 0;

  @override
  Map<String, Object> toJson() {
    return <String, Object>{
      'platform': 'Android',
975
      _kRealTimeKey: realTime,
976 977 978 979 980 981 982 983 984 985 986 987
      _kJavaHeapKey: javaHeap,
      _kNativeHeapKey: nativeHeap,
      _kCodeKey: code,
      _kStackKey: stack,
      _kGraphicsKey: graphics,
      _kPrivateOtherKey: privateOther,
      _kSystemKey: system,
      _kTotalKey: javaHeap + nativeHeap + code + stack + graphics + privateOther + system,
    };
  }
}

988
/// A log reader that logs from `adb logcat`.
989 990
class AdbLogReader extends DeviceLogReader {
  AdbLogReader._(this._adbProcess, this.name)  {
991
    _linesController = StreamController<String>.broadcast(
Devon Carew's avatar
Devon Carew committed
992
      onListen: _start,
993
      onCancel: _stop,
Devon Carew's avatar
Devon Carew committed
994 995
    );
  }
Devon Carew's avatar
Devon Carew committed
996

997 998
  @visibleForTesting
  factory AdbLogReader.test(Process adbProcess, String name) = AdbLogReader._;
999

1000 1001 1002
  /// Create a new [AdbLogReader] from an [AndroidDevice] instance.
  static Future<AdbLogReader> createLogReader(
    AndroidDevice device,
1003 1004 1005
    ProcessManager processManager, {
    bool includePastLogs = false,
  }) async {
1006 1007
    // logcat -T is not supported on Android releases before Lollipop.
    const int kLollipopVersionCode = 21;
1008 1009 1010 1011
    final int apiVersion = (String v) {
      // If the API version string isn't found, conservatively assume that the
      // version is less recent than the one we're looking for.
      return v == null ? kLollipopVersionCode - 1 : int.tryParse(v);
1012 1013 1014
    }(await device.apiVersion);

    // Start the adb logcat process and filter the most recent logs since `lastTimestamp`.
1015 1016
    // Some devices (notably LG) will only output logcat via shell
    // https://github.com/flutter/flutter/issues/51853
1017
    final List<String> args = <String>[
1018 1019
      'shell',
      '-x',
1020 1021 1022 1023
      'logcat',
      '-v',
      'time',
    ];
1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036

    // If past logs are included then filter for 'flutter' logs only.
    if (includePastLogs) {
      args.addAll(<String>['-s', 'flutter']);
    } else if (apiVersion != null && apiVersion >= kLollipopVersionCode) {
      // Otherwise, filter for logs appearing past the present.
      // '-T 0` means the timestamp of the logcat command invocation.
      final String lastLogcatTimestamp = await device.lastLogcatTimestamp();
      args.addAll(<String>[
        '-T',
        if (lastLogcatTimestamp != null) '\'$lastLogcatTimestamp\'' else '0',
      ]);
    }
1037 1038 1039
    final Process process = await processManager.start(device.adbCommandForDevice(args));
    return AdbLogReader._(process, device.name);
  }
1040

1041
  final Process _adbProcess;
1042

1043 1044 1045 1046 1047 1048 1049 1050 1051
  @override
  final String name;

  StreamController<String> _linesController;

  @override
  Stream<String> get logLines => _linesController.stream;

  void _start() {
1052 1053 1054
    // We expect logcat streams to occasionally contain invalid utf-8,
    // see: https://github.com/flutter/flutter/pull/8864.
    const Utf8Decoder decoder = Utf8Decoder(reportErrors: false);
1055 1056 1057 1058 1059 1060 1061
    _adbProcess.stdout.transform<String>(decoder)
      .transform<String>(const LineSplitter())
      .listen(_onLine);
    _adbProcess.stderr.transform<String>(decoder)
      .transform<String>(const LineSplitter())
      .listen(_onLine);
    unawaited(_adbProcess.exitCode.whenComplete(() {
1062 1063 1064 1065
      if (_linesController.hasListener) {
        _linesController.close();
      }
    }));
1066 1067
  }

1068
  // 'W/ActivityManager(pid): '
1069
  static final RegExp _logFormat = RegExp(r'^[VDIWEF]\/.*?\(\s*(\d+)\):\s');
1070

1071
  static final List<RegExp> _allowedTags = <RegExp>[
1072 1073 1074
    RegExp(r'^[VDIWEF]\/flutter[^:]*:\s+', caseSensitive: false),
    RegExp(r'^[IE]\/DartVM[^:]*:\s+'),
    RegExp(r'^[WEF]\/AndroidRuntime:\s+'),
1075
    RegExp(r'^[WEF]\/AndroidRuntime\([0-9]+\):\s+'),
1076 1077
    RegExp(r'^[WEF]\/ActivityManager:\s+.*(\bflutter\b|\bdomokit\b|\bsky\b)'),
    RegExp(r'^[WEF]\/System\.err:\s+'),
1078
    RegExp(r'^[F]\/[\S^:]+:\s+'),
1079 1080
  ];

1081
  // 'F/libc(pid): Fatal signal 11'
1082
  static final RegExp _fatalLog = RegExp(r'^F\/libc\s*\(\s*\d+\):\sFatal signal (\d+)');
1083 1084

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

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

1090 1091 1092
  // we default to true in case none of the log lines match
  bool _acceptedLastLine = true;

1093 1094 1095 1096 1097
  // 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;

1098 1099 1100
  // 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): ....
1101
  void _onLine(String line) {
1102 1103 1104 1105 1106
    // This line might be processed after the subscription is closed but before
    // adb stops streaming logs.
    if (_linesController.isClosed) {
      return;
    }
1107
    final Match timeMatch = AndroidDevice._timeRegExp.firstMatch(line);
1108
    if (timeMatch == null || line.length == timeMatch.end) {
1109
      _acceptedLastLine = false;
1110 1111 1112 1113
      return;
    }
    // Chop off the time.
    line = line.substring(timeMatch.end + 1);
1114 1115 1116
    final Match logMatch = _logFormat.firstMatch(line);
    if (logMatch != null) {
      bool acceptLine = false;
1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134

      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) {
1135
        acceptLine = true;
1136 1137 1138 1139 1140

        if (_fatalLog.hasMatch(line)) {
          // Hit fatal signal, app is now crashing
          _fatalCrash = true;
        }
1141 1142
      } else {
        // Filter on approved names and levels.
1143
        acceptLine = _allowedTags.any((RegExp re) => re.hasMatch(line));
1144
      }
1145

1146 1147 1148 1149
      if (acceptLine) {
        _acceptedLastLine = true;
        _linesController.add(line);
        return;
1150
      }
1151 1152
      _acceptedLastLine = false;
    } else if (line == '--------- beginning of system' ||
1153
               line == '--------- beginning of main') {
1154 1155
      // hide the ugly adb logcat log boundaries at the start
      _acceptedLastLine = false;
1156
    } else {
1157 1158 1159
      // 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) {
1160
        _linesController.add(line);
1161 1162
        return;
      }
1163
    }
Devon Carew's avatar
Devon Carew committed
1164 1165
  }

Devon Carew's avatar
Devon Carew committed
1166
  void _stop() {
1167 1168
    _linesController.close();
    _adbProcess?.kill();
Devon Carew's avatar
Devon Carew committed
1169
  }
1170 1171 1172 1173 1174

  @override
  void dispose() {
    _stop();
  }
Devon Carew's avatar
Devon Carew committed
1175
}
1176

1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192
/// A [DevicePortForwarder] implemented for Android devices that uses adb.
class AndroidDevicePortForwarder extends DevicePortForwarder {
  AndroidDevicePortForwarder({
    @required ProcessManager processManager,
    @required Logger logger,
    @required String deviceId,
    @required String adbPath,
  }) : _deviceId = deviceId,
       _adbPath = adbPath,
       _logger = logger,
       _processUtils = ProcessUtils(logger: logger, processManager: processManager);

  final String _deviceId;
  final String _adbPath;
  final Logger _logger;
  final ProcessUtils _processUtils;
1193 1194

  static int _extractPort(String portString) {
1195
    return int.tryParse(portString.trim());
1196 1197
  }

1198
  @override
1199 1200 1201
  List<ForwardedPort> get forwardedPorts {
    final List<ForwardedPort> ports = <ForwardedPort>[];

1202 1203
    String stdout;
    try {
1204 1205 1206 1207 1208 1209 1210 1211
      stdout = _processUtils.runSync(
        <String>[
          _adbPath,
          '-s',
          _deviceId,
          'forward',
          '--list',
        ],
1212 1213 1214
        throwOnError: true,
      ).stdout.trim();
    } on ProcessException catch (error) {
1215
      _logger.printError('Failed to list forwarded ports: $error.');
1216 1217
      return ports;
    }
1218

1219
    final List<String> lines = LineSplitter.split(stdout).toList();
1220
    for (final String line in lines) {
1221
      if (!line.startsWith(_deviceId)) {
1222 1223 1224
        continue;
      }
      final List<String> splitLine = line.split('tcp:');
1225

1226 1227 1228 1229
      // Sanity check splitLine.
      if (splitLine.length != 3) {
        continue;
      }
1230

1231 1232 1233
      // Attempt to extract ports.
      final int hostPort = _extractPort(splitLine[1]);
      final int devicePort = _extractPort(splitLine[2]);
1234

1235 1236 1237
      // Failed, skip.
      if (hostPort == null || devicePort == null) {
        continue;
1238
      }
1239 1240

      ports.add(ForwardedPort(hostPort, devicePort));
1241 1242 1243 1244 1245
    }

    return ports;
  }

1246
  @override
1247
  Future<int> forward(int devicePort, { int hostPort }) async {
1248
    hostPort ??= 0;
1249 1250 1251 1252 1253 1254 1255 1256 1257
    final RunResult process = await _processUtils.run(
      <String>[
        _adbPath,
        '-s',
        _deviceId,
        'forward',
        'tcp:$hostPort',
        'tcp:$devicePort',
      ],
1258 1259
      throwOnError: true,
    );
1260

1261
    if (process.stderr.isNotEmpty) {
1262
      process.throwException('adb returned error:\n${process.stderr}');
1263
    }
1264 1265

    if (process.exitCode != 0) {
1266
      if (process.stdout.isNotEmpty) {
1267
        process.throwException('adb returned error:\n${process.stdout}');
1268
      }
1269 1270 1271 1272
      process.throwException('adb failed without a message');
    }

    if (hostPort == 0) {
1273
      if (process.stdout.isEmpty) {
1274
        process.throwException('adb did not report forwarded port');
1275 1276 1277 1278 1279
      }
      hostPort = int.tryParse(process.stdout);
      if (hostPort == null) {
        process.throwException('adb returned invalid port number:\n${process.stdout}');
      }
1280
    } else {
1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293
      // 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.
1294
      if (process.stdout.isNotEmpty && process.stdout.trim() != '$hostPort') {
1295
        process.throwException('adb returned error:\n${process.stdout}');
1296
      }
1297 1298
    }

1299 1300 1301
    return hostPort;
  }

1302
  @override
1303
  Future<void> unforward(ForwardedPort forwardedPort) async {
1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314
    final String tcpLine = 'tcp:${forwardedPort.hostPort}';
    final RunResult runResult = await _processUtils.run(
      <String>[
        _adbPath,
        '-s',
        _deviceId,
        'forward',
        '--remove',
        tcpLine,
      ],
      throwOnError: false,
1315
    );
1316 1317 1318 1319 1320 1321 1322
    // The port may have already been unforwarded, for example if there
    // are multiple attach process already connected.
    if (runResult.exitCode == 0 || runResult
      .stderr.contains("listener '$tcpLine' not found")) {
      return;
    }
    runResult.throwException('Process exited abnormally:\n$runResult');
1323
  }
1324 1325 1326

  @override
  Future<void> dispose() async {
1327
    for (final ForwardedPort port in forwardedPorts) {
1328 1329 1330
      await unforward(port);
    }
  }
1331
}
1332 1333 1334 1335 1336 1337 1338

// In platform tools 29.0.0 adb.exe seems to be ending with this heap
// corruption error code on seemingly successful termination. Ignore
// this error on windows.
bool _allowHeapCorruptionOnWindows(int exitCode, Platform platform) {
  return exitCode == -1073740940 && platform.isWindows;
}