Add support for NDK discovery and add --prefer-shared-library option (#12788)

* Add support for NDK discovery and add --prefer-shared-library option

We would like to be able to use native tools (e.g. simpleperf, gdb) with
precompiled flutter apps.  The native tools work much better with *.so
files instead of the custom formats the Dart VM uses by default.

The reason for using blobs / instruction snapshots is that we do not
want to force flutter users to install the Android NDK.

This CL adds a `--prefer-shared-library` flag to e.g. `flutter build
apk` which will use the NDK compiler (if available) to turn the
precompiled app assembly file to an `*.so` file.  If the NDK compiler is
not available it will default to the default behavior.

* Rebase, add test for NDK detection, augment flutter.gradle with @Input for flag

* Use InMemoryFileSystem for test

* Remove unused import

* Address some analyzer warnings
diff --git a/packages/flutter_tools/lib/src/android/android_sdk.dart b/packages/flutter_tools/lib/src/android/android_sdk.dart
index fb88234..a3946d5 100644
--- a/packages/flutter_tools/lib/src/android/android_sdk.dart
+++ b/packages/flutter_tools/lib/src/android/android_sdk.dart
@@ -57,63 +57,131 @@
 }
 
 class AndroidSdk {
-  AndroidSdk(this.directory) {
+  AndroidSdk(this.directory, [this.ndkDirectory, this.ndkCompiler,
+      this.ndkCompilerArgs]) {
     _init();
   }
 
+  /// The path to the Android SDK.
   final String directory;
 
+  /// The path to the NDK (can be `null`).
+  final String ndkDirectory;
+
+  /// The path to the NDK compiler (can be `null`).
+  final String ndkCompiler;
+
+  /// The mandatory arguments to the NDK compiler (can be `null`).
+  final List<String> ndkCompilerArgs;
+
   List<AndroidSdkVersion> _sdkVersions;
   AndroidSdkVersion _latestVersion;
 
   static AndroidSdk locateAndroidSdk() {
-    String androidHomeDir;
+    String findAndroidHomeDir() {
+      String androidHomeDir;
+      if (config.containsKey('android-sdk')) {
+        androidHomeDir = config.getValue('android-sdk');
+      } else if (platform.environment.containsKey(kAndroidHome)) {
+        androidHomeDir = platform.environment[kAndroidHome];
+      } else if (platform.isLinux) {
+        if (homeDirPath != null)
+          androidHomeDir = fs.path.join(homeDirPath, 'Android', 'Sdk');
+      } else if (platform.isMacOS) {
+        if (homeDirPath != null)
+          androidHomeDir = fs.path.join(homeDirPath, 'Library', 'Android', 'sdk');
+      } else if (platform.isWindows) {
+        if (homeDirPath != null)
+          androidHomeDir = fs.path.join(homeDirPath, 'AppData', 'Local', 'Android', 'sdk');
+      }
 
-    if (config.containsKey('android-sdk')) {
-      androidHomeDir = config.getValue('android-sdk');
-    } else if (platform.environment.containsKey(kAndroidHome)) {
-      androidHomeDir = platform.environment[kAndroidHome];
-    } else if (platform.isLinux) {
-      if (homeDirPath != null)
-        androidHomeDir = fs.path.join(homeDirPath, 'Android', 'Sdk');
-    } else if (platform.isMacOS) {
-      if (homeDirPath != null)
-        androidHomeDir = fs.path.join(homeDirPath, 'Library', 'Android', 'sdk');
-    } else if (platform.isWindows) {
-      if (homeDirPath != null)
-        androidHomeDir = fs.path.join(homeDirPath, 'AppData', 'Local', 'Android', 'sdk');
+      if (androidHomeDir != null) {
+        if (validSdkDirectory(androidHomeDir))
+          return androidHomeDir;
+        if (validSdkDirectory(fs.path.join(androidHomeDir, 'sdk')))
+          return fs.path.join(androidHomeDir, 'sdk');
+      }
+
+      // in build-tools/$version/aapt
+      final List<File> aaptBins = os.whichAll('aapt');
+      for (File aaptBin in aaptBins) {
+        // Make sure we're using the aapt from the SDK.
+        aaptBin = fs.file(aaptBin.resolveSymbolicLinksSync());
+        final String dir = aaptBin.parent.parent.parent.path;
+        if (validSdkDirectory(dir))
+          return dir;
+      }
+
+      // in platform-tools/adb
+      final List<File> adbBins = os.whichAll('adb');
+      for (File adbBin in adbBins) {
+        // Make sure we're using the adb from the SDK.
+        adbBin = fs.file(adbBin.resolveSymbolicLinksSync());
+        final String dir = adbBin.parent.parent.path;
+        if (validSdkDirectory(dir))
+          return dir;
+      }
+
+      return null;
     }
 
-    if (androidHomeDir != null) {
-      if (validSdkDirectory(androidHomeDir))
-        return new AndroidSdk(androidHomeDir);
-      if (validSdkDirectory(fs.path.join(androidHomeDir, 'sdk')))
-        return new AndroidSdk(fs.path.join(androidHomeDir, 'sdk'));
+    String findNdk(String androidHomeDir) {
+      final String ndkDirectory = fs.path.join(androidHomeDir, 'ndk-bundle');
+      if (fs.isDirectorySync(ndkDirectory)) {
+        return ndkDirectory;
+      }
+      return null;
     }
 
-    // in build-tools/$version/aapt
-    final List<File> aaptBins = os.whichAll('aapt');
-    for (File aaptBin in aaptBins) {
-      // Make sure we're using the aapt from the SDK.
-      aaptBin = fs.file(aaptBin.resolveSymbolicLinksSync());
-      final String dir = aaptBin.parent.parent.parent.path;
-      if (validSdkDirectory(dir))
-        return new AndroidSdk(dir);
+    String findNdkCompiler(String ndkDirectory) {
+      String directory;
+      if (platform.isLinux) {
+        directory = 'linux-x86_64';
+      } else if (platform.isMacOS) {
+        directory = 'darwin-x86_64';
+      }
+      if (directory != null) {
+        final String ndkCompiler = fs.path.join(ndkDirectory,
+            'toolchains', 'arm-linux-androideabi-4.9', 'prebuilt', directory,
+            'bin', 'arm-linux-androideabi-gcc');
+        if (fs.isFileSync(ndkCompiler)) {
+          return ndkCompiler;
+        }
+      }
+      return null;
     }
 
-    // in platform-tools/adb
-    final List<File> adbBins = os.whichAll('adb');
-    for (File adbBin in adbBins) {
-      // Make sure we're using the adb from the SDK.
-      adbBin = fs.file(adbBin.resolveSymbolicLinksSync());
-      final String dir = adbBin.parent.parent.path;
-      if (validSdkDirectory(dir))
-        return new AndroidSdk(dir);
+    List<String> computeNdkCompilerArgs(String ndkDirectory) {
+      final String armPlatform = fs.path.join(ndkDirectory, 'platforms',
+          'android-9', 'arch-arm');
+      if (fs.isDirectorySync(armPlatform)) {
+        return <String>['--sysroot', armPlatform];
+      }
+      return null;
     }
 
-    // No dice.
-    printTrace('Unable to locate an Android SDK.');
-    return null;
+    final String androidHomeDir = findAndroidHomeDir();
+    if (androidHomeDir == null) {
+      // No dice.
+      printTrace('Unable to locate an Android SDK.');
+      return null;
+    }
+
+    // Try to find the NDK compiler.  If we can't find it, it's also ok.
+    final String ndkDir = findNdk(androidHomeDir);
+    String ndkCompiler;
+    List<String> ndkCompilerArgs;
+    if (ndkDir != null) {
+      ndkCompiler = findNdkCompiler(ndkDir);
+      if (ndkCompiler != null) {
+        ndkCompilerArgs = computeNdkCompilerArgs(ndkDir);
+        if (ndkCompilerArgs == null) {
+          ndkCompiler = null;
+        }
+      }
+    }
+
+    return new AndroidSdk(androidHomeDir, ndkDir, ndkCompiler, ndkCompilerArgs);
   }
 
   static bool validSdkDirectory(String dir) {
diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart
index 1fc76ae..99d1f6f 100644
--- a/packages/flutter_tools/lib/src/android/android_workflow.dart
+++ b/packages/flutter_tools/lib/src/android/android_workflow.dart
@@ -119,6 +119,14 @@
 
     messages.add(new ValidationMessage('Android SDK at ${androidSdk.directory}'));
 
+    messages.add(new ValidationMessage(androidSdk.ndkDirectory == null
+          ? 'Unable to locate Android NDK.\n'
+          : 'Android NDK at ${androidSdk.ndkDirectory}'));
+
+    messages.add(new ValidationMessage(androidSdk.ndkCompiler == null
+          ? 'Unable to locate compiler in Android NDK.\n'
+          : 'Compiler in Android NDK at ${androidSdk.ndkCompiler}'));
+
     String sdkVersionText;
     if (androidSdk.latestVersion != null) {
       sdkVersionText = 'Android SDK ${androidSdk.latestVersion.buildToolsVersionName}';
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index bb930af..b3b5b74 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 
+import '../android/android_sdk.dart';
 import '../artifacts.dart';
 import '../base/common.dart';
 import '../base/file_system.dart';
@@ -289,12 +290,17 @@
   if (target != null) {
     command.add('-Ptarget=$target');
   }
-  if (buildInfo.previewDart2)
+  if (buildInfo.previewDart2) {
     command.add('-Ppreview-dart-2=true');
   if (buildInfo.extraFrontEndOptions != null)
     command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions}');
   if (buildInfo.extraGenSnapshotOptions != null)
     command.add('-Pextra-gen-snapshot-options=${buildInfo.extraGenSnapshotOptions}');
+  }
+  if (buildInfo.preferSharedLibrary && androidSdk.ndkCompiler != null) {
+    command.add('-Pprefer-shared-library=true');
+  }
+
   command.add(assembleTask);
   final int exitCode = await runCommandAndStreamOutput(
       command,