| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| |
| import 'package:meta/meta.dart'; |
| import 'package:platform/platform.dart'; |
| import 'package:process/process.dart'; |
| |
| import '../base/common.dart'; |
| import '../base/context.dart'; |
| import '../base/file_system.dart'; |
| import '../base/io.dart'; |
| import '../base/logger.dart'; |
| import '../base/os.dart'; |
| import '../base/process.dart'; |
| import '../base/user_messages.dart'; |
| import '../base/utils.dart'; |
| import '../base/version.dart'; |
| import '../convert.dart'; |
| import '../doctor.dart'; |
| import '../globals.dart' as globals; |
| import 'android_sdk.dart'; |
| import 'android_studio.dart'; |
| |
| const int kAndroidSdkMinVersion = 28; |
| final Version kAndroidJavaMinVersion = Version(1, 8, 0); |
| final Version kAndroidSdkBuildToolsMinVersion = Version(28, 0, 3); |
| |
| AndroidWorkflow get androidWorkflow => context.get<AndroidWorkflow>(); |
| AndroidValidator get androidValidator => context.get<AndroidValidator>(); |
| AndroidLicenseValidator get androidLicenseValidator => context.get<AndroidLicenseValidator>(); |
| |
| enum LicensesAccepted { |
| none, |
| some, |
| all, |
| unknown, |
| } |
| |
| final RegExp licenseCounts = RegExp(r'(\d+) of (\d+) SDK package licenses? not accepted.'); |
| final RegExp licenseNotAccepted = RegExp(r'licenses? not accepted', caseSensitive: false); |
| final RegExp licenseAccepted = RegExp(r'All SDK package licenses accepted.'); |
| |
| class AndroidWorkflow implements Workflow { |
| @override |
| bool get appliesToHostPlatform => true; |
| |
| @override |
| bool get canListDevices => getAdbPath(androidSdk) != null; |
| |
| @override |
| bool get canLaunchDevices => androidSdk != null && androidSdk.validateSdkWellFormed().isEmpty; |
| |
| @override |
| bool get canListEmulators => getEmulatorPath(androidSdk) != null; |
| } |
| |
| class AndroidValidator extends DoctorValidator { |
| AndroidValidator({ |
| @required AndroidSdk androidSdk, |
| @required AndroidStudio androidStudio, |
| @required FileSystem fileSystem, |
| @required Logger logger, |
| @required Platform platform, |
| @required ProcessManager processManager, |
| @required UserMessages userMessages, |
| }) : _androidSdk = androidSdk, |
| _androidStudio = androidStudio, |
| _fileSystem = fileSystem, |
| _logger = logger, |
| _operatingSystemUtils = OperatingSystemUtils( |
| fileSystem: fileSystem, |
| logger: logger, |
| platform: platform, |
| processManager: processManager, |
| ), |
| _platform = platform, |
| _processManager = processManager, |
| _userMessages = userMessages, |
| super('Android toolchain - develop for Android devices'); |
| |
| final AndroidSdk _androidSdk; |
| final AndroidStudio _androidStudio; |
| final FileSystem _fileSystem; |
| final Logger _logger; |
| final OperatingSystemUtils _operatingSystemUtils; |
| final Platform _platform; |
| final ProcessManager _processManager; |
| final UserMessages _userMessages; |
| |
| @override |
| String get slowWarning => '${_task ?? 'This'} is taking a long time...'; |
| String _task; |
| |
| /// Finds the semantic version anywhere in a text. |
| static final RegExp _javaVersionPattern = RegExp(r'(\d+)(\.(\d+)(\.(\d+))?)?'); |
| |
| /// `java -version` response is not only a number, but also includes other |
| /// information eg. `openjdk version "1.7.0_212"`. |
| /// This method extracts only the semantic version from from that response. |
| static String _extractJavaVersion(String text) { |
| final Match match = _javaVersionPattern.firstMatch(text ?? ''); |
| return text?.substring(match.start, match.end); |
| } |
| |
| /// Returns false if we cannot determine the Java version or if the version |
| /// is older that the minimum allowed version of 1.8. |
| Future<bool> _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) async { |
| _task = 'Checking Java status'; |
| try { |
| if (!_processManager.canRun(javaBinary)) { |
| messages.add(ValidationMessage.error(_userMessages.androidCantRunJavaBinary(javaBinary))); |
| return false; |
| } |
| String javaVersionText; |
| try { |
| _logger.printTrace('java -version'); |
| final ProcessResult result = await _processManager.run(<String>[javaBinary, '-version']); |
| if (result.exitCode == 0) { |
| final List<String> versionLines = (result.stderr as String).split('\n'); |
| javaVersionText = versionLines.length >= 2 ? versionLines[1] : versionLines[0]; |
| } |
| } on Exception catch (error) { |
| _logger.printTrace(error.toString()); |
| } |
| if (javaVersionText == null || javaVersionText.isEmpty) { |
| // Could not determine the java version. |
| messages.add(ValidationMessage.error(_userMessages.androidUnknownJavaVersion)); |
| return false; |
| } |
| final Version javaVersion = Version.parse(_extractJavaVersion(javaVersionText)); |
| if (javaVersion < kAndroidJavaMinVersion) { |
| messages.add(ValidationMessage.error(_userMessages.androidJavaMinimumVersion(javaVersionText))); |
| return false; |
| } |
| messages.add(ValidationMessage(_userMessages.androidJavaVersion(javaVersionText))); |
| return true; |
| } finally { |
| _task = null; |
| } |
| } |
| |
| @override |
| Future<ValidationResult> validate() async { |
| final List<ValidationMessage> messages = <ValidationMessage>[]; |
| |
| if (_androidSdk == null) { |
| // No Android SDK found. |
| if (_platform.environment.containsKey(kAndroidHome)) { |
| final String androidHomeDir = _platform.environment[kAndroidHome]; |
| messages.add(ValidationMessage.error(_userMessages.androidBadSdkDir(kAndroidHome, androidHomeDir))); |
| } else { |
| // Instruct user to set [kAndroidSdkRoot] and not deprecated [kAndroidHome] |
| // See https://github.com/flutter/flutter/issues/39301 |
| messages.add(ValidationMessage.error(_userMessages.androidMissingSdkInstructions(kAndroidSdkRoot))); |
| } |
| return ValidationResult(ValidationType.missing, messages); |
| } |
| |
| if (_androidSdk.licensesAvailable && !_androidSdk.platformToolsAvailable) { |
| messages.add(ValidationMessage.hint(_userMessages.androidSdkLicenseOnly(kAndroidHome))); |
| return ValidationResult(ValidationType.partial, messages); |
| } |
| |
| messages.add(ValidationMessage(_userMessages.androidSdkLocation(_androidSdk.directory))); |
| |
| messages.add(ValidationMessage(_androidSdk.ndk == null |
| ? _userMessages.androidMissingNdk |
| : _userMessages.androidNdkLocation(_androidSdk.ndk.directory))); |
| |
| String sdkVersionText; |
| if (_androidSdk.latestVersion != null) { |
| if (_androidSdk.latestVersion.sdkLevel < 28 || _androidSdk.latestVersion.buildToolsVersion < kAndroidSdkBuildToolsMinVersion) { |
| messages.add(ValidationMessage.error( |
| _userMessages.androidSdkBuildToolsOutdated(_androidSdk.sdkManagerPath, kAndroidSdkMinVersion, kAndroidSdkBuildToolsMinVersion.toString())), |
| ); |
| return ValidationResult(ValidationType.missing, messages); |
| } |
| sdkVersionText = _userMessages.androidStatusInfo(_androidSdk.latestVersion.buildToolsVersionName); |
| |
| messages.add(ValidationMessage(_userMessages.androidSdkPlatformToolsVersion( |
| _androidSdk.latestVersion.platformName, |
| _androidSdk.latestVersion.buildToolsVersionName))); |
| } else { |
| messages.add(ValidationMessage.error(_userMessages.androidMissingSdkInstructions(kAndroidHome))); |
| } |
| |
| if (_platform.environment.containsKey(kAndroidHome)) { |
| final String androidHomeDir = _platform.environment[kAndroidHome]; |
| messages.add(ValidationMessage('$kAndroidHome = $androidHomeDir')); |
| } |
| if (_platform.environment.containsKey(kAndroidSdkRoot)) { |
| final String androidSdkRoot = _platform.environment[kAndroidSdkRoot]; |
| messages.add(ValidationMessage('$kAndroidSdkRoot = $androidSdkRoot')); |
| } |
| |
| final List<String> validationResult = _androidSdk.validateSdkWellFormed(); |
| |
| if (validationResult.isNotEmpty) { |
| // Android SDK is not functional. |
| messages.addAll(validationResult.map<ValidationMessage>((String message) { |
| return ValidationMessage.error(message); |
| })); |
| messages.add(ValidationMessage(_userMessages.androidSdkInstallHelp)); |
| return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText); |
| } |
| |
| // Now check for the JDK. |
| final String javaBinary = AndroidSdk.findJavaBinary( |
| androidStudio: _androidStudio, |
| fileSystem: _fileSystem, |
| operatingSystemUtils: _operatingSystemUtils, |
| platform: _platform, |
| ); |
| if (javaBinary == null) { |
| messages.add(ValidationMessage.error(_userMessages.androidMissingJdk)); |
| return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText); |
| } |
| messages.add(ValidationMessage(_userMessages.androidJdkLocation(javaBinary))); |
| |
| // Check JDK version. |
| if (! await _checkJavaVersion(javaBinary, messages)) { |
| return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText); |
| } |
| |
| // Success. |
| return ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText); |
| } |
| } |
| |
| class AndroidLicenseValidator extends DoctorValidator { |
| AndroidLicenseValidator() : super('Android license subvalidator',); |
| |
| @override |
| String get slowWarning => 'Checking Android licenses is taking an unexpectedly long time...'; |
| |
| @override |
| Future<ValidationResult> validate() async { |
| final List<ValidationMessage> messages = <ValidationMessage>[]; |
| |
| // Match pre-existing early termination behavior |
| if (androidSdk == null || androidSdk.latestVersion == null || |
| androidSdk.validateSdkWellFormed().isNotEmpty || |
| ! await _checkJavaVersionNoOutput()) { |
| return ValidationResult(ValidationType.missing, messages); |
| } |
| |
| final String sdkVersionText = userMessages.androidStatusInfo(androidSdk.latestVersion.buildToolsVersionName); |
| |
| // Check for licenses. |
| switch (await licensesAccepted) { |
| case LicensesAccepted.all: |
| messages.add(ValidationMessage(userMessages.androidLicensesAll)); |
| break; |
| case LicensesAccepted.some: |
| messages.add(ValidationMessage.hint(userMessages.androidLicensesSome)); |
| return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText); |
| case LicensesAccepted.none: |
| messages.add(ValidationMessage.error(userMessages.androidLicensesNone)); |
| return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText); |
| case LicensesAccepted.unknown: |
| messages.add(ValidationMessage.error(userMessages.androidLicensesUnknown)); |
| return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText); |
| } |
| return ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText); |
| } |
| |
| Future<bool> _checkJavaVersionNoOutput() async { |
| final String javaBinary = AndroidSdk.findJavaBinary( |
| androidStudio: globals.androidStudio, |
| fileSystem: globals.fs, |
| operatingSystemUtils: globals.os, |
| platform: globals.platform, |
| ); |
| if (javaBinary == null) { |
| return false; |
| } |
| if (!globals.processManager.canRun(javaBinary)) { |
| return false; |
| } |
| String javaVersion; |
| try { |
| final ProcessResult result = await globals.processManager.run(<String>[javaBinary, '-version']); |
| if (result.exitCode == 0) { |
| final List<String> versionLines = (result.stderr as String).split('\n'); |
| javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0]; |
| } |
| } on Exception catch (error) { |
| globals.printTrace(error.toString()); |
| } |
| if (javaVersion == null) { |
| // Could not determine the java version. |
| return false; |
| } |
| return true; |
| } |
| |
| Future<LicensesAccepted> get licensesAccepted async { |
| LicensesAccepted status; |
| |
| void _handleLine(String line) { |
| if (licenseCounts.hasMatch(line)) { |
| final Match match = licenseCounts.firstMatch(line); |
| if (match.group(1) != match.group(2)) { |
| status = LicensesAccepted.some; |
| } else { |
| status = LicensesAccepted.none; |
| } |
| } else if (licenseNotAccepted.hasMatch(line)) { |
| // The licenseNotAccepted pattern is trying to match the same line as |
| // licenseCounts, but is more general. In case the format changes, a |
| // more general match may keep doctor mostly working. |
| status = LicensesAccepted.none; |
| } else if (licenseAccepted.hasMatch(line)) { |
| status ??= LicensesAccepted.all; |
| } |
| } |
| |
| if (!_canRunSdkManager()) { |
| return LicensesAccepted.unknown; |
| } |
| |
| try { |
| final Process process = await processUtils.start( |
| <String>[androidSdk.sdkManagerPath, '--licenses'], |
| environment: androidSdk.sdkManagerEnv, |
| ); |
| process.stdin.write('n\n'); |
| // We expect logcat streams to occasionally contain invalid utf-8, |
| // see: https://github.com/flutter/flutter/pull/8864. |
| final Future<void> output = process.stdout |
| .transform<String>(const Utf8Decoder(reportErrors: false)) |
| .transform<String>(const LineSplitter()) |
| .listen(_handleLine) |
| .asFuture<void>(null); |
| final Future<void> errors = process.stderr |
| .transform<String>(const Utf8Decoder(reportErrors: false)) |
| .transform<String>(const LineSplitter()) |
| .listen(_handleLine) |
| .asFuture<void>(null); |
| await Future.wait<void>(<Future<void>>[output, errors]); |
| return status ?? LicensesAccepted.unknown; |
| } on ProcessException catch (e) { |
| globals.printTrace('Failed to run Android sdk manager: $e'); |
| return LicensesAccepted.unknown; |
| } |
| } |
| |
| /// Run the Android SDK manager tool in order to accept SDK licenses. |
| static Future<bool> runLicenseManager() async { |
| if (androidSdk == null) { |
| globals.printStatus(userMessages.androidSdkShort); |
| return false; |
| } |
| |
| if (!_canRunSdkManager()) { |
| throwToolExit(userMessages.androidMissingSdkManager(androidSdk.sdkManagerPath)); |
| } |
| |
| try { |
| final Process process = await processUtils.start( |
| <String>[androidSdk.sdkManagerPath, '--licenses'], |
| environment: androidSdk.sdkManagerEnv, |
| ); |
| |
| // The real stdin will never finish streaming. Pipe until the child process |
| // finishes. |
| unawaited(process.stdin.addStream(globals.stdio.stdin) |
| // If the process exits unexpectedly with an error, that will be |
| // handled by the caller. |
| .catchError((dynamic err, StackTrace stack) { |
| globals.printTrace('Echoing stdin to the licenses subprocess failed:'); |
| globals.printTrace('$err\n$stack'); |
| } |
| )); |
| |
| // Wait for stdout and stderr to be fully processed, because process.exitCode |
| // may complete first. |
| try { |
| await waitGroup<void>(<Future<void>>[ |
| globals.stdio.addStdoutStream(process.stdout), |
| globals.stdio.addStderrStream(process.stderr), |
| ]); |
| } on Exception catch (err, stack) { |
| globals.printTrace('Echoing stdout or stderr from the license subprocess failed:'); |
| globals.printTrace('$err\n$stack'); |
| } |
| |
| final int exitCode = await process.exitCode; |
| return exitCode == 0; |
| } on ProcessException catch (e) { |
| throwToolExit(userMessages.androidCannotRunSdkManager( |
| androidSdk.sdkManagerPath, |
| e.toString(), |
| )); |
| return false; |
| } |
| } |
| |
| static bool _canRunSdkManager() { |
| assert(androidSdk != null); |
| final String sdkManagerPath = androidSdk.sdkManagerPath; |
| return globals.processManager.canRun(sdkManagerPath); |
| } |
| } |