android_device.dart 44.2 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
      if (debuggingOptions.traceAllowlist != null)
635 636 637
        ...<String>['--es', 'trace-allowlist', debuggingOptions.traceAllowlist],
      if (debuggingOptions.traceSkiaAllowlist != null)
        ...<String>['--es', 'trace-skia-allowlist', debuggingOptions.traceSkiaAllowlist],
638 639
      if (debuggingOptions.traceSystrace)
        ...<String>['--ez', 'trace-systrace', 'true'],
640 641
      if (debuggingOptions.endlessTraceBuffer)
        ...<String>['--ez', 'endless-trace-buffer', 'true'],
642 643
      if (debuggingOptions.dumpSkpOnShaderCompilation)
        ...<String>['--ez', 'dump-skp-on-shader-compilation', 'true'],
644 645
      if (debuggingOptions.cacheSkSL)
      ...<String>['--ez', 'cache-sksl', 'true'],
646 647
      if (debuggingOptions.purgePersistentCache)
        ...<String>['--ez', 'purge-persistent-cache', 'true'],
648 649 650 651
      if (debuggingOptions.debuggingEnabled) ...<String>[
        if (debuggingOptions.buildInfo.isDebug) ...<String>[
          ...<String>['--ez', 'enable-checked-mode', 'true'],
          ...<String>['--ez', 'verify-entry-points', 'true'],
652
        ],
653 654 655 656
        if (debuggingOptions.startPaused)
          ...<String>['--ez', 'start-paused', 'true'],
        if (debuggingOptions.disableServiceAuthCodes)
          ...<String>['--ez', 'disable-service-auth-codes', 'true'],
657 658
        if (dartVmFlags.isNotEmpty)
          ...<String>['--es', 'dart-flags', dartVmFlags],
659 660 661 662
        if (debuggingOptions.useTestFonts)
          ...<String>['--ez', 'use-test-fonts', 'true'],
        if (debuggingOptions.verboseSystemLogs)
          ...<String>['--ez', 'verbose-logging', 'true'],
663 664
        if (userIdentifier != null)
          ...<String>['--user', userIdentifier],
665
      ],
666
      package.launchActivity,
667 668
    ];
    final String result = (await runAdbCheckedAsync(cmd)).stdout;
669 670
    // This invocation returns 0 even when it fails.
    if (result.contains('Error: ')) {
671
      _logger.printError(result.trim(), wrap: false);
672
      return LaunchResult.failed();
673
    }
674

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

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

704
  @override
705 706 707 708
  bool get supportsHotReload => true;

  @override
  bool get supportsHotRestart => true;
709

710 711 712
  @override
  bool get supportsFastStart => true;

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

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

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

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

754
  @override
755 756 757 758 759 760 761 762
  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,
763
        _processManager,
764 765 766 767 768
        includePastLogs: true,
      );
    } else {
      return _logReader ??= await AdbLogReader.createLogReader(
        this,
769
        _processManager,
770 771
      );
    }
772
  }
773

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

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

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

801
  @override
802 803
  bool isSupported() => true;

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

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

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

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

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

840 841 842 843 844 845
/// Process the dumpsys info formatted in a table-like structure.
///
/// Currently this only pulls information from the  "App Summary" subsection.
///
/// Example output:
///
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 896 897
/// 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
/// ...
898
/// ```
899 900 901 902 903
///
/// For more information, see https://developer.android.com/studio/command-line/dumpsys.
@visibleForTesting
AndroidMemoryInfo parseMeminfoDump(String input) {
  final AndroidMemoryInfo androidMemoryInfo = AndroidMemoryInfo();
904 905 906 907 908 909 910 911 912

  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
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 947 948
    .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 {
949 950
  static const String _kUpTimeKey = 'Uptime';
  static const String _kRealTimeKey = 'Realtime';
951 952 953 954 955 956 957 958 959
  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';

960 961 962 963
  // 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;

964 965 966 967 968 969 970 971 972 973 974 975 976
  // 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',
977
      _kRealTimeKey: realTime,
978 979 980 981 982 983 984 985 986 987 988 989
      _kJavaHeapKey: javaHeap,
      _kNativeHeapKey: nativeHeap,
      _kCodeKey: code,
      _kStackKey: stack,
      _kGraphicsKey: graphics,
      _kPrivateOtherKey: privateOther,
      _kSystemKey: system,
      _kTotalKey: javaHeap + nativeHeap + code + stack + graphics + privateOther + system,
    };
  }
}

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

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

1002 1003 1004
  /// Create a new [AdbLogReader] from an [AndroidDevice] instance.
  static Future<AdbLogReader> createLogReader(
    AndroidDevice device,
1005 1006 1007
    ProcessManager processManager, {
    bool includePastLogs = false,
  }) async {
1008 1009
    // logcat -T is not supported on Android releases before Lollipop.
    const int kLollipopVersionCode = 21;
1010 1011 1012 1013
    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);
1014 1015 1016
    }(await device.apiVersion);

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

    // 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',
1036
        if (lastLogcatTimestamp != null) "'$lastLogcatTimestamp'" else '0',
1037 1038
      ]);
    }
1039 1040 1041
    final Process process = await processManager.start(device.adbCommandForDevice(args));
    return AdbLogReader._(process, device.name);
  }
1042

1043
  final Process _adbProcess;
1044

1045 1046 1047 1048 1049 1050 1051 1052 1053
  @override
  final String name;

  StreamController<String> _linesController;

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

  void _start() {
1054 1055 1056
    // We expect logcat streams to occasionally contain invalid utf-8,
    // see: https://github.com/flutter/flutter/pull/8864.
    const Utf8Decoder decoder = Utf8Decoder(reportErrors: false);
1057 1058 1059 1060 1061 1062 1063
    _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(() {
1064 1065 1066 1067
      if (_linesController.hasListener) {
        _linesController.close();
      }
    }));
1068 1069
  }

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

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

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

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

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

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

1095 1096 1097 1098 1099
  // 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;

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

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

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

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

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

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

1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194
/// 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;
1195 1196

  static int _extractPort(String portString) {
1197
    return int.tryParse(portString.trim());
1198 1199
  }

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

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

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

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

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

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

      ports.add(ForwardedPort(hostPort, devicePort));
1243 1244 1245 1246 1247
    }

    return ports;
  }

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

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

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

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

1301 1302 1303
    return hostPort;
  }

1304
  @override
1305
  Future<void> unforward(ForwardedPort forwardedPort) async {
1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316
    final String tcpLine = 'tcp:${forwardedPort.hostPort}';
    final RunResult runResult = await _processUtils.run(
      <String>[
        _adbPath,
        '-s',
        _deviceId,
        'forward',
        '--remove',
        tcpLine,
      ],
      throwOnError: false,
1317
    );
1318
    if (runResult.exitCode == 0) {
1319 1320
      return;
    }
1321
    _logger.printError('Failed to unforward port: $runResult');
1322
  }
1323 1324 1325

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

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