Improvements to flutter doctor JDK search. (#8745)

- [x] Add custom logic on MacOS to determine if Java is installed before invoking `java`.
- [x] Check JAVA_HOME, platform specific logic, and finally PATH to locate the `java` executable.
- [x] Improved doctor messages.

Fixes #8508
Fixes #8521
diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart
index e9977a3..ba332f4 100644
--- a/packages/flutter_tools/lib/src/android/android_workflow.dart
+++ b/packages/flutter_tools/lib/src/android/android_workflow.dart
@@ -5,7 +5,10 @@
 import 'dart:async';
 
 import '../base/io.dart';
+import '../base/file_system.dart';
+import '../base/os.dart';
 import '../base/platform.dart';
+import '../base/process.dart';
 import '../base/process_manager.dart';
 import '../doctor.dart';
 import '../globals.dart';
@@ -23,13 +26,71 @@
   @override
   bool get canLaunchDevices => androidSdk != null && androidSdk.validateSdkWellFormed().isEmpty;
 
+  static const String _kJavaHomeEnvironmentVariable = 'JAVA_HOME';
+  static const String _kJavaExecutable = 'java';
+  static const String _kJdkDownload = 'https://www.oracle.com/technetwork/java/javase/downloads/';
+
+  /// First sniff JAVA_HOME, then fallback to PATH.
+  String _findJavaBinary() {
+
+    final String javaHomeEnv = platform.environment[_kJavaHomeEnvironmentVariable];
+    if (javaHomeEnv != null) {
+      // Trust JAVA_HOME.
+      return fs.path.join(javaHomeEnv, 'bin', 'java');
+    }
+
+    // MacOS specific logic to avoid popping up a dialog window.
+    // See: http://stackoverflow.com/questions/14292698/how-do-i-check-if-the-java-jdk-is-installed-on-mac.
+    if (platform.isMacOS) {
+      try {
+        final String javaHomeOutput = runCheckedSync(<String>['/usr/libexec/java_home'], hideStdout: true);
+        if (javaHomeOutput != null) {
+          final List<String> javaHomeOutputSplit = javaHomeOutput.split('\n');
+          if ((javaHomeOutputSplit != null) && (javaHomeOutputSplit.length > 0)) {
+            final String javaHome = javaHomeOutputSplit[0].trim();
+            return fs.path.join(javaHome, 'bin', 'java');
+          }
+        }
+      } catch (_) { /* ignore */ }
+    }
+
+    // Fallback to PATH based lookup.
+    return os.which(_kJavaExecutable)?.path;
+  }
+
+  /// Returns false if we cannot determine the Java version or if the version
+  /// is not compatible.
+  bool _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) {
+    if (!processManager.canRun(javaBinary)) {
+      messages.add(new ValidationMessage.error('Cannot execute $javaBinary to determine the version'));
+      return false;
+    }
+    String javaVersion;
+    try {
+      printTrace('java -version');
+      final ProcessResult result = processManager.runSync(<String>[javaBinary, '-version']);
+      if (result.exitCode == 0) {
+        final List<String> versionLines = result.stderr.split('\n');
+        javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
+      }
+    } catch (_) { /* ignore */ }
+    if (javaVersion == null) {
+      // Could not determine the java version.
+      messages.add(new ValidationMessage.error('Could not determine java version'));
+      return false;
+    }
+    messages.add(new ValidationMessage('Java version: $javaVersion'));
+    // TODO(johnmccutchan): Validate version.
+    return true;
+  }
+
+
   @override
   Future<ValidationResult> validate() async {
     final List<ValidationMessage> messages = <ValidationMessage>[];
-    ValidationType type = ValidationType.missing;
-    String sdkVersionText;
 
     if (androidSdk == null) {
+      // No Android SDK found.
       if (platform.environment.containsKey(kAndroidHome)) {
         final String androidHomeDir = platform.environment[kAndroidHome];
         messages.add(new ValidationMessage.error(
@@ -42,63 +103,57 @@
           '(or visit https://flutter.io/setup/#android-setup for detailed instructions).'
         ));
       }
-    } else {
-      type = ValidationType.partial;
 
-      messages.add(new ValidationMessage('Android SDK at ${androidSdk.directory}'));
-
-      if (androidSdk.latestVersion != null) {
-        sdkVersionText = 'Android SDK ${androidSdk.latestVersion.buildToolsVersionName}';
-
-        messages.add(new ValidationMessage(
-          'Platform ${androidSdk.latestVersion.platformVersionName}, '
-          'build-tools ${androidSdk.latestVersion.buildToolsVersionName}'
-        ));
-      }
-
-      if (platform.environment.containsKey(kAndroidHome)) {
-        final String androidHomeDir = platform.environment[kAndroidHome];
-        messages.add(new ValidationMessage('$kAndroidHome = $androidHomeDir'));
-      }
-
-      final List<String> validationResult = androidSdk.validateSdkWellFormed();
-
-      if (validationResult.isEmpty) {
-        // Empty result means SDK is well formed.
-        // The SDK also requires a valid Java JDK installation.
-        const String _kJdkDownload = 'https://www.oracle.com/technetwork/java/javase/downloads/';
-        String javaVersion;
-
-        try {
-          printTrace('java -version');
-
-          final ProcessResult result = processManager.runSync(<String>['java', '-version']);
-          if (result.exitCode == 0) {
-            javaVersion = result.stderr;
-            final List<String> versionLines = javaVersion.split('\n');
-            javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
-           }
-        } catch (error) {
-        }
-
-        if (javaVersion == null) {
-          messages.add(new ValidationMessage.error(
-              'No Java Development Kit (JDK) found; you can download the JDK from $_kJdkDownload.'
-          ));
-        } else {
-          messages.add(new ValidationMessage(javaVersion));
-          type = ValidationType.installed;
-        }
-      } else {
-        messages.addAll(validationResult.map((String message) {
-          return new ValidationMessage.error(message);
-        }));
-        messages.add(new ValidationMessage(
-          'Try re-installing or updating your Android SDK,\n'
-          'visit https://flutter.io/setup/#android-setup for detailed instructions.'));
-      }
+      return new ValidationResult(ValidationType.missing, messages);
     }
 
-    return new ValidationResult(type, messages, statusInfo: sdkVersionText);
+    messages.add(new ValidationMessage('Android SDK at ${androidSdk.directory}'));
+
+    String sdkVersionText;
+    if (androidSdk.latestVersion != null) {
+      sdkVersionText = 'Android SDK ${androidSdk.latestVersion.buildToolsVersionName}';
+
+      messages.add(new ValidationMessage(
+        'Platform ${androidSdk.latestVersion.platformVersionName}, '
+        'build-tools ${androidSdk.latestVersion.buildToolsVersionName}'
+      ));
+    }
+
+    if (platform.environment.containsKey(kAndroidHome)) {
+      final String androidHomeDir = platform.environment[kAndroidHome];
+      messages.add(new ValidationMessage('$kAndroidHome = $androidHomeDir'));
+    }
+
+    final List<String> validationResult = androidSdk.validateSdkWellFormed();
+
+    if (validationResult.isNotEmpty) {
+      // Android SDK is not functional.
+      messages.addAll(validationResult.map((String message) {
+        return new ValidationMessage.error(message);
+      }));
+      messages.add(new ValidationMessage(
+          'Try re-installing or updating your Android SDK,\n'
+          'visit https://flutter.io/setup/#android-setup for detailed instructions.'));
+      return new ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
+    }
+
+    // Now check for the JDK.
+    final String javaBinary = _findJavaBinary();
+    if (javaBinary == null) {
+      messages.add(new ValidationMessage.error(
+          'No Java Development Kit (JDK) found; You must have the environment '
+          'variable JAVA_HOME set and the java binary in your PATH. '
+          'You can download the JDK from $_kJdkDownload.'));
+      return new ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
+    }
+    messages.add(new ValidationMessage('Java binary at: $javaBinary'));
+
+    // Check JDK version.
+    if (!_checkJavaVersion(javaBinary, messages)) {
+      return new ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
+    }
+
+    // Success.
+    return new ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText);
   }
 }