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 '../base/common.dart'; 8import '../base/context.dart'; 9import '../base/io.dart'; 10import '../base/platform.dart'; 11import '../base/process.dart'; 12import '../base/process_manager.dart'; 13import '../base/user_messages.dart'; 14import '../base/utils.dart'; 15import '../base/version.dart'; 16import '../convert.dart'; 17import '../doctor.dart'; 18import '../globals.dart'; 19import 'android_sdk.dart'; 20 21const int kAndroidSdkMinVersion = 28; 22final Version kAndroidSdkBuildToolsMinVersion = Version(28, 0, 3); 23 24AndroidWorkflow get androidWorkflow => context.get<AndroidWorkflow>(); 25AndroidValidator get androidValidator => context.get<AndroidValidator>(); 26AndroidLicenseValidator get androidLicenseValidator => context.get<AndroidLicenseValidator>(); 27 28enum LicensesAccepted { 29 none, 30 some, 31 all, 32 unknown, 33} 34 35final RegExp licenseCounts = RegExp(r'(\d+) of (\d+) SDK package licenses? not accepted.'); 36final RegExp licenseNotAccepted = RegExp(r'licenses? not accepted', caseSensitive: false); 37final RegExp licenseAccepted = RegExp(r'All SDK package licenses accepted.'); 38 39class AndroidWorkflow implements Workflow { 40 @override 41 bool get appliesToHostPlatform => true; 42 43 @override 44 bool get canListDevices => getAdbPath(androidSdk) != null; 45 46 @override 47 bool get canLaunchDevices => androidSdk != null && androidSdk.validateSdkWellFormed().isEmpty; 48 49 @override 50 bool get canListEmulators => getEmulatorPath(androidSdk) != null; 51} 52 53class AndroidValidator extends DoctorValidator { 54 AndroidValidator() : super('Android toolchain - develop for Android devices',); 55 56 @override 57 String get slowWarning => '${_task ?? 'This'} is taking a long time...'; 58 String _task; 59 60 /// Returns false if we cannot determine the Java version or if the version 61 /// is not compatible. 62 Future<bool> _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) async { 63 _task = 'Checking Java status'; 64 try { 65 if (!processManager.canRun(javaBinary)) { 66 messages.add(ValidationMessage.error(userMessages.androidCantRunJavaBinary(javaBinary))); 67 return false; 68 } 69 String javaVersion; 70 try { 71 printTrace('java -version'); 72 final ProcessResult result = await processManager.run(<String>[javaBinary, '-version']); 73 if (result.exitCode == 0) { 74 final List<String> versionLines = result.stderr.split('\n'); 75 javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0]; 76 } 77 } catch (error) { 78 printTrace(error.toString()); 79 } 80 if (javaVersion == null) { 81 // Could not determine the java version. 82 messages.add(ValidationMessage.error(userMessages.androidUnknownJavaVersion)); 83 return false; 84 } 85 messages.add(ValidationMessage(userMessages.androidJavaVersion(javaVersion))); 86 // TODO(johnmccutchan): Validate version. 87 return true; 88 } finally { 89 _task = null; 90 } 91 } 92 93 @override 94 Future<ValidationResult> validate() async { 95 final List<ValidationMessage> messages = <ValidationMessage>[]; 96 97 if (androidSdk == null) { 98 // No Android SDK found. 99 if (platform.environment.containsKey(kAndroidHome)) { 100 final String androidHomeDir = platform.environment[kAndroidHome]; 101 messages.add(ValidationMessage.error(userMessages.androidBadSdkDir(kAndroidHome, androidHomeDir))); 102 } else { 103 messages.add(ValidationMessage.error(userMessages.androidMissingSdkInstructions(kAndroidHome))); 104 } 105 return ValidationResult(ValidationType.missing, messages); 106 } 107 108 if (androidSdk.licensesAvailable && !androidSdk.platformToolsAvailable) { 109 messages.add(ValidationMessage.hint(userMessages.androidSdkLicenseOnly(kAndroidHome))); 110 return ValidationResult(ValidationType.partial, messages); 111 } 112 113 messages.add(ValidationMessage(userMessages.androidSdkLocation(androidSdk.directory))); 114 115 messages.add(ValidationMessage(androidSdk.ndk == null 116 ? userMessages.androidMissingNdk 117 : userMessages.androidNdkLocation(androidSdk.ndk.directory))); 118 119 String sdkVersionText; 120 if (androidSdk.latestVersion != null) { 121 if (androidSdk.latestVersion.sdkLevel < 28 || androidSdk.latestVersion.buildToolsVersion < kAndroidSdkBuildToolsMinVersion) { 122 messages.add(ValidationMessage.error( 123 userMessages.androidSdkBuildToolsOutdated(androidSdk.sdkManagerPath, kAndroidSdkMinVersion, kAndroidSdkBuildToolsMinVersion.toString())), 124 ); 125 return ValidationResult(ValidationType.missing, messages); 126 } 127 sdkVersionText = userMessages.androidStatusInfo(androidSdk.latestVersion.buildToolsVersionName); 128 129 messages.add(ValidationMessage(userMessages.androidSdkPlatformToolsVersion( 130 androidSdk.latestVersion.platformName, 131 androidSdk.latestVersion.buildToolsVersionName))); 132 } else { 133 messages.add(ValidationMessage.error(userMessages.androidMissingSdkInstructions(kAndroidHome))); 134 } 135 136 if (platform.environment.containsKey(kAndroidHome)) { 137 final String androidHomeDir = platform.environment[kAndroidHome]; 138 messages.add(ValidationMessage('$kAndroidHome = $androidHomeDir')); 139 } 140 if (platform.environment.containsKey(kAndroidSdkRoot)) { 141 final String androidSdkRoot = platform.environment[kAndroidSdkRoot]; 142 messages.add(ValidationMessage('$kAndroidSdkRoot = $androidSdkRoot')); 143 } 144 145 final List<String> validationResult = androidSdk.validateSdkWellFormed(); 146 147 if (validationResult.isNotEmpty) { 148 // Android SDK is not functional. 149 messages.addAll(validationResult.map<ValidationMessage>((String message) { 150 return ValidationMessage.error(message); 151 })); 152 messages.add(ValidationMessage(userMessages.androidSdkInstallHelp)); 153 return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText); 154 } 155 156 // Now check for the JDK. 157 final String javaBinary = AndroidSdk.findJavaBinary(); 158 if (javaBinary == null) { 159 messages.add(ValidationMessage.error(userMessages.androidMissingJdk)); 160 return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText); 161 } 162 messages.add(ValidationMessage(userMessages.androidJdkLocation(javaBinary))); 163 164 // Check JDK version. 165 if (! await _checkJavaVersion(javaBinary, messages)) { 166 return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText); 167 } 168 169 // Success. 170 return ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText); 171 } 172} 173 174class AndroidLicenseValidator extends DoctorValidator { 175 AndroidLicenseValidator() : super('Android license subvalidator',); 176 177 @override 178 String get slowWarning => 'Checking Android licenses is taking an unexpectedly long time...'; 179 180 @override 181 Future<ValidationResult> validate() async { 182 final List<ValidationMessage> messages = <ValidationMessage>[]; 183 184 // Match pre-existing early termination behavior 185 if (androidSdk == null || androidSdk.latestVersion == null || 186 androidSdk.validateSdkWellFormed().isNotEmpty || 187 ! await _checkJavaVersionNoOutput()) { 188 return ValidationResult(ValidationType.missing, messages); 189 } 190 191 final String sdkVersionText = userMessages.androidStatusInfo(androidSdk.latestVersion.buildToolsVersionName); 192 193 // Check for licenses. 194 switch (await licensesAccepted) { 195 case LicensesAccepted.all: 196 messages.add(ValidationMessage(userMessages.androidLicensesAll)); 197 break; 198 case LicensesAccepted.some: 199 messages.add(ValidationMessage.hint(userMessages.androidLicensesSome)); 200 return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText); 201 case LicensesAccepted.none: 202 messages.add(ValidationMessage.error(userMessages.androidLicensesNone)); 203 return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText); 204 case LicensesAccepted.unknown: 205 messages.add(ValidationMessage.error(userMessages.androidLicensesUnknown)); 206 return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText); 207 } 208 return ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText); 209 } 210 211 Future<bool> _checkJavaVersionNoOutput() async { 212 final String javaBinary = AndroidSdk.findJavaBinary(); 213 if (javaBinary == null) { 214 return false; 215 } 216 if (!processManager.canRun(javaBinary)) { 217 return false; 218 } 219 String javaVersion; 220 try { 221 final ProcessResult result = await processManager.run(<String>[javaBinary, '-version']); 222 if (result.exitCode == 0) { 223 final List<String> versionLines = result.stderr.split('\n'); 224 javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0]; 225 } 226 } catch (error) { 227 printTrace(error.toString()); 228 } 229 if (javaVersion == null) { 230 // Could not determine the java version. 231 return false; 232 } 233 return true; 234 } 235 236 Future<LicensesAccepted> get licensesAccepted async { 237 LicensesAccepted status; 238 239 void _handleLine(String line) { 240 if (licenseCounts.hasMatch(line)) { 241 final Match match = licenseCounts.firstMatch(line); 242 if (match.group(1) != match.group(2)) { 243 status = LicensesAccepted.some; 244 } else { 245 status = LicensesAccepted.none; 246 } 247 } else if (licenseNotAccepted.hasMatch(line)) { 248 // The licenseNotAccepted pattern is trying to match the same line as 249 // licenseCounts, but is more general. In case the format changes, a 250 // more general match may keep doctor mostly working. 251 status = LicensesAccepted.none; 252 } else if (licenseAccepted.hasMatch(line)) { 253 status ??= LicensesAccepted.all; 254 } 255 } 256 257 if (!_canRunSdkManager()) { 258 return LicensesAccepted.unknown; 259 } 260 261 try { 262 final Process process = await runCommand( 263 <String>[androidSdk.sdkManagerPath, '--licenses'], 264 environment: androidSdk.sdkManagerEnv, 265 ); 266 process.stdin.write('n\n'); 267 // We expect logcat streams to occasionally contain invalid utf-8, 268 // see: https://github.com/flutter/flutter/pull/8864. 269 final Future<void> output = process.stdout 270 .transform<String>(const Utf8Decoder(reportErrors: false)) 271 .transform<String>(const LineSplitter()) 272 .listen(_handleLine) 273 .asFuture<void>(null); 274 final Future<void> errors = process.stderr 275 .transform<String>(const Utf8Decoder(reportErrors: false)) 276 .transform<String>(const LineSplitter()) 277 .listen(_handleLine) 278 .asFuture<void>(null); 279 await Future.wait<void>(<Future<void>>[output, errors]); 280 return status ?? LicensesAccepted.unknown; 281 } on ProcessException catch (e) { 282 printTrace('Failed to run Android sdk manager: $e'); 283 return LicensesAccepted.unknown; 284 } 285 } 286 287 /// Run the Android SDK manager tool in order to accept SDK licenses. 288 static Future<bool> runLicenseManager() async { 289 if (androidSdk == null) { 290 printStatus(userMessages.androidSdkShort); 291 return false; 292 } 293 294 if (!_canRunSdkManager()) { 295 throwToolExit(userMessages.androidMissingSdkManager(androidSdk.sdkManagerPath)); 296 } 297 298 final Version sdkManagerVersion = Version.parse(androidSdk.sdkManagerVersion); 299 if (sdkManagerVersion == null || sdkManagerVersion.major < 26) { 300 // SDK manager is found, but needs to be updated. 301 throwToolExit(userMessages.androidSdkManagerOutdated(androidSdk.sdkManagerPath)); 302 } 303 304 try { 305 final Process process = await runCommand( 306 <String>[androidSdk.sdkManagerPath, '--licenses'], 307 environment: androidSdk.sdkManagerEnv, 308 ); 309 310 // The real stdin will never finish streaming. Pipe until the child process 311 // finishes. 312 unawaited(process.stdin.addStream(stdin)); 313 // Wait for stdout and stderr to be fully processed, because process.exitCode 314 // may complete first. 315 await waitGroup<void>(<Future<void>>[ 316 stdout.addStream(process.stdout), 317 stderr.addStream(process.stderr), 318 ]); 319 320 final int exitCode = await process.exitCode; 321 return exitCode == 0; 322 } on ProcessException catch (e) { 323 throwToolExit(userMessages.androidCannotRunSdkManager( 324 androidSdk.sdkManagerPath, e.toString())); 325 return false; 326 } 327 } 328 329 static bool _canRunSdkManager() { 330 assert(androidSdk != null); 331 final String sdkManagerPath = androidSdk.sdkManagerPath; 332 return processManager.canRun(sdkManagerPath); 333 } 334} 335