• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:meta/meta.dart';
8
9import '../android/android_sdk.dart';
10import '../android/android_workflow.dart';
11import '../android/apk.dart';
12import '../application_package.dart';
13import '../base/common.dart' show throwToolExit;
14import '../base/file_system.dart';
15import '../base/io.dart';
16import '../base/logger.dart';
17import '../base/platform.dart';
18import '../base/process.dart';
19import '../base/process_manager.dart';
20import '../build_info.dart';
21import '../convert.dart';
22import '../device.dart';
23import '../globals.dart';
24import '../project.dart';
25import '../protocol_discovery.dart';
26
27import 'adb.dart';
28import 'android.dart';
29import 'android_console.dart';
30import 'android_sdk.dart';
31
32enum _HardwareType { emulator, physical }
33
34/// Map to help our `isLocalEmulator` detection.
35const Map<String, _HardwareType> _knownHardware = <String, _HardwareType>{
36  'goldfish': _HardwareType.emulator,
37  'qcom': _HardwareType.physical,
38  'ranchu': _HardwareType.emulator,
39  'samsungexynos7420': _HardwareType.physical,
40  'samsungexynos7580': _HardwareType.physical,
41  'samsungexynos7870': _HardwareType.physical,
42  'samsungexynos8890': _HardwareType.physical,
43  'samsungexynos8895': _HardwareType.physical,
44  'samsungexynos9810': _HardwareType.physical,
45};
46
47bool allowHeapCorruptionOnWindows(int exitCode) {
48  // In platform tools 29.0.0 adb.exe seems to be ending with this heap
49  // corruption error code on seemingly successful termination.
50  // So we ignore this error on Windows.
51  return exitCode == -1073740940 && platform.isWindows;
52}
53
54class AndroidDevices extends PollingDeviceDiscovery {
55  AndroidDevices() : super('Android devices');
56
57  @override
58  bool get supportsPlatform => true;
59
60  @override
61  bool get canListAnything => androidWorkflow.canListDevices;
62
63  @override
64  Future<List<Device>> pollingGetDevices() async => getAdbDevices();
65
66  @override
67  Future<List<String>> getDiagnostics() async => getAdbDeviceDiagnostics();
68}
69
70class AndroidDevice extends Device {
71  AndroidDevice(
72    String id, {
73    this.productID,
74    this.modelID,
75    this.deviceCodeName,
76  }) : super(
77      id,
78      category: Category.mobile,
79      platformType: PlatformType.android,
80      ephemeral: true,
81  );
82
83  final String productID;
84  final String modelID;
85  final String deviceCodeName;
86
87  Map<String, String> _properties;
88  bool _isLocalEmulator;
89  TargetPlatform _platform;
90
91  Future<String> _getProperty(String name) async {
92    if (_properties == null) {
93      _properties = <String, String>{};
94
95      final List<String> propCommand = adbCommandForDevice(<String>['shell', 'getprop']);
96      printTrace(propCommand.join(' '));
97
98      try {
99        // We pass an encoding of latin1 so that we don't try and interpret the
100        // `adb shell getprop` result as UTF8.
101        final ProcessResult result = await processManager.run(
102          propCommand,
103          stdoutEncoding: latin1,
104          stderrEncoding: latin1,
105        );
106        if (result.exitCode == 0 || allowHeapCorruptionOnWindows(result.exitCode)) {
107          _properties = parseAdbDeviceProperties(result.stdout);
108        } else {
109          printError('Error ${result.exitCode} retrieving device properties for $name:');
110          printError(result.stderr);
111        }
112      } on ProcessException catch (error) {
113        printError('Error retrieving device properties for $name: $error');
114      }
115    }
116
117    return _properties[name];
118  }
119
120  @override
121  Future<bool> get isLocalEmulator async {
122    if (_isLocalEmulator == null) {
123      final String hardware = await _getProperty('ro.hardware');
124      printTrace('ro.hardware = $hardware');
125      if (_knownHardware.containsKey(hardware)) {
126        // Look for known hardware models.
127        _isLocalEmulator = _knownHardware[hardware] == _HardwareType.emulator;
128      } else {
129        // Fall back to a best-effort heuristic-based approach.
130        final String characteristics = await _getProperty('ro.build.characteristics');
131        printTrace('ro.build.characteristics = $characteristics');
132        _isLocalEmulator = characteristics != null && characteristics.contains('emulator');
133      }
134    }
135    return _isLocalEmulator;
136  }
137
138  /// The unique identifier for the emulator that corresponds to this device, or
139  /// null if it is not an emulator.
140  ///
141  /// The ID returned matches that in the output of `flutter emulators`. Fetching
142  /// this name may require connecting to the device and if an error occurs null
143  /// will be returned.
144  @override
145  Future<String> get emulatorId async {
146    if (!(await isLocalEmulator))
147      return null;
148
149    // Emulators always have IDs in the format emulator-(port) where port is the
150    // Android Console port number.
151    final RegExp emulatorPortRegex = RegExp(r'emulator-(\d+)');
152
153    final Match portMatch = emulatorPortRegex.firstMatch(id);
154    if (portMatch == null || portMatch.groupCount < 1) {
155      return null;
156    }
157
158    const String host = 'localhost';
159    final int port = int.parse(portMatch.group(1));
160    printTrace('Fetching avd name for $name via Android console on $host:$port');
161
162    try {
163      final Socket socket = await androidConsoleSocketFactory(host, port);
164      final AndroidConsole console = AndroidConsole(socket);
165
166      try {
167        await console
168            .connect()
169            .timeout(timeoutConfiguration.fastOperation,
170                onTimeout: () => throw TimeoutException('Connection timed out'));
171
172        return await console
173            .getAvdName()
174            .timeout(timeoutConfiguration.fastOperation,
175                onTimeout: () => throw TimeoutException('"avd name" timed out'));
176      } finally {
177        console.destroy();
178      }
179    } catch (e) {
180      printTrace('Failed to fetch avd name for emulator at $host:$port: $e');
181      // If we fail to connect to the device, we should not fail so just return
182      // an empty name. This data is best-effort.
183      return null;
184    }
185  }
186
187  @override
188  Future<TargetPlatform> get targetPlatform async {
189    if (_platform == null) {
190      // http://developer.android.com/ndk/guides/abis.html (x86, armeabi-v7a, ...)
191      switch (await _getProperty('ro.product.cpu.abi')) {
192        case 'arm64-v8a':
193          _platform = TargetPlatform.android_arm64;
194          break;
195        case 'x86_64':
196          _platform = TargetPlatform.android_x64;
197          break;
198        case 'x86':
199          _platform = TargetPlatform.android_x86;
200          break;
201        default:
202          _platform = TargetPlatform.android_arm;
203          break;
204      }
205    }
206
207    return _platform;
208  }
209
210  @override
211  Future<String> get sdkNameAndVersion async =>
212      'Android ${await _sdkVersion} (API ${await _apiVersion})';
213
214  Future<String> get _sdkVersion => _getProperty('ro.build.version.release');
215
216  Future<String> get _apiVersion => _getProperty('ro.build.version.sdk');
217
218  _AdbLogReader _logReader;
219  _AndroidDevicePortForwarder _portForwarder;
220
221  List<String> adbCommandForDevice(List<String> args) {
222    return <String>[getAdbPath(androidSdk), '-s', id, ...args];
223  }
224
225  String runAdbCheckedSync(
226      List<String> params, {
227        String workingDirectory,
228        bool allowReentrantFlutter = false,
229        Map<String, String> environment}) {
230    return runCheckedSync(adbCommandForDevice(params), workingDirectory: workingDirectory,
231        allowReentrantFlutter: allowReentrantFlutter,
232        environment: environment,
233        whiteListFailures: allowHeapCorruptionOnWindows
234    );
235  }
236
237  Future<RunResult> runAdbCheckedAsync(
238      List<String> params, {
239        String workingDirectory,
240        bool allowReentrantFlutter = false,
241      }) async {
242    return runCheckedAsync(adbCommandForDevice(params), workingDirectory: workingDirectory,
243        allowReentrantFlutter: allowReentrantFlutter,
244        whiteListFailures: allowHeapCorruptionOnWindows);
245  }
246
247  bool _isValidAdbVersion(String adbVersion) {
248    // Sample output: 'Android Debug Bridge version 1.0.31'
249    final Match versionFields = RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
250    if (versionFields != null) {
251      final int majorVersion = int.parse(versionFields[1]);
252      final int minorVersion = int.parse(versionFields[2]);
253      final int patchVersion = int.parse(versionFields[3]);
254      if (majorVersion > 1) {
255        return true;
256      }
257      if (majorVersion == 1 && minorVersion > 0) {
258        return true;
259      }
260      if (majorVersion == 1 && minorVersion == 0 && patchVersion >= 39) {
261        return true;
262      }
263      return false;
264    }
265    printError(
266        'Unrecognized adb version string $adbVersion. Skipping version check.');
267    return true;
268  }
269
270  Future<bool> _checkForSupportedAdbVersion() async {
271    if (androidSdk == null)
272      return false;
273
274    try {
275      final RunResult adbVersion = await runCheckedAsync(<String>[getAdbPath(androidSdk), 'version']);
276      if (_isValidAdbVersion(adbVersion.stdout))
277        return true;
278      printError('The ADB at "${getAdbPath(androidSdk)}" is too old; please install version 1.0.39 or later.');
279    } catch (error, trace) {
280      printError('Error running ADB: $error', stackTrace: trace);
281    }
282
283    return false;
284  }
285
286  Future<bool> _checkForSupportedAndroidVersion() async {
287    try {
288      // If the server is automatically restarted, then we get irrelevant
289      // output lines like this, which we want to ignore:
290      //   adb server is out of date.  killing..
291      //   * daemon started successfully *
292      await runCheckedAsync(<String>[getAdbPath(androidSdk), 'start-server']);
293
294      // Sample output: '22'
295      final String sdkVersion = await _getProperty('ro.build.version.sdk');
296
297
298      final int sdkVersionParsed = int.tryParse(sdkVersion);
299      if (sdkVersionParsed == null) {
300        printError('Unexpected response from getprop: "$sdkVersion"');
301        return false;
302      }
303
304      if (sdkVersionParsed < minApiLevel) {
305        printError(
306          'The Android version ($sdkVersion) on the target device is too old. Please '
307          'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.');
308        return false;
309      }
310
311      return true;
312    } catch (e) {
313      printError('Unexpected failure from adb: $e');
314      return false;
315    }
316  }
317
318  String _getDeviceSha1Path(ApplicationPackage app) {
319    return '/data/local/tmp/sky.${app.id}.sha1';
320  }
321
322  Future<String> _getDeviceApkSha1(ApplicationPackage app) async {
323    final RunResult result = await runAsync(adbCommandForDevice(<String>['shell', 'cat', _getDeviceSha1Path(app)]));
324    return result.stdout;
325  }
326
327  String _getSourceSha1(ApplicationPackage app) {
328    final AndroidApk apk = app;
329    final File shaFile = fs.file('${apk.file.path}.sha1');
330    return shaFile.existsSync() ? shaFile.readAsStringSync() : '';
331  }
332
333  @override
334  String get name => modelID;
335
336  @override
337  Future<bool> isAppInstalled(ApplicationPackage app) async {
338    // This call takes 400ms - 600ms.
339    try {
340      final RunResult listOut = await runAdbCheckedAsync(<String>['shell', 'pm', 'list', 'packages', app.id]);
341      return LineSplitter.split(listOut.stdout).contains('package:${app.id}');
342    } catch (error) {
343      printTrace('$error');
344      return false;
345    }
346  }
347
348  @override
349  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async {
350    final String installedSha1 = await _getDeviceApkSha1(app);
351    return installedSha1.isNotEmpty && installedSha1 == _getSourceSha1(app);
352  }
353
354  @override
355  Future<bool> installApp(ApplicationPackage app) async {
356    final AndroidApk apk = app;
357    if (!apk.file.existsSync()) {
358      printError('"${fs.path.relative(apk.file.path)}" does not exist.');
359      return false;
360    }
361
362    if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
363      return false;
364
365    final Status status = logger.startProgress('Installing ${fs.path.relative(apk.file.path)}...', timeout: timeoutConfiguration.slowOperation);
366    final RunResult installResult = await runAsync(adbCommandForDevice(<String>['install', '-t', '-r', apk.file.path]));
367    status.stop();
368    // Some versions of adb exit with exit code 0 even on failure :(
369    // Parsing the output to check for failures.
370    final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true);
371    final String failure = failureExp.stringMatch(installResult.stdout);
372    if (failure != null) {
373      printError('Package install error: $failure');
374      return false;
375    }
376    if (installResult.exitCode != 0) {
377      printError('Error: ADB exited with exit code ${installResult.exitCode}');
378      printError('$installResult');
379      return false;
380    }
381    try {
382      await runAdbCheckedAsync(<String>[
383        'shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app),
384      ]);
385    } on ProcessException catch (error) {
386      printError('adb shell failed to write the SHA hash: $error.');
387      return false;
388    }
389    return true;
390  }
391
392  @override
393  Future<bool> uninstallApp(ApplicationPackage app) async {
394    if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
395      return false;
396
397    String uninstallOut;
398    try {
399      uninstallOut = (await runCheckedAsync(adbCommandForDevice(<String>['uninstall', app.id]))).stdout;
400    } catch (error) {
401      printError('adb uninstall failed: $error');
402      return false;
403    }
404    final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true);
405    final String failure = failureExp.stringMatch(uninstallOut);
406    if (failure != null) {
407      printError('Package uninstall error: $failure');
408      return false;
409    }
410
411    return true;
412  }
413
414  Future<bool> _installLatestApp(ApplicationPackage package) async {
415    final bool wasInstalled = await isAppInstalled(package);
416    if (wasInstalled) {
417      if (await isLatestBuildInstalled(package)) {
418        printTrace('Latest build already installed.');
419        return true;
420      }
421    }
422    printTrace('Installing APK.');
423    if (!await installApp(package)) {
424      printTrace('Warning: Failed to install APK.');
425      if (wasInstalled) {
426        printStatus('Uninstalling old version...');
427        if (!await uninstallApp(package)) {
428          printError('Error: Uninstalling old version failed.');
429          return false;
430        }
431        if (!await installApp(package)) {
432          printError('Error: Failed to install APK again.');
433          return false;
434        }
435        return true;
436      }
437      return false;
438    }
439    return true;
440  }
441
442  @override
443  Future<LaunchResult> startApp(
444    ApplicationPackage package, {
445    String mainPath,
446    String route,
447    DebuggingOptions debuggingOptions,
448    Map<String, dynamic> platformArgs,
449    bool prebuiltApplication = false,
450    bool ipv6 = false,
451    bool usesTerminalUi = true,
452  }) async {
453    if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
454      return LaunchResult.failed();
455
456    final TargetPlatform devicePlatform = await targetPlatform;
457    if (!(devicePlatform == TargetPlatform.android_arm ||
458          devicePlatform == TargetPlatform.android_arm64) &&
459        !debuggingOptions.buildInfo.isDebug) {
460      printError('Profile and release builds are only supported on ARM targets.');
461      return LaunchResult.failed();
462    }
463
464    AndroidArch androidArch;
465    switch (devicePlatform) {
466      case TargetPlatform.android_arm:
467        androidArch = AndroidArch.armeabi_v7a;
468        break;
469      case TargetPlatform.android_arm64:
470        androidArch = AndroidArch.arm64_v8a;
471        break;
472      case TargetPlatform.android_x64:
473        androidArch = AndroidArch.x86_64;
474        break;
475      case TargetPlatform.android_x86:
476        androidArch = AndroidArch.x86;
477        break;
478      default:
479        printError('Android platforms are only supported.');
480        return LaunchResult.failed();
481    }
482
483    if (!prebuiltApplication || androidSdk.licensesAvailable && androidSdk.latestVersion == null) {
484      printTrace('Building APK');
485      final FlutterProject project = FlutterProject.current();
486      await buildApk(
487          project: project,
488          target: mainPath,
489          androidBuildInfo: AndroidBuildInfo(debuggingOptions.buildInfo,
490            targetArchs: <AndroidArch>[androidArch]
491          ),
492      );
493      // Package has been built, so we can get the updated application ID and
494      // activity name from the .apk.
495      package = await AndroidApk.fromAndroidProject(project.android);
496    }
497    // There was a failure parsing the android project information.
498    if (package == null) {
499      throwToolExit('Problem building Android application: see above error(s).');
500    }
501
502    printTrace("Stopping app '${package.name}' on $name.");
503    await stopApp(package);
504
505    if (!await _installLatestApp(package))
506      return LaunchResult.failed();
507
508    final bool traceStartup = platformArgs['trace-startup'] ?? false;
509    final AndroidApk apk = package;
510    printTrace('$this startApp');
511
512    ProtocolDiscovery observatoryDiscovery;
513
514    if (debuggingOptions.debuggingEnabled) {
515      // TODO(devoncarew): Remember the forwarding information (so we can later remove the
516      // port forwarding or set it up again when adb fails on us).
517      observatoryDiscovery = ProtocolDiscovery.observatory(
518        getLogReader(),
519        portForwarder: portForwarder,
520        hostPort: debuggingOptions.observatoryPort,
521        ipv6: ipv6,
522      );
523    }
524
525    List<String> cmd;
526
527    cmd = <String>[
528      'shell', 'am', 'start',
529      '-a', 'android.intent.action.RUN',
530      '-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP
531      '--ez', 'enable-background-compilation', 'true',
532      '--ez', 'enable-dart-profiling', 'true',
533      if (traceStartup)
534        ...<String>['--ez', 'trace-startup', 'true'],
535      if (route != null)
536        ...<String>['--es', 'route', route],
537      if (debuggingOptions.enableSoftwareRendering)
538        ...<String>['--ez', 'enable-software-rendering', 'true'],
539      if (debuggingOptions.skiaDeterministicRendering)
540        ...<String>['--ez', 'skia-deterministic-rendering', 'true'],
541      if (debuggingOptions.traceSkia)
542        ...<String>['--ez', 'trace-skia', 'true'],
543      if (debuggingOptions.traceSystrace)
544        ...<String>['--ez', 'trace-systrace', 'true'],
545      if (debuggingOptions.dumpSkpOnShaderCompilation)
546        ...<String>['--ez', 'dump-skp-on-shader-compilation', 'true'],
547      if (debuggingOptions.debuggingEnabled)
548        ...<String>[
549          if (debuggingOptions.buildInfo.isDebug)
550            ...<String>[
551              ...<String>['--ez', 'enable-checked-mode', 'true'],
552              ...<String>['--ez', 'verify-entry-points', 'true'],
553            ],
554          if (debuggingOptions.startPaused)
555            ...<String>['--ez', 'start-paused', 'true'],
556          if (debuggingOptions.disableServiceAuthCodes)
557            ...<String>['--ez', 'disable-service-auth-codes', 'true'],
558          if (debuggingOptions.dartFlags.isNotEmpty)
559            ...<String>['--es', 'dart-flags', debuggingOptions.dartFlags],
560          if (debuggingOptions.useTestFonts)
561            ...<String>['--ez', 'use-test-fonts', 'true'],
562          if (debuggingOptions.verboseSystemLogs)
563            ...<String>['--ez', 'verbose-logging', 'true'],
564        ],
565      apk.launchActivity,
566    ];
567    final String result = (await runAdbCheckedAsync(cmd)).stdout;
568    // This invocation returns 0 even when it fails.
569    if (result.contains('Error: ')) {
570      printError(result.trim(), wrap: false);
571      return LaunchResult.failed();
572    }
573
574    if (!debuggingOptions.debuggingEnabled)
575      return LaunchResult.succeeded();
576
577    // Wait for the service protocol port here. This will complete once the
578    // device has printed "Observatory is listening on...".
579    printTrace('Waiting for observatory port to be available...');
580
581    // TODO(danrubel): Waiting for observatory services can be made common across all devices.
582    try {
583      Uri observatoryUri;
584
585      if (debuggingOptions.buildInfo.isDebug || debuggingOptions.buildInfo.isProfile) {
586        observatoryUri = await observatoryDiscovery.uri;
587      }
588
589      return LaunchResult.succeeded(observatoryUri: observatoryUri);
590    } catch (error) {
591      printError('Error waiting for a debug connection: $error');
592      return LaunchResult.failed();
593    } finally {
594      await observatoryDiscovery.cancel();
595    }
596  }
597
598  @override
599  bool get supportsHotReload => true;
600
601  @override
602  bool get supportsHotRestart => true;
603
604  @override
605  Future<bool> stopApp(ApplicationPackage app) {
606    final List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]);
607    return runCommandAndStreamOutput(command).then<bool>(
608        (int exitCode) => exitCode == 0 || allowHeapCorruptionOnWindows(exitCode));
609  }
610
611  @override
612  void clearLogs() {
613    runSync(adbCommandForDevice(<String>['logcat', '-c']));
614  }
615
616  @override
617  DeviceLogReader getLogReader({ ApplicationPackage app }) {
618    // The Android log reader isn't app-specific.
619    _logReader ??= _AdbLogReader(this);
620    return _logReader;
621  }
622
623  @override
624  DevicePortForwarder get portForwarder => _portForwarder ??= _AndroidDevicePortForwarder(this);
625
626  static final RegExp _timeRegExp = RegExp(r'^\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}', multiLine: true);
627
628  /// Return the most recent timestamp in the Android log or [null] if there is
629  /// no available timestamp. The format can be passed to logcat's -T option.
630  String get lastLogcatTimestamp {
631    String output;
632    try {
633      output = runAdbCheckedSync(<String>[
634        'shell', '-x', 'logcat', '-v', 'time', '-t', '1',
635      ]);
636    } catch (error) {
637      printError('Failed to extract the most recent timestamp from the Android log: $error.');
638      return null;
639    }
640    final Match timeMatch = _timeRegExp.firstMatch(output);
641    return timeMatch?.group(0);
642  }
643
644  @override
645  bool isSupported() => true;
646
647  @override
648  bool get supportsScreenshot => true;
649
650  @override
651  Future<void> takeScreenshot(File outputFile) async {
652    const String remotePath = '/data/local/tmp/flutter_screenshot.png';
653    await runAdbCheckedAsync(<String>['shell', 'screencap', '-p', remotePath]);
654    await runCheckedAsync(adbCommandForDevice(<String>['pull', remotePath, outputFile.path]));
655    await runAdbCheckedAsync(<String>['shell', 'rm', remotePath]);
656  }
657
658  @override
659  bool isSupportedForProject(FlutterProject flutterProject) {
660    return flutterProject.android.existsSync();
661  }
662}
663
664Map<String, String> parseAdbDeviceProperties(String str) {
665  final Map<String, String> properties = <String, String>{};
666  final RegExp propertyExp = RegExp(r'\[(.*?)\]: \[(.*?)\]');
667  for (Match match in propertyExp.allMatches(str))
668    properties[match.group(1)] = match.group(2);
669  return properties;
670}
671
672/// Return the list of connected ADB devices.
673List<AndroidDevice> getAdbDevices() {
674  final String adbPath = getAdbPath(androidSdk);
675  if (adbPath == null)
676    return <AndroidDevice>[];
677  String text;
678  try {
679    text = runSync(<String>[adbPath, 'devices', '-l']);
680  } on ArgumentError catch (exception) {
681    throwToolExit('Unable to find "adb", check your Android SDK installation and '
682      'ANDROID_HOME environment variable: ${exception.message}');
683  } on ProcessException catch (exception) {
684    throwToolExit('Unable to run "adb", check your Android SDK installation and '
685      'ANDROID_HOME environment variable: ${exception.executable}');
686  }
687  final List<AndroidDevice> devices = <AndroidDevice>[];
688  parseADBDeviceOutput(text, devices: devices);
689  return devices;
690}
691
692/// Get diagnostics about issues with any connected devices.
693Future<List<String>> getAdbDeviceDiagnostics() async {
694  final String adbPath = getAdbPath(androidSdk);
695  if (adbPath == null)
696    return <String>[];
697
698  final RunResult result = await runAsync(<String>[adbPath, 'devices', '-l']);
699  if (result.exitCode != 0) {
700    return <String>[];
701  } else {
702    final String text = result.stdout;
703    final List<String> diagnostics = <String>[];
704    parseADBDeviceOutput(text, diagnostics: diagnostics);
705    return diagnostics;
706  }
707}
708
709// 015d172c98400a03       device usb:340787200X product:nakasi model:Nexus_7 device:grouper
710final RegExp _kDeviceRegex = RegExp(r'^(\S+)\s+(\S+)(.*)');
711
712/// Parse the given `adb devices` output in [text], and fill out the given list
713/// of devices and possible device issue diagnostics. Either argument can be null,
714/// in which case information for that parameter won't be populated.
715@visibleForTesting
716void parseADBDeviceOutput(
717  String text, {
718  List<AndroidDevice> devices,
719  List<String> diagnostics,
720}) {
721  // Check for error messages from adb
722  if (!text.contains('List of devices')) {
723    diagnostics?.add(text);
724    return;
725  }
726
727  for (String line in text.trim().split('\n')) {
728    // Skip lines like: * daemon started successfully *
729    if (line.startsWith('* daemon '))
730      continue;
731
732    // Skip lines about adb server and client version not matching
733    if (line.startsWith(RegExp(r'adb server (version|is out of date)'))) {
734      diagnostics?.add(line);
735      continue;
736    }
737
738    if (line.startsWith('List of devices'))
739      continue;
740
741    if (_kDeviceRegex.hasMatch(line)) {
742      final Match match = _kDeviceRegex.firstMatch(line);
743
744      final String deviceID = match[1];
745      final String deviceState = match[2];
746      String rest = match[3];
747
748      final Map<String, String> info = <String, String>{};
749      if (rest != null && rest.isNotEmpty) {
750        rest = rest.trim();
751        for (String data in rest.split(' ')) {
752          if (data.contains(':')) {
753            final List<String> fields = data.split(':');
754            info[fields[0]] = fields[1];
755          }
756        }
757      }
758
759      if (info['model'] != null)
760        info['model'] = cleanAdbDeviceName(info['model']);
761
762      if (deviceState == 'unauthorized') {
763        diagnostics?.add(
764          'Device $deviceID is not authorized.\n'
765          'You might need to check your device for an authorization dialog.'
766        );
767      } else if (deviceState == 'offline') {
768        diagnostics?.add('Device $deviceID is offline.');
769      } else {
770        devices?.add(AndroidDevice(
771          deviceID,
772          productID: info['product'],
773          modelID: info['model'] ?? deviceID,
774          deviceCodeName: info['device'],
775        ));
776      }
777    } else {
778      diagnostics?.add(
779        'Unexpected failure parsing device information from adb output:\n'
780        '$line\n'
781        'Please report a bug at https://github.com/flutter/flutter/issues/new/choose');
782    }
783  }
784}
785
786/// A log reader that logs from `adb logcat`.
787class _AdbLogReader extends DeviceLogReader {
788  _AdbLogReader(this.device) {
789    _linesController = StreamController<String>.broadcast(
790      onListen: _start,
791      onCancel: _stop,
792    );
793  }
794
795  final AndroidDevice device;
796
797  StreamController<String> _linesController;
798  Process _process;
799
800  @override
801  Stream<String> get logLines => _linesController.stream;
802
803  @override
804  String get name => device.name;
805
806  DateTime _timeOrigin;
807
808  DateTime _adbTimestampToDateTime(String adbTimestamp) {
809    // The adb timestamp format is: mm-dd hours:minutes:seconds.milliseconds
810    // Dart's DateTime parse function accepts this format so long as we provide
811    // the year, resulting in:
812    // yyyy-mm-dd hours:minutes:seconds.milliseconds.
813    return DateTime.parse('${DateTime.now().year}-$adbTimestamp');
814  }
815
816  void _start() {
817    // Start the adb logcat process.
818    final List<String> args = <String>['shell', '-x', 'logcat', '-v', 'time'];
819    final String lastTimestamp = device.lastLogcatTimestamp;
820    if (lastTimestamp != null)
821      _timeOrigin = _adbTimestampToDateTime(lastTimestamp);
822    else
823      _timeOrigin = null;
824    runCommand(device.adbCommandForDevice(args)).then<void>((Process process) {
825      _process = process;
826      // We expect logcat streams to occasionally contain invalid utf-8,
827      // see: https://github.com/flutter/flutter/pull/8864.
828      const Utf8Decoder decoder = Utf8Decoder(reportErrors: false);
829      _process.stdout.transform<String>(decoder).transform<String>(const LineSplitter()).listen(_onLine);
830      _process.stderr.transform<String>(decoder).transform<String>(const LineSplitter()).listen(_onLine);
831      _process.exitCode.whenComplete(() {
832        if (_linesController.hasListener)
833          _linesController.close();
834      });
835    });
836  }
837
838  // 'W/ActivityManager(pid): '
839  static final RegExp _logFormat = RegExp(r'^[VDIWEF]\/.*?\(\s*(\d+)\):\s');
840
841  static final List<RegExp> _whitelistedTags = <RegExp>[
842    RegExp(r'^[VDIWEF]\/flutter[^:]*:\s+', caseSensitive: false),
843    RegExp(r'^[IE]\/DartVM[^:]*:\s+'),
844    RegExp(r'^[WEF]\/AndroidRuntime:\s+'),
845    RegExp(r'^[WEF]\/ActivityManager:\s+.*(\bflutter\b|\bdomokit\b|\bsky\b)'),
846    RegExp(r'^[WEF]\/System\.err:\s+'),
847    RegExp(r'^[F]\/[\S^:]+:\s+'),
848  ];
849
850  // 'F/libc(pid): Fatal signal 11'
851  static final RegExp _fatalLog = RegExp(r'^F\/libc\s*\(\s*\d+\):\sFatal signal (\d+)');
852
853  // 'I/DEBUG(pid): ...'
854  static final RegExp _tombstoneLine = RegExp(r'^[IF]\/DEBUG\s*\(\s*\d+\):\s(.+)$');
855
856  // 'I/DEBUG(pid): Tombstone written to: '
857  static final RegExp _tombstoneTerminator = RegExp(r'^Tombstone written to:\s');
858
859  // we default to true in case none of the log lines match
860  bool _acceptedLastLine = true;
861
862  // Whether a fatal crash is happening or not.
863  // During a fatal crash only lines from the crash are accepted, the rest are
864  // dropped.
865  bool _fatalCrash = false;
866
867  // The format of the line is controlled by the '-v' parameter passed to
868  // adb logcat. We are currently passing 'time', which has the format:
869  // mm-dd hh:mm:ss.milliseconds Priority/Tag( PID): ....
870  void _onLine(String line) {
871    final Match timeMatch = AndroidDevice._timeRegExp.firstMatch(line);
872    if (timeMatch == null) {
873      return;
874    }
875    if (_timeOrigin != null) {
876      final String timestamp = timeMatch.group(0);
877      final DateTime time = _adbTimestampToDateTime(timestamp);
878      if (!time.isAfter(_timeOrigin)) {
879        // Ignore log messages before the origin.
880        return;
881      }
882    }
883    if (line.length == timeMatch.end) {
884      return;
885    }
886    // Chop off the time.
887    line = line.substring(timeMatch.end + 1);
888    final Match logMatch = _logFormat.firstMatch(line);
889    if (logMatch != null) {
890      bool acceptLine = false;
891
892      if (_fatalCrash) {
893        // While a fatal crash is going on, only accept lines from the crash
894        // Otherwise the crash log in the console may get interrupted
895
896        final Match fatalMatch = _tombstoneLine.firstMatch(line);
897
898        if (fatalMatch != null) {
899          acceptLine = true;
900
901          line = fatalMatch[1];
902
903          if (_tombstoneTerminator.hasMatch(fatalMatch[1])) {
904            // Hit crash terminator, stop logging the crash info
905            _fatalCrash = false;
906          }
907        }
908      } else if (appPid != null && int.parse(logMatch.group(1)) == appPid) {
909        acceptLine = true;
910
911        if (_fatalLog.hasMatch(line)) {
912          // Hit fatal signal, app is now crashing
913          _fatalCrash = true;
914        }
915      } else {
916        // Filter on approved names and levels.
917        acceptLine = _whitelistedTags.any((RegExp re) => re.hasMatch(line));
918      }
919
920      if (acceptLine) {
921        _acceptedLastLine = true;
922        _linesController.add(line);
923        return;
924      }
925      _acceptedLastLine = false;
926    } else if (line == '--------- beginning of system' ||
927               line == '--------- beginning of main') {
928      // hide the ugly adb logcat log boundaries at the start
929      _acceptedLastLine = false;
930    } else {
931      // If it doesn't match the log pattern at all, then pass it through if we
932      // passed the last matching line through. It might be a multiline message.
933      if (_acceptedLastLine) {
934        _linesController.add(line);
935        return;
936      }
937    }
938  }
939
940  void _stop() {
941    // TODO(devoncarew): We should remove adb port forwarding here.
942
943    _process?.kill();
944  }
945}
946
947class _AndroidDevicePortForwarder extends DevicePortForwarder {
948  _AndroidDevicePortForwarder(this.device);
949
950  final AndroidDevice device;
951
952  static int _extractPort(String portString) {
953
954    return int.tryParse(portString.trim());
955  }
956
957  @override
958  List<ForwardedPort> get forwardedPorts {
959    final List<ForwardedPort> ports = <ForwardedPort>[];
960
961    String stdout;
962    try {
963      stdout = runCheckedSync(device.adbCommandForDevice(
964        <String>['forward', '--list']
965      ));
966    } catch (error) {
967      printError('Failed to list forwarded ports: $error.');
968      return ports;
969    }
970
971    final List<String> lines = LineSplitter.split(stdout).toList();
972    for (String line in lines) {
973      if (line.startsWith(device.id)) {
974        final List<String> splitLine = line.split('tcp:');
975
976        // Sanity check splitLine.
977        if (splitLine.length != 3)
978          continue;
979
980        // Attempt to extract ports.
981        final int hostPort = _extractPort(splitLine[1]);
982        final int devicePort = _extractPort(splitLine[2]);
983
984        // Failed, skip.
985        if (hostPort == null || devicePort == null)
986          continue;
987
988        ports.add(ForwardedPort(hostPort, devicePort));
989      }
990    }
991
992    return ports;
993  }
994
995  @override
996  Future<int> forward(int devicePort, { int hostPort }) async {
997    hostPort ??= 0;
998    final RunResult process = await runCheckedAsync(device.adbCommandForDevice(
999      <String>['forward', 'tcp:$hostPort', 'tcp:$devicePort']
1000    ));
1001
1002    if (process.stderr.isNotEmpty)
1003      process.throwException('adb returned error:\n${process.stderr}');
1004
1005    if (process.exitCode != 0) {
1006      if (process.stdout.isNotEmpty)
1007        process.throwException('adb returned error:\n${process.stdout}');
1008      process.throwException('adb failed without a message');
1009    }
1010
1011    if (hostPort == 0) {
1012      if (process.stdout.isEmpty)
1013        process.throwException('adb did not report forwarded port');
1014      hostPort = int.tryParse(process.stdout) ?? (throw 'adb returned invalid port number:\n${process.stdout}');
1015    } else {
1016      // stdout may be empty or the port we asked it to forward, though it's
1017      // not documented (or obvious) what triggers each case.
1018      //
1019      // Observations are:
1020      //   - On MacOS it's always empty when Flutter spawns the process, but
1021      //   - On MacOS it prints the port number when run from the terminal, unless
1022      //     the port is already forwarded, when it also prints nothing.
1023      //   - On ChromeOS, the port appears to be printed even when Flutter spawns
1024      //     the process
1025      //
1026      // To cover all cases, we accept the output being either empty or exactly
1027      // the port number, but treat any other output as probably being an error
1028      // message.
1029      if (process.stdout.isNotEmpty && process.stdout.trim() != '$hostPort')
1030        process.throwException('adb returned error:\n${process.stdout}');
1031    }
1032
1033    return hostPort;
1034  }
1035
1036  @override
1037  Future<void> unforward(ForwardedPort forwardedPort) async {
1038    await runCheckedAsync(device.adbCommandForDevice(
1039      <String>['forward', '--remove', 'tcp:${forwardedPort.hostPort}']
1040    ));
1041  }
1042}
1043