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

import 'dart:async';

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

10
import '../application_package.dart';
11
import '../base/common.dart' show throwToolExit;
12
import '../base/file_system.dart';
13
import '../base/io.dart';
14
import '../base/logger.dart';
15
import '../base/platform.dart';
16
import '../base/process.dart';
17
import '../build_info.dart';
18
import '../convert.dart';
19
import '../device.dart';
20
import '../device_port_forwarder.dart';
21
import '../project.dart';
22
import '../protocol_discovery.dart';
23

24
import 'android.dart';
25
import 'android_builder.dart';
26
import 'android_console.dart';
27
import 'android_sdk.dart';
28
import 'application_package.dart';
29

30 31
/// Whether the [AndroidDevice] is believed to be a physical device or an emulator.
enum HardwareType { emulator, physical }
32 33

/// Map to help our `isLocalEmulator` detection.
34 35 36 37 38 39 40 41 42 43 44 45 46 47
///
/// 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,
48 49
};

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

89
  final String? productID;
90
  final String modelID;
91
  final String? deviceCodeName;
92

93 94
  late final Future<Map<String, String>> _properties = () async {
    Map<String, String> properties = <String, String>{};
95

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

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

119 120
  Future<String?> _getProperty(String name) async {
    return (await _properties)[name];
121
  }
122

123
  @override
124 125 126 127 128 129
  late final Future<bool> isLocalEmulator = () async {
    final String? hardware = await _getProperty('ro.hardware');
    _logger.printTrace('ro.hardware = $hardware');
    if (kKnownHardware.containsKey(hardware)) {
      // Look for known hardware models.
      return kKnownHardware[hardware] == HardwareType.emulator;
130
    }
131 132 133 134 135
    // Fall back to a best-effort heuristic-based approach.
    final String? characteristics = await _getProperty('ro.build.characteristics');
    _logger.printTrace('ro.build.characteristics = $characteristics');
    return characteristics != null && characteristics.contains('emulator');
  }();
136

137 138 139 140 141 142 143
  /// 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
144
  Future<String?> get emulatorId async {
145
    if (!(await isLocalEmulator)) {
146
      return null;
147
    }
148 149 150 151 152

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

153
    final Match? portMatch = emulatorPortRegex.firstMatch(id);
154 155 156 157 158
    if (portMatch == null || portMatch.groupCount < 1) {
      return null;
    }

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

    try {
163
      final Socket socket = await _androidConsoleSocketFactory(host, port);
164 165 166 167 168
      final AndroidConsole console = AndroidConsole(socket);

      try {
        await console
            .connect()
169
            .timeout(const Duration(seconds: 2),
170 171 172 173
                onTimeout: () => throw TimeoutException('Connection timed out'));

        return await console
            .getAvdName()
174
            .timeout(const Duration(seconds: 2),
175 176 177 178
                onTimeout: () => throw TimeoutException('"avd name" timed out'));
      } finally {
        console.destroy();
      }
179
    } on Exception catch (e) {
180
      _logger.printTrace('Failed to fetch avd name for emulator at $host:$port: $e');
181 182 183 184 185 186
      // If we fail to connect to the device, we should not fail so just return
      // an empty name. This data is best-effort.
      return null;
    }
  }

187
  @override
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
  late final Future<TargetPlatform> targetPlatform = () async {
    // http://developer.android.com/ndk/guides/abis.html (x86, armeabi-v7a, ...)
    switch (await _getProperty('ro.product.cpu.abi')) {
      case 'arm64-v8a':
        // 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')) {
          return TargetPlatform.android_arm64;
        } else {
          return TargetPlatform.android_arm;
        }
      case 'x86_64':
        return TargetPlatform.android_x64;
      case 'x86':
        return TargetPlatform.android_x86;
      default:
        return TargetPlatform.android_arm;
208
    }
209
  }();
210

211 212 213 214 215 216 217 218 219
  @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;
220 221 222 223 224 225 226 227 228 229
      case TargetPlatform.android:
      case TargetPlatform.darwin:
      case TargetPlatform.fuchsia_arm64:
      case TargetPlatform.fuchsia_x64:
      case TargetPlatform.ios:
      case TargetPlatform.linux_arm64:
      case TargetPlatform.linux_x64:
      case TargetPlatform.tester:
      case TargetPlatform.web_javascript:
      case TargetPlatform.windows_x64:
230 231 232 233
        throw UnsupportedError('Invalid target platform for Android');
    }
  }

234
  @override
235
  Future<String> get sdkNameAndVersion async => 'Android ${await _sdkVersion} (API ${await apiVersion})';
236

237
  Future<String?> get _sdkVersion => _getProperty('ro.build.version.release');
238

239
  @visibleForTesting
240
  Future<String?> get apiVersion => _getProperty('ro.build.version.sdk');
241

242 243
  AdbLogReader? _logReader;
  AdbLogReader? _pastLogReader;
244

245
  List<String> adbCommandForDevice(List<String> args) {
246
    return <String>[_androidSdk.adbPath!, '-s', id, ...args];
247 248
  }

249
  Future<RunResult> runAdbCheckedAsync(
250
    List<String> params, {
251
    String? workingDirectory,
252 253
    bool allowReentrantFlutter = false,
  }) async {
254
    return _processUtils.run(
255 256 257 258
      adbCommandForDevice(params),
      throwOnError: true,
      workingDirectory: workingDirectory,
      allowReentrantFlutter: allowReentrantFlutter,
259
      allowedFailures: (int value) => _allowHeapCorruptionOnWindows(value, _platform),
260
    );
261 262
  }

263 264
  bool _isValidAdbVersion(String adbVersion) {
    // Sample output: 'Android Debug Bridge version 1.0.31'
265
    final Match? versionFields = RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
266
    if (versionFields != null) {
267 268 269
      final int majorVersion = int.parse(versionFields[1]!);
      final int minorVersion = int.parse(versionFields[2]!);
      final int patchVersion = int.parse(versionFields[3]!);
270 271 272 273 274 275
      if (majorVersion > 1) {
        return true;
      }
      if (majorVersion == 1 && minorVersion > 0) {
        return true;
      }
276
      if (majorVersion == 1 && minorVersion == 0 && patchVersion >= 39) {
277 278 279 280
        return true;
      }
      return false;
    }
281
    _logger.printError(
282 283 284 285
        'Unrecognized adb version string $adbVersion. Skipping version check.');
    return true;
  }

286
  Future<bool> _checkForSupportedAdbVersion() async {
287 288
    final String? adbPath = _androidSdk.adbPath;
    if (adbPath == null) {
289
      return false;
290
    }
291

292
    try {
293
      final RunResult adbVersion = await _processUtils.run(
294
        <String>[adbPath, 'version'],
295 296 297
        throwOnError: true,
      );
      if (_isValidAdbVersion(adbVersion.stdout)) {
298
        return true;
299
      }
300
      _logger.printError('The ADB at "$adbPath" is too old; please install version 1.0.39 or later.');
301
    } on Exception catch (error, trace) {
302
      _logger.printError('Error running ADB: $error', stackTrace: trace);
303
    }
304

305 306 307
    return false;
  }

308
  Future<bool> _checkForSupportedAndroidVersion() async {
309 310 311 312
    final String? adbPath = _androidSdk.adbPath;
    if (adbPath == null) {
      return false;
    }
313 314 315 316 317
    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 *
318
      await _processUtils.run(
319
        <String>[adbPath, 'start-server'],
320 321
        throwOnError: true,
      );
322

323 324
      // This has been reported to return null on some devices. In this case,
      // assume the lowest supported API to still allow Flutter to run.
325
      // Sample output: '22'
326 327
      final String sdkVersion = await _getProperty('ro.build.version.sdk')
        ?? minApiLevel.toString();
328

329
      final int? sdkVersionParsed = int.tryParse(sdkVersion);
330
      if (sdkVersionParsed == null) {
331
        _logger.printError('Unexpected response from getprop: "$sdkVersion"');
332 333
        return false;
      }
334

335
      if (sdkVersionParsed < minApiLevel) {
336
        _logger.printError(
337 338 339 340
          'The Android version ($sdkVersion) on the target device is too old. Please '
          'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.');
        return false;
      }
341

342
      return true;
343
    } on Exception catch (e, stacktrace) {
344 345
      _logger.printError('Unexpected failure from adb: $e');
      _logger.printError('Stacktrace: $stacktrace');
346
      return false;
347 348 349
    }
  }

350 351
  String _getDeviceSha1Path(AndroidApk apk) {
    return '/data/local/tmp/sky.${apk.id}.sha1';
352 353
  }

354
  Future<String> _getDeviceApkSha1(AndroidApk apk) async {
355
    final RunResult result = await _processUtils.run(
356
      adbCommandForDevice(<String>['shell', 'cat', _getDeviceSha1Path(apk)]));
357
    return result.stdout;
358 359
  }

360
  String _getSourceSha1(AndroidApk apk) {
361
    final File shaFile = _fileSystem.file('${apk.applicationPackage.path}.sha1');
Devon Carew's avatar
Devon Carew committed
362
    return shaFile.existsSync() ? shaFile.readAsStringSync() : '';
363 364
  }

365
  @override
366 367 368
  String get name => modelID;

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

391
  @override
392
  Future<bool> isLatestBuildInstalled(AndroidApk app) async {
393
    final String installedSha1 = await _getDeviceApkSha1(app);
394 395 396
    return installedSha1.isNotEmpty && installedSha1 == _getSourceSha1(app);
  }

397
  @override
398 399
  Future<bool> installApp(
    AndroidApk app, {
400
    String? userIdentifier,
401
  }) async {
402
    if (!await _adbIsValid) {
403 404 405 406 407 408 409 410 411 412 413 414 415
      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) {
416 417
      return false;
    }
418 419 420 421 422 423 424 425 426 427 428
    _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;
  }
429

430 431
  Future<bool> _installApp(
    AndroidApk app, {
432
    String? userIdentifier,
433
  }) async {
434 435
    if (!app.applicationPackage.existsSync()) {
      _logger.printError('"${_fileSystem.path.relative(app.applicationPackage.path)}" does not exist.');
436
      return false;
437
    }
438

439
    final Status status = _logger.startProgress(
440
      'Installing ${_fileSystem.path.relative(app.applicationPackage.path)}...',
441 442
    );
    final RunResult installResult = await _processUtils.run(
443 444 445 446 447 448
      adbCommandForDevice(<String>[
        'install',
        '-t',
        '-r',
        if (userIdentifier != null)
          ...<String>['--user', userIdentifier],
449
        app.applicationPackage.path,
450
      ]));
Devon Carew's avatar
Devon Carew committed
451
    status.stop();
452 453
    // Some versions of adb exit with exit code 0 even on failure :(
    // Parsing the output to check for failures.
454
    final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true);
455
    final String? failure = failureExp.stringMatch(installResult.stdout);
456
    if (failure != null) {
457
      _logger.printError('Package install error: $failure');
458 459
      return false;
    }
460
    if (installResult.exitCode != 0) {
461 462 463 464 465 466
      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');
      }
467 468
      return false;
    }
469 470 471 472 473
    try {
      await runAdbCheckedAsync(<String>[
        'shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app),
      ]);
    } on ProcessException catch (error) {
474
      _logger.printError('adb shell failed to write the SHA hash: $error.');
475 476
      return false;
    }
477 478 479
    return true;
  }

480
  @override
481 482
  Future<bool> uninstallApp(
    AndroidApk app, {
483
    String? userIdentifier,
484
  }) async {
485
    if (!await _adbIsValid) {
486
      return false;
487
    }
488

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

514
  // Whether the adb and Android versions are aligned.
515 516 517
  late final Future<bool> _adbIsValid = () async {
    return await _checkForSupportedAdbVersion() && await _checkForSupportedAndroidVersion();
  }();
518

519
  AndroidApk? _package;
520

521 522
  @override
  Future<LaunchResult> startApp(
523
    AndroidApk package, {
524 525 526 527
    String? mainPath,
    String? route,
    required DebuggingOptions debuggingOptions,
    Map<String, Object?> platformArgs = const <String, Object>{},
528 529
    bool prebuiltApplication = false,
    bool ipv6 = false,
530
    String? userIdentifier,
531
  }) async {
532
    if (!await _adbIsValid) {
533
      return LaunchResult.failed();
534
    }
535

536
    final TargetPlatform devicePlatform = await targetPlatform;
537 538
    if (devicePlatform == TargetPlatform.android_x86 &&
       !debuggingOptions.buildInfo.isDebug) {
539
      _logger.printError('Profile and release builds are only supported on ARM/x64 targets.');
540
      return LaunchResult.failed();
541 542
    }

543
    AndroidApk? builtPackage = package;
544 545 546 547 548 549 550 551 552 553 554 555 556 557
    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;
558 559 560 561 562 563 564 565 566 567
      case TargetPlatform.android:
      case TargetPlatform.darwin:
      case TargetPlatform.fuchsia_arm64:
      case TargetPlatform.fuchsia_x64:
      case TargetPlatform.ios:
      case TargetPlatform.linux_arm64:
      case TargetPlatform.linux_x64:
      case TargetPlatform.tester:
      case TargetPlatform.web_javascript:
      case TargetPlatform.windows_x64:
568
        _logger.printError('Android platforms are only supported.');
569 570
        return LaunchResult.failed();
    }
571

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

595 596
    _logger.printTrace("Stopping app '${builtPackage.name}' on $name.");
    await stopApp(builtPackage, userIdentifier: userIdentifier);
597

598
    if (!await installApp(builtPackage, userIdentifier: userIdentifier)) {
599
      return LaunchResult.failed();
600
    }
601

602 603
    final bool traceStartup = platformArgs['trace-startup'] as bool? ?? false;
    ProtocolDiscovery? observatoryDiscovery;
604

605
    if (debuggingOptions.debuggingEnabled) {
606
      observatoryDiscovery = ProtocolDiscovery.observatory(
607 608 609 610 611 612 613
        // 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,
        ),
614
        portForwarder: portForwarder,
615
        hostPort: debuggingOptions.hostVmServicePort,
616
        devicePort: debuggingOptions.deviceVmServicePort,
617
        ipv6: ipv6,
618
        logger: _logger,
619
      );
Devon Carew's avatar
Devon Carew committed
620
    }
621

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

684
    _package = builtPackage;
685
    if (!debuggingOptions.debuggingEnabled) {
686
      return LaunchResult.succeeded();
687
    }
Devon Carew's avatar
Devon Carew committed
688

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

713
  @override
714 715 716 717
  bool get supportsHotReload => true;

  @override
  bool get supportsHotRestart => true;
718

719 720 721
  @override
  bool get supportsFastStart => true;

722
  @override
723 724
  Future<bool> stopApp(
    AndroidApk app, {
725
    String? userIdentifier,
726
  }) {
727 728 729
    if (app == null) {
      return Future<bool>.value(false);
    }
730 731 732 733 734 735 736 737
    final List<String> command = adbCommandForDevice(<String>[
      'shell',
      'am',
      'force-stop',
      if (userIdentifier != null)
        ...<String>['--user', userIdentifier],
      app.id,
    ]);
738
    return _processUtils.stream(command).then<bool>(
739
        (int exitCode) => exitCode == 0 || _allowHeapCorruptionOnWindows(exitCode, _platform));
740 741
  }

742 743
  @override
  Future<MemoryInfo> queryMemoryInfo() async {
744 745 746 747 748
    final AndroidApk? package = _package;
    if (package == null) {
      _logger.printError('Android package unknown, skipping dumpsys meminfo.');
      return const MemoryInfo.empty();
    }
749
    final RunResult runResult = await _processUtils.run(adbCommandForDevice(<String>[
750 751 752
      'shell',
      'dumpsys',
      'meminfo',
753
      package.id,
754 755
      '-d',
    ]));
756

757 758 759 760 761 762
    if (runResult.exitCode != 0) {
      return const MemoryInfo.empty();
    }
    return parseMeminfoDump(runResult.stdout);
  }

763
  @override
764
  void clearLogs() {
765
    _processUtils.runSync(adbCommandForDevice(<String>['logcat', '-c']));
766 767
  }

768
  @override
769
  FutureOr<DeviceLogReader> getLogReader({
770
    AndroidApk? app,
771 772 773 774 775 776
    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,
777
        _processManager,
778 779 780 781 782
        includePastLogs: true,
      );
    } else {
      return _logReader ??= await AdbLogReader.createLogReader(
        this,
783
        _processManager,
784 785
      );
    }
786
  }
787

788
  @override
789 790 791 792 793 794 795 796 797 798 799 800
  late final DevicePortForwarder? portForwarder = () {
    final String? adbPath = _androidSdk.adbPath;
    if (adbPath == null) {
      return null;
    }
    return AndroidDevicePortForwarder(
      processManager: _processManager,
      logger: _logger,
      deviceId: id,
      adbPath: adbPath,
    );
  }();
801

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

804
  /// Return the most recent timestamp in the Android log or [null] if there is
805
  /// no available timestamp. The format can be passed to logcat's -T option.
806
  @visibleForTesting
807
  Future<String?> lastLogcatTimestamp() async {
808
    RunResult output;
809
    try {
810
      output = await runAdbCheckedAsync(<String>[
811
        'shell', '-x', 'logcat', '-v', 'time', '-t', '1',
812
      ]);
813
    } on Exception catch (error) {
814
      _logger.printError('Failed to extract the most recent timestamp from the Android log: $error.');
815 816
      return null;
    }
817
    final Match? timeMatch = _timeRegExp.firstMatch(output.stdout);
818
    return timeMatch?.group(0);
819 820
  }

821
  @override
822 823
  bool isSupported() => true;

Devon Carew's avatar
Devon Carew committed
824 825 826 827
  @override
  bool get supportsScreenshot => true;

  @override
828
  Future<void> takeScreenshot(File outputFile) async {
Devon Carew's avatar
Devon Carew committed
829
    const String remotePath = '/data/local/tmp/flutter_screenshot.png';
830
    await runAdbCheckedAsync(<String>['shell', 'screencap', '-p', remotePath]);
831
    await _processUtils.run(
832 833 834
      adbCommandForDevice(<String>['pull', remotePath, outputFile.path]),
      throwOnError: true,
    );
835
    await runAdbCheckedAsync(<String>['shell', 'rm', remotePath]);
Devon Carew's avatar
Devon Carew committed
836
  }
837 838 839 840 841

  @override
  bool isSupportedForProject(FlutterProject flutterProject) {
    return flutterProject.android.existsSync();
  }
842 843 844 845

  @override
  Future<void> dispose() async {
    _logReader?._stop();
846
    _pastLogReader?._stop();
847
  }
848
}
849

850
Map<String, String> parseAdbDeviceProperties(String str) {
851
  final Map<String, String> properties = <String, String>{};
852
  final RegExp propertyExp = RegExp(r'\[(.*?)\]: \[(.*?)\]');
853
  for (final Match match in propertyExp.allMatches(str)) {
854
    properties[match.group(1)!] = match.group(2)!;
855
  }
856 857 858
  return properties;
}

859 860 861 862 863 864
/// Process the dumpsys info formatted in a table-like structure.
///
/// Currently this only pulls information from the  "App Summary" subsection.
///
/// Example output:
///
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 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916
/// 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
/// ...
917
/// ```
918 919 920 921 922
///
/// For more information, see https://developer.android.com/studio/command-line/dumpsys.
@visibleForTesting
AndroidMemoryInfo parseMeminfoDump(String input) {
  final AndroidMemoryInfo androidMemoryInfo = AndroidMemoryInfo();
923 924 925 926 927 928 929 930 931

  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
932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967
    .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 {
968 969
  static const String _kUpTimeKey = 'Uptime';
  static const String _kRealTimeKey = 'Realtime';
970 971 972 973 974 975 976 977 978
  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';

979 980 981 982
  // 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;

983 984 985 986 987 988 989 990 991 992 993 994 995
  // 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',
996
      _kRealTimeKey: realTime,
997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
      _kJavaHeapKey: javaHeap,
      _kNativeHeapKey: nativeHeap,
      _kCodeKey: code,
      _kStackKey: stack,
      _kGraphicsKey: graphics,
      _kPrivateOtherKey: privateOther,
      _kSystemKey: system,
      _kTotalKey: javaHeap + nativeHeap + code + stack + graphics + privateOther + system,
    };
  }
}

1009
/// A log reader that logs from `adb logcat`.
1010
class AdbLogReader extends DeviceLogReader {
1011
  AdbLogReader._(this._adbProcess, this.name);
Devon Carew's avatar
Devon Carew committed
1012

1013 1014
  @visibleForTesting
  factory AdbLogReader.test(Process adbProcess, String name) = AdbLogReader._;
1015

1016 1017 1018
  /// Create a new [AdbLogReader] from an [AndroidDevice] instance.
  static Future<AdbLogReader> createLogReader(
    AndroidDevice device,
1019 1020 1021
    ProcessManager processManager, {
    bool includePastLogs = false,
  }) async {
1022 1023
    // logcat -T is not supported on Android releases before Lollipop.
    const int kLollipopVersionCode = 21;
1024
    final int? apiVersion = (String? v) {
1025 1026 1027
      // 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);
1028 1029 1030
    }(await device.apiVersion);

    // Start the adb logcat process and filter the most recent logs since `lastTimestamp`.
1031 1032
    // Some devices (notably LG) will only output logcat via shell
    // https://github.com/flutter/flutter/issues/51853
1033
    final List<String> args = <String>[
1034 1035
      'shell',
      '-x',
1036 1037 1038 1039
      'logcat',
      '-v',
      'time',
    ];
1040 1041 1042 1043 1044 1045 1046

    // 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.
1047
      final String? lastLogcatTimestamp = await device.lastLogcatTimestamp();
1048 1049
      args.addAll(<String>[
        '-T',
1050
        if (lastLogcatTimestamp != null) "'$lastLogcatTimestamp'" else '0',
1051 1052
      ]);
    }
1053 1054 1055
    final Process process = await processManager.start(device.adbCommandForDevice(args));
    return AdbLogReader._(process, device.name);
  }
1056

1057
  final Process _adbProcess;
1058

1059 1060 1061
  @override
  final String name;

1062 1063 1064 1065
  late final StreamController<String> _linesController = StreamController<String>.broadcast(
    onListen: _start,
    onCancel: _stop,
  );
1066 1067 1068 1069 1070

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

  void _start() {
1071 1072 1073
    // We expect logcat streams to occasionally contain invalid utf-8,
    // see: https://github.com/flutter/flutter/pull/8864.
    const Utf8Decoder decoder = Utf8Decoder(reportErrors: false);
1074 1075 1076 1077 1078 1079 1080
    _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(() {
1081 1082 1083 1084
      if (_linesController.hasListener) {
        _linesController.close();
      }
    }));
1085 1086
  }

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

1090
  static final List<RegExp> _allowedTags = <RegExp>[
1091 1092 1093
    RegExp(r'^[VDIWEF]\/flutter[^:]*:\s+', caseSensitive: false),
    RegExp(r'^[IE]\/DartVM[^:]*:\s+'),
    RegExp(r'^[WEF]\/AndroidRuntime:\s+'),
1094
    RegExp(r'^[WEF]\/AndroidRuntime\([0-9]+\):\s+'),
1095 1096
    RegExp(r'^[WEF]\/ActivityManager:\s+.*(\bflutter\b|\bdomokit\b|\bsky\b)'),
    RegExp(r'^[WEF]\/System\.err:\s+'),
1097
    RegExp(r'^[F]\/[\S^:]+:\s+'),
1098 1099
  ];

1100
  // 'F/libc(pid): Fatal signal 11'
1101
  static final RegExp _fatalLog = RegExp(r'^F\/libc\s*\(\s*\d+\):\sFatal signal (\d+)');
1102 1103

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

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

1109 1110 1111
  // we default to true in case none of the log lines match
  bool _acceptedLastLine = true;

1112 1113 1114 1115 1116
  // 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;

1117 1118 1119
  // 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): ....
1120
  void _onLine(String line) {
1121 1122 1123 1124 1125
    // This line might be processed after the subscription is closed but before
    // adb stops streaming logs.
    if (_linesController.isClosed) {
      return;
    }
1126
    final Match? timeMatch = AndroidDevice._timeRegExp.firstMatch(line);
1127
    if (timeMatch == null || line.length == timeMatch.end) {
1128
      _acceptedLastLine = false;
1129 1130 1131 1132
      return;
    }
    // Chop off the time.
    line = line.substring(timeMatch.end + 1);
1133
    final Match? logMatch = _logFormat.firstMatch(line);
1134 1135
    if (logMatch != null) {
      bool acceptLine = false;
1136 1137 1138 1139 1140

      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

1141
        final Match? fatalMatch = _tombstoneLine.firstMatch(line);
1142 1143 1144 1145

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

1146
          line = fatalMatch[1]!;
1147

1148
          if (_tombstoneTerminator.hasMatch(line)) {
1149 1150 1151 1152
            // Hit crash terminator, stop logging the crash info
            _fatalCrash = false;
          }
        }
1153
      } else if (appPid != null && int.parse(logMatch.group(1)!) == appPid) {
1154
        acceptLine = true;
1155 1156 1157 1158 1159

        if (_fatalLog.hasMatch(line)) {
          // Hit fatal signal, app is now crashing
          _fatalCrash = true;
        }
1160 1161
      } else {
        // Filter on approved names and levels.
1162
        acceptLine = _allowedTags.any((RegExp re) => re.hasMatch(line));
1163
      }
1164

1165 1166 1167 1168
      if (acceptLine) {
        _acceptedLastLine = true;
        _linesController.add(line);
        return;
1169
      }
1170 1171
      _acceptedLastLine = false;
    } else if (line == '--------- beginning of system' ||
1172
               line == '--------- beginning of main') {
1173 1174
      // hide the ugly adb logcat log boundaries at the start
      _acceptedLastLine = false;
1175
    } else {
1176 1177 1178
      // 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) {
1179
        _linesController.add(line);
1180 1181
        return;
      }
1182
    }
Devon Carew's avatar
Devon Carew committed
1183 1184
  }

Devon Carew's avatar
Devon Carew committed
1185
  void _stop() {
1186
    _linesController.close();
1187
    _adbProcess.kill();
Devon Carew's avatar
Devon Carew committed
1188
  }
1189 1190 1191 1192 1193

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

1196 1197 1198
/// A [DevicePortForwarder] implemented for Android devices that uses adb.
class AndroidDevicePortForwarder extends DevicePortForwarder {
  AndroidDevicePortForwarder({
1199 1200 1201 1202
    required ProcessManager processManager,
    required Logger logger,
    required String deviceId,
    required String adbPath,
1203 1204 1205 1206 1207 1208 1209 1210 1211
  }) : _deviceId = deviceId,
       _adbPath = adbPath,
       _logger = logger,
       _processUtils = ProcessUtils(logger: logger, processManager: processManager);

  final String _deviceId;
  final String _adbPath;
  final Logger _logger;
  final ProcessUtils _processUtils;
1212

1213
  static int? _extractPort(String portString) {
1214
    return int.tryParse(portString.trim());
1215 1216
  }

1217
  @override
1218 1219 1220
  List<ForwardedPort> get forwardedPorts {
    final List<ForwardedPort> ports = <ForwardedPort>[];

1221 1222
    String stdout;
    try {
1223 1224 1225 1226 1227 1228 1229 1230
      stdout = _processUtils.runSync(
        <String>[
          _adbPath,
          '-s',
          _deviceId,
          'forward',
          '--list',
        ],
1231 1232 1233
        throwOnError: true,
      ).stdout.trim();
    } on ProcessException catch (error) {
1234
      _logger.printError('Failed to list forwarded ports: $error.');
1235 1236
      return ports;
    }
1237

1238
    final List<String> lines = LineSplitter.split(stdout).toList();
1239
    for (final String line in lines) {
1240
      if (!line.startsWith(_deviceId)) {
1241 1242 1243
        continue;
      }
      final List<String> splitLine = line.split('tcp:');
1244

1245 1246 1247 1248
      // Sanity check splitLine.
      if (splitLine.length != 3) {
        continue;
      }
1249

1250
      // Attempt to extract ports.
1251 1252
      final int? hostPort = _extractPort(splitLine[1]);
      final int? devicePort = _extractPort(splitLine[2]);
1253

1254 1255 1256
      // Failed, skip.
      if (hostPort == null || devicePort == null) {
        continue;
1257
      }
1258 1259

      ports.add(ForwardedPort(hostPort, devicePort));
1260 1261 1262 1263 1264
    }

    return ports;
  }

1265
  @override
1266
  Future<int> forward(int devicePort, { int? hostPort }) async {
1267
    hostPort ??= 0;
1268 1269 1270 1271 1272 1273 1274 1275 1276
    final RunResult process = await _processUtils.run(
      <String>[
        _adbPath,
        '-s',
        _deviceId,
        'forward',
        'tcp:$hostPort',
        'tcp:$devicePort',
      ],
1277 1278
      throwOnError: true,
    );
1279

1280
    if (process.stderr.isNotEmpty) {
1281
      process.throwException('adb returned error:\n${process.stderr}');
1282
    }
1283 1284

    if (process.exitCode != 0) {
1285
      if (process.stdout.isNotEmpty) {
1286
        process.throwException('adb returned error:\n${process.stdout}');
1287
      }
1288 1289 1290 1291
      process.throwException('adb failed without a message');
    }

    if (hostPort == 0) {
1292
      if (process.stdout.isEmpty) {
1293
        process.throwException('adb did not report forwarded port');
1294 1295 1296 1297 1298
      }
      hostPort = int.tryParse(process.stdout);
      if (hostPort == null) {
        process.throwException('adb returned invalid port number:\n${process.stdout}');
      }
1299
    } else {
1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312
      // 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.
1313
      if (process.stdout.isNotEmpty && process.stdout.trim() != '$hostPort') {
1314
        process.throwException('adb returned error:\n${process.stdout}');
1315
      }
1316 1317
    }

1318
    return hostPort!;
1319 1320
  }

1321
  @override
1322
  Future<void> unforward(ForwardedPort forwardedPort) async {
1323 1324 1325 1326 1327 1328 1329 1330 1331 1332
    final String tcpLine = 'tcp:${forwardedPort.hostPort}';
    final RunResult runResult = await _processUtils.run(
      <String>[
        _adbPath,
        '-s',
        _deviceId,
        'forward',
        '--remove',
        tcpLine,
      ],
1333
    );
1334
    if (runResult.exitCode == 0) {
1335 1336
      return;
    }
1337
    _logger.printError('Failed to unforward port: $runResult');
1338
  }
1339 1340 1341

  @override
  Future<void> dispose() async {
1342
    for (final ForwardedPort port in forwardedPorts) {
1343 1344 1345
      await unforward(port);
    }
  }
1346
}
1347 1348 1349 1350 1351 1352 1353

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