android_device.dart 45.1 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 '../android/android_builder.dart';
11
import '../android/android_sdk.dart';
12
import '../application_package.dart';
13
import '../base/common.dart' show throwToolExit, unawaited;
14
import '../base/file_system.dart';
15
import '../base/io.dart';
16
import '../base/logger.dart';
17
import '../base/platform.dart';
18
import '../base/process.dart';
19
import '../build_info.dart';
20
import '../convert.dart';
21
import '../device.dart';
22
import '../device_port_forwarder.dart';
23
import '../project.dart';
24
import '../protocol_discovery.dart';
25

26
import 'android.dart';
27
import 'android_console.dart';
28
import 'android_sdk.dart';
29
import 'application_package.dart';
30

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

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

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

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

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

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

100 101 102 103 104 105 106 107 108 109 110 111 112
    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);
113
      }
114 115
    } on ProcessException catch (error) {
      _logger.printError('Error retrieving device properties for $name: $error');
116
    }
117 118
    return properties;
  }();
119

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

124
  @override
125 126 127 128 129 130
  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;
131
    }
132 133 134 135 136
    // 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');
  }();
137

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

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

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

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

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

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

        return await console
            .getAvdName()
175
            .timeout(const Duration(seconds: 2),
176 177 178 179
                onTimeout: () => throw TimeoutException('"avd name" timed out'));
      } finally {
        console.destroy();
      }
180
    } on Exception catch (e) {
181
      _logger.printTrace('Failed to fetch avd name for emulator at $host:$port: $e');
182 183 184 185 186 187
      // 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;
    }
  }

188
  @override
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
  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;
209
    }
210
  }();
211

212 213 214 215 216 217 218 219 220
  @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;
221 222 223 224 225 226 227 228 229 230 231
      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_uwp_x64:
      case TargetPlatform.windows_x64:
232 233 234 235
        throw UnsupportedError('Invalid target platform for Android');
    }
  }

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

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

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

244 245
  AdbLogReader? _logReader;
  AdbLogReader? _pastLogReader;
246

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

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

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

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

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

307 308 309
    return false;
  }

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

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

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

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

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

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

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

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

367
  @override
368 369 370
  String get name => modelID;

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

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

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

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

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

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

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

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

520
  AndroidApk? _package;
521

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

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

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

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

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

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

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

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

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

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

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

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

  @override
  bool get supportsHotRestart => true;
717

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  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
931 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
    .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 {
967 968
  static const String _kUpTimeKey = 'Uptime';
  static const String _kRealTimeKey = 'Realtime';
969 970 971 972 973 974 975 976 977
  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';

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

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

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

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

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

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

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

1056
  final Process _adbProcess;
1057

1058 1059 1060
  @override
  final String name;

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

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

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

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

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

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

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

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

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

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

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

      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

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

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

1145
          line = fatalMatch[1]!;
1146

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return ports;
  }

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

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

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

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

1317
    return hostPort!;
1318 1319
  }

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

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

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