Add split-debug and obfuscation to build aar (#56342)

diff --git a/dev/devicelab/bin/tasks/android_obfuscate_test.dart b/dev/devicelab/bin/tasks/android_obfuscate_test.dart
index f1d4bcf..b087ba1 100644
--- a/dev/devicelab/bin/tasks/android_obfuscate_test.dart
+++ b/dev/devicelab/bin/tasks/android_obfuscate_test.dart
@@ -12,7 +12,7 @@
 Future<void> main() async {
   await task(() async {
     try {
-      bool foundProjectName = false;
+      bool foundApkProjectName = false;
       await runProjectTest((FlutterProject flutterProject) async {
         section('APK content for task assembleRelease with --obfuscate');
         await inDirectory(flutterProject.rootPath, () async {
@@ -21,13 +21,14 @@
             '--target-platform=android-arm',
             '--obfuscate',
             '--split-debug-info=foo/',
+            '--verbose',
           ]);
         });
-        final String outputDirectory = path.join(
+        final String outputApkDirectory = path.join(
           flutterProject.rootPath,
           'build/app/outputs/apk/release/app-release.apk',
         );
-        final Iterable<String> apkFiles = await getFilesInApk(outputDirectory);
+        final Iterable<String> apkFiles = await getFilesInApk(outputApkDirectory);
 
         checkCollectionContains<String>(<String>[
           ...flutterAssets,
@@ -38,20 +39,69 @@
         // Verify that an identifier from the Dart project code is not present
         // in the compiled binary.
         await inDirectory(flutterProject.rootPath, () async {
-          await exec('unzip', <String>[outputDirectory]);
+          await exec('unzip', <String>[outputApkDirectory]);
+          checkFileExists(path.join(flutterProject.rootPath, 'lib/armeabi-v7a/libapp.so'));
           final String response = await eval(
             'grep',
             <String>[flutterProject.name, 'lib/armeabi-v7a/libapp.so'],
             canFail: true,
           );
           if (response.trim().contains('matches')) {
-            foundProjectName = true;
+            foundApkProjectName = true;
           }
         });
       });
-      if (foundProjectName) {
-        return TaskResult.failure('Found project name in obfuscated dart library');
+
+      bool foundAarProjectName = false;
+      await runModuleProjectTest((FlutterModuleProject flutterProject) async {
+        section('AAR content with --obfuscate');
+
+        await inDirectory(flutterProject.rootPath, () async {
+          await flutter('build', options: <String>[
+            'aar',
+            '--target-platform=android-arm',
+            '--obfuscate',
+            '--split-debug-info=foo/',
+            '--no-debug',
+            '--no-profile',
+            '--verbose',
+          ]);
+        });
+
+        final String outputAarDirectory = path.join(
+          flutterProject.rootPath,
+          'build/host/outputs/repo/com/example/${flutterProject.name}/flutter_release/1.0/flutter_release-1.0.aar',
+        );
+        final Iterable<String> aarFiles = await getFilesInAar(outputAarDirectory);
+
+        checkCollectionContains<String>(<String>[
+          ...flutterAssets,
+          'jni/armeabi-v7a/libapp.so',
+        ], aarFiles);
+
+        // Verify that an identifier from the Dart project code is not present
+        // in the compiled binary.
+        await inDirectory(flutterProject.rootPath, () async {
+          await exec('unzip', <String>[outputAarDirectory]);
+          checkFileExists(path.join(flutterProject.rootPath, 'jni/armeabi-v7a/libapp.so'));
+          final String response = await eval(
+            'grep',
+            <String>[flutterProject.name, 'jni/armeabi-v7a/libapp.so'],
+            canFail: true,
+          );
+          if (response.trim().contains('matches')) {
+            foundAarProjectName = true;
+          }
+        });
+      });
+
+      if (foundApkProjectName) {
+        return TaskResult.failure('Found project name in obfuscated APK dart library');
       }
+      if (foundAarProjectName) {
+        return TaskResult.failure('Found project name in obfuscated AAR dart library');
+      }
+
       return TaskResult.success(null);
     } on TaskResult catch (taskResult) {
       return taskResult;
diff --git a/dev/devicelab/lib/framework/apk_utils.dart b/dev/devicelab/lib/framework/apk_utils.dart
index c6623f3..fc9b5ab 100644
--- a/dev/devicelab/lib/framework/apk_utils.dart
+++ b/dev/devicelab/lib/framework/apk_utils.dart
@@ -51,6 +51,18 @@
   }
 }
 
+/// Runs the given [testFunction] on a freshly generated Flutter module project.
+Future<void> runModuleProjectTest(Future<void> testFunction(FlutterModuleProject moduleProject)) async {
+  final Directory tempDir = Directory.systemTemp.createTempSync('flutter_devicelab_gradle_module_test.');
+  final FlutterModuleProject moduleProject = await FlutterModuleProject.create(tempDir, 'hello_module');
+
+  try {
+    await testFunction(moduleProject);
+  } finally {
+    rmTree(tempDir);
+  }
+}
+
 /// Returns the list of files inside an Android Package Kit.
 Future<Iterable<String>> getFilesInApk(String apk) async {
   if (!File(apk).existsSync()) {
@@ -357,6 +369,22 @@
   }
 }
 
+class FlutterModuleProject {
+  FlutterModuleProject(this.parent, this.name);
+
+  final Directory parent;
+  final String name;
+
+  static Future<FlutterModuleProject> create(Directory directory, String name) async {
+    await inDirectory(directory, () async {
+      await flutter('create', options: <String>['--template=module', name]);
+    });
+    return FlutterModuleProject(directory, name);
+  }
+
+  String get rootPath => path.join(parent.path, name);
+}
+
 Future<void> _runGradleTask({String workingDirectory, String task, List<String> options}) async {
   final ProcessResult result = await _resultOfGradleTask(
       workingDirectory: workingDirectory,
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index b491d3a..09a517d 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -525,7 +525,8 @@
     throwToolExit('AARs can only be built for plugin or module projects.');
   }
 
-  final String aarTask = getAarTaskFor(androidBuildInfo.buildInfo);
+  final BuildInfo buildInfo = androidBuildInfo.buildInfo;
+  final String aarTask = getAarTaskFor(buildInfo);
   final Status status = globals.logger.startProgress(
     "Running Gradle task '$aarTask'...",
     timeout: timeoutConfiguration.slowOperation,
@@ -548,10 +549,28 @@
     '-Pis-plugin=${manifest.isPlugin}',
     '-PbuildNumber=$buildNumber'
   ];
+  if (globals.logger.isVerbose) {
+    command.add('-Pverbose=true');
+  } else {
+    command.add('-q');
+  }
 
   if (target != null && target.isNotEmpty) {
     command.add('-Ptarget=$target');
   }
+  if (buildInfo.splitDebugInfoPath != null) {
+    command.add('-Psplit-debug-info=${buildInfo.splitDebugInfoPath}');
+  }
+  if (buildInfo.treeShakeIcons) {
+    command.add('-Pfont-subset=true');
+  }
+  if (buildInfo.dartObfuscation) {
+    if (buildInfo.mode == BuildMode.debug || buildInfo.mode == BuildMode.profile) {
+      globals.printStatus('Dart obfuscation is not supported in ${toTitleCase(buildInfo.friendlyModeName)} mode, building as unobfuscated.');
+    } else {
+      command.add('-Pdart-obfuscation=true');
+    }
+  }
 
   if (globals.artifacts is LocalEngineArtifacts) {
     final LocalEngineArtifacts localEngineArtifacts = globals.artifacts as LocalEngineArtifacts;
@@ -564,11 +583,8 @@
       'Local Maven repo: ${localEngineRepo.path}'
     );
     command.add('-Plocal-engine-repo=${localEngineRepo.path}');
-    command.add('-Plocal-engine-build-mode=${androidBuildInfo.buildInfo.modeName}');
+    command.add('-Plocal-engine-build-mode=${buildInfo.modeName}');
     command.add('-Plocal-engine-out=${localEngineArtifacts.engineOutPath}');
-    if (androidBuildInfo.buildInfo.treeShakeIcons) {
-      command.add('-Pfont-subset=true');
-    }
 
     // Copy the local engine repo in the output directory.
     try {
diff --git a/packages/flutter_tools/lib/src/commands/build_aar.dart b/packages/flutter_tools/lib/src/commands/build_aar.dart
index 9c7551a..f74a2b2 100644
--- a/packages/flutter_tools/lib/src/commands/build_aar.dart
+++ b/packages/flutter_tools/lib/src/commands/build_aar.dart
@@ -38,6 +38,8 @@
     usesFlavorOption();
     usesBuildNumberOption();
     usesPubOption();
+    addSplitDebugInfoOption();
+    addDartObfuscationOption();
     argParser
       ..addMultiOption(
         'target-platform',
@@ -104,15 +106,18 @@
 
     for (final String buildMode in const <String>['debug', 'profile', 'release']) {
       if (boolArg(buildMode)) {
-        androidBuildInfo.add(AndroidBuildInfo(
-          BuildInfo(BuildMode.fromName(buildMode), stringArg('flavor'), treeShakeIcons: boolArg('tree-shake-icons')),
-          targetArchs: targetArchitectures,
-        ));
+        androidBuildInfo.add(
+          AndroidBuildInfo(
+            getBuildInfo(forcedBuildMode: BuildMode.fromName(buildMode)),
+            targetArchs: targetArchitectures,
+          )
+        );
       }
     }
     if (androidBuildInfo.isEmpty) {
       throwToolExit('Please specify a build mode and try again.');
     }
+
     await androidBuilder.buildAar(
       project: _getProject(),
       target: '', // Not needed because this command only builds Android's code.
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index c0d8d69..3c5c5df 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -558,10 +558,12 @@
   }
 
   /// Compute the [BuildInfo] for the current flutter command.
+  /// Commands that build multiple build modes can pass in a [forcedBuildMode]
+  /// to be used instead of parsing flags.
   ///
   /// Throws a [ToolExit] if the current set of options is not compatible with
-  /// eachother.
-  BuildInfo getBuildInfo() {
+  /// each other.
+  BuildInfo getBuildInfo({ BuildMode forcedBuildMode }) {
     final bool trackWidgetCreation = argParser.options.containsKey('track-widget-creation') &&
       boolArg('track-widget-creation');
 
@@ -603,7 +605,7 @@
         'combination with "--${FlutterOptions.kSplitDebugInfoOption}"',
       );
     }
-    final BuildMode buildMode = getBuildMode();
+    final BuildMode buildMode = forcedBuildMode ?? getBuildMode();
     final bool treeShakeIcons = argParser.options.containsKey('tree-shake-icons')
       && buildMode.isPrecompiled
       && boolArg('tree-shake-icons');
diff --git a/packages/flutter_tools/test/general.shard/android/gradle_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_test.dart
index 3656890..7d76f82 100644
--- a/packages/flutter_tools/test/general.shard/android/gradle_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/gradle_test.dart
@@ -761,19 +761,12 @@
 
   group('buildPluginsAsAar', () {
     FileSystem fs;
-    MockProcessManager mockProcessManager;
+    FakeProcessManager fakeProcessManager;
     MockAndroidSdk mockAndroidSdk;
 
     setUp(() {
       fs = MemoryFileSystem();
-
-      mockProcessManager = MockProcessManager();
-      when(mockProcessManager.run(
-        any,
-        workingDirectory: anyNamed('workingDirectory'),
-        environment: anyNamed('environment'),
-      )).thenAnswer((_) async => ProcessResult(1, 0, '', ''));
-
+      fakeProcessManager = FakeProcessManager.list(<FakeCommand>[]);
       mockAndroidSdk = MockAndroidSdk();
       when(mockAndroidSdk.directory).thenReturn('irrelevant');
     });
@@ -830,12 +823,6 @@
         .childDirectory('repo')
         .createSync(recursive: true);
 
-      await buildPluginsAsAar(
-        FlutterProject.fromPath(androidDirectory.path),
-        const AndroidBuildInfo(BuildInfo.release),
-        buildDirectory: buildDirectory,
-      );
-
       final String flutterRoot = globals.fs.path.absolute(Cache.flutterRoot);
       final String initScript = globals.fs.path.join(
         flutterRoot,
@@ -844,40 +831,55 @@
         'gradle',
         'aar_init_script.gradle',
       );
-      verify(mockProcessManager.run(
-        <String>[
-          'gradlew',
-          '-I=$initScript',
-          '-Pflutter-root=$flutterRoot',
-          '-Poutput-dir=${buildDirectory.path}',
-          '-Pis-plugin=true',
-          '-PbuildNumber=1.0',
-          '-Ptarget-platform=android-arm,android-arm64,android-x64',
-          'assembleAarRelease',
-        ],
-        environment: anyNamed('environment'),
-        workingDirectory: plugin1.childDirectory('android').path),
-      ).called(1);
 
-      verify(mockProcessManager.run(
-        <String>[
-          'gradlew',
-          '-I=$initScript',
-          '-Pflutter-root=$flutterRoot',
-          '-Poutput-dir=${buildDirectory.path}',
-          '-Pis-plugin=true',
-          '-PbuildNumber=1.0',
-          '-Ptarget-platform=android-arm,android-arm64,android-x64',
-          'assembleAarRelease',
-        ],
-        environment: anyNamed('environment'),
-        workingDirectory: plugin2.childDirectory('android').path),
-      ).called(1);
+      fakeProcessManager
+        ..addCommand(FakeCommand(
+          command: <String>[
+            'gradlew',
+            '-I=$initScript',
+            '-Pflutter-root=$flutterRoot',
+            '-Poutput-dir=${buildDirectory.path}',
+            '-Pis-plugin=true',
+            '-PbuildNumber=1.0',
+            '-q',
+            '-Pfont-subset=true',
+            '-Ptarget-platform=android-arm,android-arm64,android-x64',
+            'assembleAarRelease',
+          ],
+          workingDirectory: plugin1.childDirectory('android').path,
+        ))
+        ..addCommand(FakeCommand(
+          command: <String>[
+            'gradlew',
+            '-I=$initScript',
+            '-Pflutter-root=$flutterRoot',
+            '-Poutput-dir=${buildDirectory.path}',
+            '-Pis-plugin=true',
+            '-PbuildNumber=1.0',
+            '-q',
+            '-Pfont-subset=true',
+            '-Ptarget-platform=android-arm,android-arm64,android-x64',
+            'assembleAarRelease',
+          ],
+          workingDirectory: plugin2.childDirectory('android').path,
+        ));
 
+      await buildPluginsAsAar(
+        FlutterProject.fromPath(androidDirectory.path),
+        const AndroidBuildInfo(BuildInfo(
+          BuildMode.release,
+          '',
+          treeShakeIcons: true,
+          dartObfuscation: true,
+          buildNumber: '2.0'
+        )),
+        buildDirectory: buildDirectory,
+      );
+      expect(fakeProcessManager.hasRemainingExpectations, isFalse);
     }, overrides: <Type, Generator>{
       AndroidSdk: () => mockAndroidSdk,
       FileSystem: () => fs,
-      ProcessManager: () => mockProcessManager,
+      ProcessManager: () => fakeProcessManager,
       GradleUtils: () => FakeGradleUtils(),
     });
 
@@ -920,32 +922,11 @@
         const AndroidBuildInfo(BuildInfo.release),
         buildDirectory: buildDirectory,
       );
-
-      final String flutterRoot = globals.fs.path.absolute(Cache.flutterRoot);
-      final String initScript = globals.fs.path.join(
-        flutterRoot,
-        'packages',
-        'flutter_tools',
-        'gradle',
-        'aar_init_script.gradle',
-      );
-      verifyNever(mockProcessManager.run(
-        <String>[
-          'gradlew',
-          '-I=$initScript',
-          '-Pflutter-root=$flutterRoot',
-          '-Poutput-dir=${buildDirectory.path}',
-          '-Pis-plugin=true',
-          '-Ptarget-platform=android-arm,android-arm64,android-x64',
-          'assembleAarRelease',
-        ],
-        environment: anyNamed('environment'),
-        workingDirectory: plugin1.childDirectory('android').path),
-      );
+      expect(fakeProcessManager.hasRemainingExpectations, isFalse);
     }, overrides: <Type, Generator>{
       AndroidSdk: () => mockAndroidSdk,
       FileSystem: () => fs,
-      ProcessManager: () => mockProcessManager,
+      ProcessManager: () => fakeProcessManager,
       GradleUtils: () => FakeGradleUtils(),
     });
   });
diff --git a/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart b/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
index eb16597f..5ec53ce 100644
--- a/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
@@ -8,6 +8,7 @@
 import 'package:flutter_tools/src/android/android_builder.dart';
 import 'package:flutter_tools/src/android/android_sdk.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/build_aar.dart';
 import 'package:flutter_tools/src/project.dart';
@@ -24,6 +25,18 @@
 void main() {
   Cache.disableLocking();
 
+  Future<BuildAarCommand> runCommandIn(String target, { List<String> arguments }) async {
+    final BuildAarCommand command = BuildAarCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+    await runner.run(<String>[
+      'aar',
+      '--no-pub',
+      ...?arguments,
+      target,
+    ]);
+    return command;
+  }
+
   group('Usage', () {
     Directory tempDir;
     Usage mockUsage;
@@ -37,18 +50,6 @@
       tryToDelete(tempDir);
     });
 
-    Future<BuildAarCommand> runCommandIn(String target, { List<String> arguments }) async {
-      final BuildAarCommand command = BuildAarCommand();
-      final CommandRunner<void> runner = createTestCommandRunner(command);
-      await runner.run(<String>[
-        'aar',
-        '--no-pub',
-        ...?arguments,
-        target,
-      ]);
-      return command;
-    }
-
     testUsingContext('indicate that project is a module', () async {
       final String projectPath = await createProject(tempDir,
           arguments: <String>['--no-pub', '--template=module']);
@@ -107,6 +108,93 @@
     });
   });
 
+  group('flag parsing', () {
+    Directory tempDir;
+    MockAndroidBuilder mockAndroidBuilder;
+
+    setUp(() {
+      mockAndroidBuilder = MockAndroidBuilder();
+      tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_build_aar_test.');
+    });
+
+    tearDown(() {
+      tryToDelete(tempDir);
+    });
+
+    testUsingContext('defaults', () async {
+      final String projectPath = await createProject(tempDir,
+        arguments: <String>['--no-pub']);
+      await runCommandIn(projectPath);
+
+      final Set<AndroidBuildInfo> androidBuildInfos = verify(mockAndroidBuilder.buildAar(
+        project: anyNamed('project'),
+        target: anyNamed('target'),
+        androidBuildInfo: captureAnyNamed('androidBuildInfo'),
+        outputDirectoryPath: anyNamed('outputDirectoryPath'),
+        buildNumber: '1.0',
+      )).captured[0] as Set<AndroidBuildInfo>;
+
+      expect(androidBuildInfos.length, 3);
+
+      final List<BuildMode> buildModes = <BuildMode>[];
+      for (final AndroidBuildInfo androidBuildInfo in androidBuildInfos) {
+        final BuildInfo buildInfo = androidBuildInfo.buildInfo;
+        buildModes.add(buildInfo.mode);
+        expect(buildInfo.treeShakeIcons, isFalse);
+        expect(buildInfo.flavor, isNull);
+        expect(buildInfo.splitDebugInfoPath, isNull);
+        expect(buildInfo.dartObfuscation, isFalse);
+        expect(androidBuildInfo.targetArchs, <AndroidArch>[AndroidArch.armeabi_v7a, AndroidArch.arm64_v8a, AndroidArch.x86_64]);
+      }
+      expect(buildModes.length, 3);
+      expect(buildModes, containsAll(<BuildMode>[BuildMode.debug, BuildMode.profile, BuildMode.release]));
+    }, overrides: <Type, Generator>{
+      AndroidBuilder: () => mockAndroidBuilder,
+    });
+
+    testUsingContext('parses flags', () async {
+      final String projectPath = await createProject(tempDir,
+        arguments: <String>['--no-pub']);
+      await runCommandIn(
+        projectPath,
+        arguments: <String>[
+          '--no-debug',
+          '--no-profile',
+          '--target-platform',
+          'android-x86',
+          '--tree-shake-icons',
+          '--flavor',
+          'free',
+          '--build-number',
+          '200',
+          '--split-debug-info',
+          '/project-name/v1.2.3/',
+          '--obfuscate',
+        ],
+      );
+
+      final Set<AndroidBuildInfo> androidBuildInfos = verify(mockAndroidBuilder.buildAar(
+        project: anyNamed('project'),
+        target: anyNamed('target'),
+        androidBuildInfo: captureAnyNamed('androidBuildInfo'),
+        outputDirectoryPath: anyNamed('outputDirectoryPath'),
+        buildNumber: '200',
+      )).captured[0] as Set<AndroidBuildInfo>;
+
+      final AndroidBuildInfo androidBuildInfo = androidBuildInfos.single;
+      expect(androidBuildInfo.targetArchs, <AndroidArch>[AndroidArch.x86]);
+
+      final BuildInfo buildInfo = androidBuildInfo.buildInfo;
+      expect(buildInfo.mode, BuildMode.release);
+      expect(buildInfo.treeShakeIcons, isTrue);
+      expect(buildInfo.flavor, 'free');
+      expect(buildInfo.splitDebugInfoPath, '/project-name/v1.2.3/');
+      expect(buildInfo.dartObfuscation, isTrue);
+    }, overrides: <Type, Generator>{
+      AndroidBuilder: () => mockAndroidBuilder,
+    });
+  });
+
   group('Gradle', () {
     ProcessManager mockProcessManager;
     Directory tempDir;
@@ -199,6 +287,7 @@
   return command;
 }
 
+class MockAndroidBuilder extends Mock implements AndroidBuilder {}
 class MockAndroidSdk extends Mock implements AndroidSdk {}
 class MockProcessManager extends Mock implements ProcessManager {}
 class MockProcess extends Mock implements Process {}