Detect Android SDK 28/28.0.3 (#26942)

* Detect Android SDK 28/28.0.3

* revert minimumAndroidSdkVersion const change
diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart
index d1978a2..dfbc083 100644
--- a/packages/flutter_tools/lib/src/android/android_workflow.dart
+++ b/packages/flutter_tools/lib/src/android/android_workflow.dart
@@ -18,6 +18,9 @@
 import '../globals.dart';
 import 'android_sdk.dart';
 
+const int kAndroidSdkMinVersion = 28;
+final Version kAndroidSdkBuildToolsMinVersion = Version(28, 0, 3);
+
 AndroidWorkflow get androidWorkflow => context[AndroidWorkflow];
 AndroidValidator get androidValidator => context[AndroidValidator];
 AndroidLicenseValidator get androidLicenseValidator => context[AndroidLicenseValidator];
@@ -110,11 +113,19 @@
 
     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)) {
@@ -271,7 +282,7 @@
     final Version sdkManagerVersion = Version.parse(androidSdk.sdkManagerVersion);
     if (sdkManagerVersion == null || sdkManagerVersion.major < 26) {
       // SDK manager is found, but needs to be updated.
-      throwToolExit(userMessages.androidSdkOutdated(androidSdk.sdkManagerPath));
+      throwToolExit(userMessages.androidSdkManagerOutdated(androidSdk.sdkManagerPath));
     }
 
     final Process process = await runCommand(
diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart
index 2248d11..813247c 100644
--- a/packages/flutter_tools/lib/src/base/user_messages.dart
+++ b/packages/flutter_tools/lib/src/base/user_messages.dart
@@ -76,7 +76,7 @@
   String get androidLicensesSome => 'Some Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses';
   String get androidLicensesNone => 'Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses';
   String get androidLicensesUnknown => 'Android license status unknown.';
-  String androidSdkOutdated(String managerPath) =>
+  String androidSdkManagerOutdated(String managerPath) =>
       'A newer version of the Android SDK is required. To update, run:\n'
       '$managerPath --update\n';
   String androidLicensesTimeout(String managerPath) => 'Intentionally killing $managerPath';
@@ -85,6 +85,11 @@
       'Android sdkmanager tool not found ($sdkManagerPath).\n'
       'Try re-installing or updating your Android SDK,\n'
       'visit https://flutter.io/setup/#android-setup for detailed instructions.';
+  String androidSdkBuildToolsOutdated(String managerPath, int sdkMinVersion, String buildToolsMinVersion) =>
+      'Flutter requires Android SDK $sdkMinVersion and the Android BuildTools $buildToolsMinVersion\n'
+      'To update using sdkmanager, run:\n'
+      '  $managerPath "platforms;android-$sdkMinVersion" "build-tools;$buildToolsMinVersion"\n'
+      'or visit https://flutter.io/setup/#android-setup for detailed instructions.';
 
   // Messages used in AndroidStudioValidator
   String androidStudioVersion(String version) => 'version $version';
diff --git a/packages/flutter_tools/test/android/android_workflow_test.dart b/packages/flutter_tools/test/android/android_workflow_test.dart
index 2cb57c0..4764b1b 100644
--- a/packages/flutter_tools/test/android/android_workflow_test.dart
+++ b/packages/flutter_tools/test/android/android_workflow_test.dart
@@ -9,6 +9,9 @@
 import 'package:flutter_tools/src/base/io.dart';
 import 'package:flutter_tools/src/android/android_sdk.dart';
 import 'package:flutter_tools/src/android/android_workflow.dart';
+import 'package:flutter_tools/src/base/user_messages.dart';
+import 'package:flutter_tools/src/base/version.dart';
+import 'package:flutter_tools/src/doctor.dart';
 import 'package:mockito/mockito.dart';
 import 'package:platform/platform.dart';
 import 'package:process/process.dart';
@@ -17,6 +20,8 @@
 import '../src/context.dart';
 import '../src/mocks.dart' show MockAndroidSdk, MockProcess, MockProcessManager, MockStdio;
 
+class MockAndroidSdkVersion extends Mock implements AndroidSdkVersion {}
+
 void main() {
   AndroidSdk sdk;
   MemoryFileSystem fs;
@@ -172,4 +177,57 @@
     ProcessManager: () => processManager,
     Stdio: () => stdio,
   });
+
+  testUsingContext('detects minium required SDK and buildtools', () async {
+    final AndroidSdkVersion mockSdkVersion = MockAndroidSdkVersion();
+
+    // Test with invalid SDK and build tools
+    when(mockSdkVersion.sdkLevel).thenReturn(26);
+    when(mockSdkVersion.buildToolsVersion).thenReturn(Version(26, 0, 3));
+    when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
+    when(sdk.latestVersion).thenReturn(mockSdkVersion);
+    when(sdk.validateSdkWellFormed()).thenReturn(<String>[]);
+    final String errorMessage = userMessages.androidSdkBuildToolsOutdated(
+      sdk.sdkManagerPath,
+      kAndroidSdkMinVersion,
+      kAndroidSdkBuildToolsMinVersion.toString(),
+    );
+
+    ValidationResult validationResult = await AndroidValidator().validate();
+    expect(validationResult.type, ValidationType.missing);
+    expect(
+      validationResult.messages.last.message,
+      errorMessage,
+    );
+
+    // Test with valid SDK but invalid build tools
+    when(mockSdkVersion.sdkLevel).thenReturn(28);
+    when(mockSdkVersion.buildToolsVersion).thenReturn(Version(28, 0, 2));
+
+    validationResult = await AndroidValidator().validate();
+    expect(validationResult.type, ValidationType.missing);
+    expect(
+      validationResult.messages.last.message,
+      errorMessage,
+    );
+
+    // Test with valid SDK and valid build tools
+    // Will still be partial because AnroidSdk.findJavaBinary is static :(
+    when(mockSdkVersion.sdkLevel).thenReturn(kAndroidSdkMinVersion);
+    when(mockSdkVersion.buildToolsVersion).thenReturn(kAndroidSdkBuildToolsMinVersion);
+
+    validationResult = await AndroidValidator().validate();
+    expect(validationResult.type, ValidationType.partial); // No Java binary
+    expect(
+      validationResult.messages.any((ValidationMessage message) => message.message == errorMessage),
+      isFalse,
+    );
+  }, overrides: <Type, Generator>{
+    AndroidSdk: () => sdk,
+    FileSystem: () => fs,
+    Platform: () => FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
+    ProcessManager: () => processManager,
+    Stdio: () => stdio,
+  });
+
 }