Revert "Revert "Add multidex flag and automatic multidex support (#90944)" (#91791)"
This reverts commit a9c31eca486249427a81bf8c00ce0237de11576b.
diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle
index 5a9131d..b1b9148 100644
--- a/packages/flutter_tools/gradle/flutter.gradle
+++ b/packages/flutter_tools/gradle/flutter.gradle
@@ -215,6 +215,38 @@
}
}
+ if (project.hasProperty("multidex-enabled") &&
+ project.property("multidex-enabled").toBoolean() &&
+ project.android.defaultConfig.minSdkVersion <= 20) {
+ String flutterMultidexKeepfile = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools",
+ "gradle", "flutter_multidex_keepfile.txt")
+ project.android {
+ defaultConfig {
+ multiDexEnabled true
+ manifestPlaceholders = [applicationName: "io.flutter.app.FlutterMultiDexApplication"]
+ }
+ buildTypes {
+ release {
+ multiDexKeepFile project.file(flutterMultidexKeepfile)
+ }
+ }
+ }
+ project.dependencies {
+ implementation "androidx.multidex:multidex:2.0.1"
+ }
+ } else {
+ String baseApplicationName = "android.app.Application"
+ if (project.hasProperty("base-application-name")) {
+ baseApplicationName = project.property("base-application-name")
+ }
+ project.android {
+ defaultConfig {
+ // Setting to android.app.Application is the same as omitting the attribute.
+ manifestPlaceholders = [applicationName: baseApplicationName]
+ }
+ }
+ }
+
if (useLocalEngine()) {
// This is required to pass the local engine to flutter build aot.
String engineOutPath = project.property('local-engine-out')
diff --git a/packages/flutter_tools/gradle/flutter_multidex_keepfile.txt b/packages/flutter_tools/gradle/flutter_multidex_keepfile.txt
new file mode 100644
index 0000000..34984e6
--- /dev/null
+++ b/packages/flutter_tools/gradle/flutter_multidex_keepfile.txt
@@ -0,0 +1,5 @@
+io/flutter/app/FlutterApplication.class
+io/flutter/app/FlutterMultiDexApplication.class
+io/flutter/embedding/engine/loader/FlutterLoader.class
+io/flutter/view/FlutterMain.class
+io/flutter/util/PathUtils.class
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index 7bdd848..4470ff5 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -595,7 +595,8 @@
androidBuildInfo: AndroidBuildInfo(
debuggingOptions.buildInfo,
targetArchs: <AndroidArch>[androidArch],
- fastStart: debuggingOptions.fastStart
+ fastStart: debuggingOptions.fastStart,
+ multidexEnabled: platformArgs['multidex'] as bool,
),
);
// Package has been built, so we can get the updated application ID and
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index be01307..fafe472 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -28,6 +28,7 @@
import 'android_studio.dart';
import 'gradle_errors.dart';
import 'gradle_utils.dart';
+import 'multidex.dart';
/// The directory where the APK artifact is generated.
Directory getApkDirectory(FlutterProject project) {
@@ -293,6 +294,22 @@
if (target != null) {
command.add('-Ptarget=$target');
}
+ // Only attempt adding multidex support if all the flutter generated files exist.
+ // If the files do not exist and it was unintentional, the app will fail to build
+ // and prompt the developer if they wish Flutter to add the files again via gradle_error.dart.
+ if (androidBuildInfo.multidexEnabled &&
+ multiDexApplicationExists(project.directory) &&
+ androidManifestHasNameVariable(project.directory)) {
+ command.add('-Pmultidex-enabled=true');
+ ensureMultiDexApplicationExists(project.directory);
+ _logger.printStatus('Building with Flutter multidex support enabled.');
+ }
+ // If using v1 embedding, we want to use FlutterApplication as the base app.
+ final String baseApplicationName =
+ project.android.getEmbeddingVersion() == AndroidEmbeddingVersion.v2 ?
+ 'android.app.Application' :
+ 'io.flutter.app.FlutterApplication';
+ command.add('-Pbase-application-name=$baseApplicationName');
final List<DeferredComponent>? deferredComponents = project.manifest.deferredComponents;
if (deferredComponents != null) {
if (deferredComponentsEnabled) {
@@ -389,6 +406,7 @@
line: detectedGradleErrorLine!,
project: project,
usesAndroidX: usesAndroidX,
+ multidexEnabled: androidBuildInfo.multidexEnabled,
);
if (retries >= 1) {
diff --git a/packages/flutter_tools/lib/src/android/gradle_errors.dart b/packages/flutter_tools/lib/src/android/gradle_errors.dart
index 2b94ef9..3277527 100644
--- a/packages/flutter_tools/lib/src/android/gradle_errors.dart
+++ b/packages/flutter_tools/lib/src/android/gradle_errors.dart
@@ -7,10 +7,12 @@
import '../base/error_handling_io.dart';
import '../base/file_system.dart';
import '../base/process.dart';
+import '../base/terminal.dart';
import '../globals_null_migrated.dart' as globals;
import '../project.dart';
import '../reporting/reporting.dart';
import 'android_studio.dart';
+import 'multidex.dart';
typedef GradleErrorTest = bool Function(String);
@@ -31,6 +33,7 @@
required String line,
required FlutterProject project,
required bool usesAndroidX,
+ required bool multidexEnabled,
}) handler;
/// The [BuildEvent] label is named gradle-[eventLabel].
@@ -71,8 +74,104 @@
minSdkVersion,
transformInputIssue,
lockFileDepMissing,
+ multidexErrorHandler,
];
+// Multidex error message.
+@visibleForTesting
+final GradleHandledError multidexErrorHandler = GradleHandledError(
+ test: _lineMatcher(const <String>[
+ 'com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:',
+ 'The number of method references in a .dex file cannot exceed 64K.',
+ ]),
+ handler: ({
+ required String line,
+ required FlutterProject project,
+ required bool usesAndroidX,
+ required bool multidexEnabled,
+ }) async {
+ globals.printStatus('${globals.logger.terminal.warningMark} App requires Multidex support', emphasis: true);
+ if (multidexEnabled) {
+ globals.printStatus(
+ 'Multidex support is required for your android app to build since the number of methods has exceeded 64k. '
+ "You may pass the --no-multidex flag to skip Flutter's multidex support to use a manual solution.\n",
+ indent: 4,
+ );
+ if (!androidManifestHasNameVariable(project.directory)) {
+ globals.printStatus(
+ r'Your `android/app/src/main/AndroidManifest.xml` does not contain `android:name="${applicationName}"` '
+ 'under the `application` element. This may be due to creating your project with an old version of Flutter. '
+ 'Add the `android:name="\${applicationName}"` attribute to your AndroidManifest.xml to enable Flutter\'s multidex support:\n',
+ indent: 4,
+ );
+ globals.printStatus(r'''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ ...
+ <application
+ ...
+ android:name=''',
+ indent: 8,
+ newline: false,
+ color: TerminalColor.grey,
+ );
+ globals.printStatus(r'"${applicationName}"', color: TerminalColor.green, newline: true);
+ globals.printStatus(r'''
+ ...>
+''',
+ indent: 8,
+ color: TerminalColor.grey,
+ );
+
+ globals.printStatus(
+ 'You may also roll your own multidex support by following the guide at: https://developer.android.com/studio/build/multidex\n',
+ indent: 4,
+ );
+ return GradleBuildStatus.exit;
+ }
+ if (!multiDexApplicationExists(project.directory)) {
+ globals.printStatus(
+ 'Flutter tool can add multidex support. The following file will be added by flutter:\n',
+ indent: 4,
+ );
+ globals.printStatus(
+ 'android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java\n',
+ indent: 8,
+ );
+ String selection = 'n';
+ // Default to 'no' if no interactive terminal.
+ try {
+ selection = await globals.terminal.promptForCharInput(
+ <String>['y', 'n'],
+ logger: globals.logger,
+ prompt: 'Do you want to continue with adding multidex support for Android?',
+ defaultChoiceIndex: 0,
+ );
+ } on StateError catch(e) {
+ globals.printError(
+ e.message,
+ indent: 0,
+ );
+ }
+ if (selection == 'y') {
+ ensureMultiDexApplicationExists(project.directory);
+ globals.printStatus(
+ 'Multidex enabled. Retrying build.\n',
+ indent: 0,
+ );
+ return GradleBuildStatus.retry;
+ }
+ }
+ } else {
+ globals.printStatus(
+ 'Flutter multidex handling is disabled. If you wish to let the tool configure multidex, use the --mutidex flag.',
+ indent: 4,
+ );
+ }
+ return GradleBuildStatus.exit;
+ },
+ eventLabel: 'multidex-error',
+);
+
// Permission defined error message.
@visibleForTesting
final GradleHandledError permissionDeniedErrorHandler = GradleHandledError(
@@ -83,12 +182,13 @@
required String line,
required FlutterProject project,
required bool usesAndroidX,
+ required bool multidexEnabled,
}) async {
globals.printStatus('${globals.logger.terminal.warningMark} Gradle does not have execution permission.', emphasis: true);
globals.printStatus(
'You should change the ownership of the project directory to your user, '
'or move the project to a directory with execute permissions.',
- indent: 4
+ indent: 4,
);
return GradleBuildStatus.exit;
},
@@ -119,6 +219,7 @@
required String line,
required FlutterProject project,
required bool usesAndroidX,
+ required bool multidexEnabled,
}) async {
globals.printError(
'${globals.logger.terminal.warningMark} Gradle threw an error while downloading artifacts from the network. '
@@ -148,6 +249,7 @@
required String line,
required FlutterProject project,
required bool usesAndroidX,
+ required bool multidexEnabled,
}) async {
globals.printStatus('${globals.logger.terminal.warningMark} The shrinker may have failed to optimize the Java bytecode.', emphasis: true);
globals.printStatus('To disable the shrinker, pass the `--no-shrink` flag to this command.', indent: 4);
@@ -169,6 +271,7 @@
required String line,
required FlutterProject project,
required bool usesAndroidX,
+ required bool multidexEnabled,
}) async {
const String licenseNotAcceptedMatcher =
r'You have not accepted the license agreements of the following SDK components:\s*\[(.+)\]';
@@ -202,6 +305,7 @@
required String line,
required FlutterProject project,
required bool usesAndroidX,
+ required bool multidexEnabled,
}) async {
final RunResult tasksRunResult = await globals.processUtils.run(
<String>[
@@ -274,6 +378,7 @@
required String line,
required FlutterProject project,
required bool usesAndroidX,
+ required bool multidexEnabled,
}) async {
final File gradleFile = project.directory
.childDirectory('android')
@@ -314,6 +419,7 @@
required String line,
required FlutterProject project,
required bool usesAndroidX,
+ required bool multidexEnabled,
}) async {
final File gradleFile = project.directory
.childDirectory('android')
@@ -347,6 +453,7 @@
required String line,
required FlutterProject project,
required bool usesAndroidX,
+ required bool multidexEnabled,
}) async {
final File gradleFile = project.directory
.childDirectory('android')
diff --git a/packages/flutter_tools/lib/src/android/multidex.dart b/packages/flutter_tools/lib/src/android/multidex.dart
new file mode 100644
index 0000000..a016e0a
--- /dev/null
+++ b/packages/flutter_tools/lib/src/android/multidex.dart
@@ -0,0 +1,99 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:xml/xml.dart';
+
+import '../base/file_system.dart';
+
+// These utility methods are used to generate the code for multidex support as
+// well as verifying the project is properly set up.
+
+File _getMultiDexApplicationFile(Directory projectDir) {
+ return projectDir.childDirectory('android')
+ .childDirectory('app')
+ .childDirectory('src')
+ .childDirectory('main')
+ .childDirectory('java')
+ .childDirectory('io')
+ .childDirectory('flutter')
+ .childDirectory('app')
+ .childFile('FlutterMultiDexApplication.java');
+}
+
+/// Creates the FlutterMultiDexApplication.java if it does not exist.
+void ensureMultiDexApplicationExists(final Directory projectDir) {
+ final File applicationFile = _getMultiDexApplicationFile(projectDir);
+ if (applicationFile.existsSync()) {
+ return;
+ }
+ applicationFile.createSync(recursive: true);
+
+ final StringBuffer buffer = StringBuffer();
+ buffer.write('''
+// Generated file.
+// If you wish to remove Flutter's multidex support, delete this entire file.
+
+package io.flutter.app;
+
+import android.content.Context;
+import androidx.annotation.CallSuper;
+import androidx.multidex.MultiDex;
+
+/**
+ * Extension of {@link io.flutter.app.FlutterApplication}, adding multidex support.
+ */
+public class FlutterMultiDexApplication extends FlutterApplication {
+ @Override
+ @CallSuper
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+ MultiDex.install(this);
+ }
+}
+''');
+ applicationFile.writeAsStringSync(buffer.toString(), flush: true);
+}
+
+/// Returns true if FlutterMultiDexApplication.java exists.
+///
+/// This function does not verify the contents of the file.
+bool multiDexApplicationExists(final Directory projectDir) {
+ if (_getMultiDexApplicationFile(projectDir).existsSync()) {
+ return true;
+ }
+ return false;
+}
+
+File _getManifestFile(Directory projectDir) {
+ return projectDir.childDirectory('android')
+ .childDirectory('app')
+ .childDirectory('src')
+ .childDirectory('main')
+ .childFile('AndroidManifest.xml');
+}
+
+/// Returns true if the `app` module AndroidManifest.xml includes the
+/// <application android:name="${applicationName}"> attribute.
+bool androidManifestHasNameVariable(final Directory projectDir) {
+ final File manifestFile = _getManifestFile(projectDir);
+ if (!manifestFile.existsSync()) {
+ return false;
+ }
+ XmlDocument document;
+ try {
+ document = XmlDocument.parse(manifestFile.readAsStringSync());
+ } on XmlParserException {
+ return false;
+ } on FileSystemException {
+ return false;
+ }
+ // Check for the ${androidName} application attribute.
+ for (final XmlElement application in document.findAllElements('application')) {
+ final String? applicationName = application.getAttribute('android:name');
+ if (applicationName == r'${applicationName}') {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index 9f62ce4..765e9f8 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -304,6 +304,7 @@
],
this.splitPerAbi = false,
this.fastStart = false,
+ this.multidexEnabled = false,
});
// The build info containing the mode and flavor.
@@ -321,6 +322,9 @@
/// Whether to bootstrap an empty application.
final bool fastStart;
+
+ /// Whether to enable multidex support for apps with more than 64k methods.
+ final bool multidexEnabled;
}
/// A summary of the compilation strategy used for Dart.
diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart
index c335b6a..63caab5 100644
--- a/packages/flutter_tools/lib/src/commands/build_apk.dart
+++ b/packages/flutter_tools/lib/src/commands/build_apk.dart
@@ -35,6 +35,7 @@
addNullSafetyModeOptions(hide: !verboseHelp);
usesAnalyzeSizeFlag();
addAndroidSpecificBuildOptions(hide: !verboseHelp);
+ addMultidexOption();
argParser
..addFlag('split-per-abi',
negatable: false,
@@ -99,9 +100,11 @@
buildInfo,
splitPerAbi: boolArg('split-per-abi'),
targetArchs: stringsArg('target-platform').map<AndroidArch>(getAndroidArchForName),
+ multidexEnabled: boolArg('multidex'),
);
validateBuild(androidBuildInfo);
displayNullSafetyMode(androidBuildInfo.buildInfo);
+ globals.terminal.usesTerminalUi = true;
await androidBuilder.buildApk(
project: FlutterProject.current(),
target: targetFile,
diff --git a/packages/flutter_tools/lib/src/commands/build_appbundle.dart b/packages/flutter_tools/lib/src/commands/build_appbundle.dart
index 329267a..7d9a282 100644
--- a/packages/flutter_tools/lib/src/commands/build_appbundle.dart
+++ b/packages/flutter_tools/lib/src/commands/build_appbundle.dart
@@ -39,6 +39,7 @@
addEnableExperimentation(hide: !verboseHelp);
usesAnalyzeSizeFlag();
addAndroidSpecificBuildOptions(hide: !verboseHelp);
+ addMultidexOption();
argParser.addMultiOption('target-platform',
splitCommas: true,
defaultsTo: <String>['android-arm', 'android-arm64', 'android-x64'],
@@ -110,6 +111,7 @@
final AndroidBuildInfo androidBuildInfo = AndroidBuildInfo(await getBuildInfo(),
targetArchs: stringsArg('target-platform').map<AndroidArch>(getAndroidArchForName),
+ multidexEnabled: boolArg('multidex'),
);
// Do all setup verification that doesn't involve loading units. Checks that
// require generated loading units are done after gen_snapshot in assemble.
@@ -144,6 +146,7 @@
validateBuild(androidBuildInfo);
displayNullSafetyMode(androidBuildInfo.buildInfo);
+ globals.terminal.usesTerminalUi = true;
await androidBuilder.buildAab(
project: FlutterProject.current(),
target: targetFile,
diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart
index fdb30d0..2084b79 100644
--- a/packages/flutter_tools/lib/src/commands/drive.dart
+++ b/packages/flutter_tools/lib/src/commands/drive.dart
@@ -65,6 +65,7 @@
// to prevent a local network permission dialog on iOS 14+,
// which cannot be accepted or dismissed in a CI environment.
addPublishPort(enabledByDefault: false, verboseHelp: verboseHelp);
+ addMultidexOption();
argParser
..addFlag('keep-app-running',
defaultsTo: null,
@@ -251,6 +252,8 @@
'trace-startup': traceStartup,
if (web)
'--no-launch-chrome': true,
+ if (boolArg('multidex'))
+ 'multidex': true,
}
);
} else {
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 3f1e025..0c65258 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -249,6 +249,7 @@
// This will allow subsequent "flutter attach" commands to connect to the VM
// without needing to know the port.
addPublishPort(verboseHelp: verboseHelp);
+ addMultidexOption();
argParser
..addFlag('enable-software-rendering',
negatable: false,
@@ -500,6 +501,7 @@
dillOutputPath: stringArg('output-dill'),
stayResident: stayResident,
ipv6: ipv6,
+ multidexEnabled: boolArg('multidex'),
);
} else if (webMode) {
return webRunnerFactory.createWebRunner(
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 686cd92..607500e 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -416,7 +416,9 @@
}
devFSWriter = device.createDevFSWriter(package, userIdentifier);
- final Map<String, dynamic> platformArgs = <String, dynamic>{};
+ final Map<String, dynamic> platformArgs = <String, dynamic>{
+ 'multidex': hotRunner.multidexEnabled,
+ };
await startEchoingDeviceLog();
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index e81bf73..5563473 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -93,6 +93,7 @@
bool stayResident = true,
bool ipv6 = false,
bool machine = false,
+ this.multidexEnabled = false,
ResidentDevtoolsHandlerFactory devtoolsHandler = createDefaultHandler,
StopwatchFactory stopwatchFactory = const StopwatchFactory(),
ReloadSourcesHelper reloadSourcesHelper = _defaultReloadSourcesHelper,
@@ -120,6 +121,7 @@
final bool benchmarkMode;
final File applicationBinary;
final bool hostIsIde;
+ final bool multidexEnabled;
/// When performing a hot restart, the tool needs to upload a new main.dart.dill to
/// each attached device's devfs. Replacing the existing file is not safe and does
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index f95dda3..98d0b91 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -818,6 +818,16 @@
);
}
+ void addMultidexOption({ bool hide = false }) {
+ argParser.addFlag('multidex',
+ negatable: true,
+ defaultsTo: true,
+ help: 'When enabled, indicates that the app should be built with multidex support. This '
+ 'flag adds the dependencies for multidex when the minimum android sdk is 20 or '
+ 'below. For android sdk versions 21 and above, multidex support is native.',
+ );
+ }
+
/// Adds build options common to all of the desktop build commands.
void addCommonDesktopBuildOptions({ @required bool verboseHelp }) {
addBuildModeFlags(verboseHelp: verboseHelp);
diff --git a/packages/flutter_tools/templates/app_shared/android.tmpl/app/src/main/AndroidManifest.xml.tmpl b/packages/flutter_tools/templates/app_shared/android.tmpl/app/src/main/AndroidManifest.xml.tmpl
index 4f1724e..e20e2be 100644
--- a/packages/flutter_tools/templates/app_shared/android.tmpl/app/src/main/AndroidManifest.xml.tmpl
+++ b/packages/flutter_tools/templates/app_shared/android.tmpl/app/src/main/AndroidManifest.xml.tmpl
@@ -2,6 +2,7 @@
package="{{androidIdentifier}}">
<application
android:label="{{projectName}}"
+ android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart
index 1112552..af937bf 100644
--- a/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart
+++ b/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart
@@ -157,6 +157,7 @@
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
+ '-Pbase-application-name=android.app.Application',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=true',
'-Ptree-shake-icons=true',
@@ -186,6 +187,7 @@
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
+ '-Pbase-application-name=android.app.Application',
'-Pdart-obfuscation=false',
'-Psplit-debug-info=${tempDir.path}',
'-Ptrack-widget-creation=true',
@@ -216,6 +218,7 @@
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
+ '-Pbase-application-name=android.app.Application',
'-Pdart-obfuscation=false',
'-Pextra-front-end-options=foo,bar',
'-Ptrack-widget-creation=true',
@@ -246,6 +249,7 @@
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
+ '-Pbase-application-name=android.app.Application',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=true',
'-Ptree-shake-icons=true',
@@ -281,6 +285,7 @@
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
+ '-Pbase-application-name=android.app.Application',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=true',
'-Ptree-shake-icons=true',
@@ -335,6 +340,7 @@
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
+ '-Pbase-application-name=android.app.Application',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=true',
'-Ptree-shake-icons=true',
@@ -381,6 +387,7 @@
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
+ '-Pbase-application-name=android.app.Application',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=true',
'-Ptree-shake-icons=true',
diff --git a/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart
index c0d50db..c18caf1 100644
--- a/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart
@@ -56,6 +56,7 @@
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=lib/main.dart',
+ '-Pbase-application-name=io.flutter.app.FlutterApplication',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=false',
'-Ptree-shake-icons=false',
@@ -101,6 +102,7 @@
String line,
FlutterProject project,
bool usesAndroidX,
+ bool multidexEnabled
}) async {
handlerCalled = true;
return GradleBuildStatus.exit;
@@ -142,6 +144,7 @@
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=lib/main.dart',
+ '-Pbase-application-name=io.flutter.app.FlutterApplication',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=false',
'-Ptree-shake-icons=false',
@@ -156,6 +159,7 @@
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=lib/main.dart',
+ '-Pbase-application-name=io.flutter.app.FlutterApplication',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=false',
'-Ptree-shake-icons=false',
@@ -205,6 +209,7 @@
String line,
FlutterProject project,
bool usesAndroidX,
+ bool multidexEnabled
}) async {
return GradleBuildStatus.retry;
},
@@ -243,6 +248,7 @@
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=lib/main.dart',
+ '-Pbase-application-name=io.flutter.app.FlutterApplication',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=false',
'-Ptree-shake-icons=false',
@@ -288,6 +294,7 @@
String line,
FlutterProject project,
bool usesAndroidX,
+ bool multidexEnabled
}) async {
handlerCalled = true;
return GradleBuildStatus.exit;
@@ -329,6 +336,7 @@
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=lib/main.dart',
+ '-Pbase-application-name=io.flutter.app.FlutterApplication',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=false',
'-Ptree-shake-icons=false',
@@ -388,6 +396,7 @@
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=lib/main.dart',
+ '-Pbase-application-name=io.flutter.app.FlutterApplication',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=false',
'-Ptree-shake-icons=false',
@@ -402,6 +411,7 @@
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=lib/main.dart',
+ '-Pbase-application-name=io.flutter.app.FlutterApplication',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=false',
'-Ptree-shake-icons=false',
@@ -450,6 +460,7 @@
String line,
FlutterProject project,
bool usesAndroidX,
+ bool multidexEnabled
}) async {
return GradleBuildStatus.retry;
},
@@ -488,6 +499,7 @@
'-q',
'-Ptarget-platform=android-arm64',
'-Ptarget=lib/main.dart',
+ '-Pbase-application-name=io.flutter.app.FlutterApplication',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=false',
'-Ptree-shake-icons=false',
@@ -580,6 +592,7 @@
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=lib/main.dart',
+ '-Pbase-application-name=io.flutter.app.FlutterApplication',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=false',
'-Ptree-shake-icons=false',
@@ -765,6 +778,7 @@
'-Plocal-engine-out=out/android_arm',
'-Ptarget-platform=android-arm',
'-Ptarget=lib/main.dart',
+ '-Pbase-application-name=io.flutter.app.FlutterApplication',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=false',
'-Ptree-shake-icons=false',
@@ -838,6 +852,7 @@
'-Plocal-engine-out=out/android_arm64',
'-Ptarget-platform=android-arm64',
'-Ptarget=lib/main.dart',
+ '-Pbase-application-name=io.flutter.app.FlutterApplication',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=false',
'-Ptree-shake-icons=false',
@@ -911,6 +926,7 @@
'-Plocal-engine-out=out/android_x86',
'-Ptarget-platform=android-x86',
'-Ptarget=lib/main.dart',
+ '-Pbase-application-name=io.flutter.app.FlutterApplication',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=false',
'-Ptree-shake-icons=false',
@@ -984,6 +1000,7 @@
'-Plocal-engine-out=out/android_x64',
'-Ptarget-platform=android-x64',
'-Ptarget=lib/main.dart',
+ '-Pbase-application-name=io.flutter.app.FlutterApplication',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=false',
'-Ptree-shake-icons=false',
@@ -1056,6 +1073,7 @@
'--no-daemon',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=lib/main.dart',
+ '-Pbase-application-name=io.flutter.app.FlutterApplication',
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=false',
'-Ptree-shake-icons=false',
diff --git a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart
index eb820c7..a1ff236 100644
--- a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart
@@ -9,7 +9,9 @@
import 'package:flutter_tools/src/android/gradle_errors.dart';
import 'package:flutter_tools/src/android/gradle_utils.dart';
import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/globals_null_migrated.dart' as globals;
import 'package:flutter_tools/src/project.dart';
@@ -30,6 +32,7 @@
minSdkVersion,
transformInputIssue,
lockFileDepMissing,
+ multidexErrorHandler,
])
);
});
@@ -329,6 +332,213 @@
});
});
+ group('multidex errors', () {
+ testUsingContext('exits if multidex AndroidManifest not detected', () async {
+ const String errorMessage = r'''
+Caused by: com.android.tools.r8.utils.b: Cannot fit requested classes in a single dex file (# methods: 85091 > 65536)
+ at com.android.tools.r8.utils.T0.error(SourceFile:1)
+ at com.android.tools.r8.utils.T0.a(SourceFile:2)
+ at com.android.tools.r8.dex.P.a(SourceFile:740)
+ at com.android.tools.r8.dex.P$h.a(SourceFile:7)
+ at com.android.tools.r8.dex.b.a(SourceFile:14)
+ at com.android.tools.r8.dex.b.b(SourceFile:25)
+ at com.android.tools.r8.D8.d(D8.java:133)
+ at com.android.tools.r8.D8.b(D8.java:1)
+ at com.android.tools.r8.utils.Y.a(SourceFile:36)
+ ... 38 more
+
+
+FAILURE: Build failed with an exception.
+
+* What went wrong:
+Execution failed for task ':app:mergeDexDebug'.
+> A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
+ > com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:
+ The number of method references in a .dex file cannot exceed 64K.
+ Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html''';
+
+ expect(formatTestErrorMessage(errorMessage, multidexErrorHandler), isTrue);
+ expect(await multidexErrorHandler.handler(project: FlutterProject.fromDirectory(globals.fs.currentDirectory), multidexEnabled: true), equals(GradleBuildStatus.exit));
+
+ expect(testLogger.statusText,
+ contains(
+ 'Multidex support is required for your android app to build since the number of methods has exceeded 64k.'
+ )
+ );
+ expect(testLogger.statusText,
+ contains(
+ 'Your `android/app/src/main/AndroidManifest.xml` does not contain'
+ )
+ );
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem.test(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+ testUsingContext('retries if multidex support enabled', () async {
+ const String errorMessage = r'''
+Caused by: com.android.tools.r8.utils.b: Cannot fit requested classes in a single dex file (# methods: 85091 > 65536)
+ at com.android.tools.r8.utils.T0.error(SourceFile:1)
+ at com.android.tools.r8.utils.T0.a(SourceFile:2)
+ at com.android.tools.r8.dex.P.a(SourceFile:740)
+ at com.android.tools.r8.dex.P$h.a(SourceFile:7)
+ at com.android.tools.r8.dex.b.a(SourceFile:14)
+ at com.android.tools.r8.dex.b.b(SourceFile:25)
+ at com.android.tools.r8.D8.d(D8.java:133)
+ at com.android.tools.r8.D8.b(D8.java:1)
+ at com.android.tools.r8.utils.Y.a(SourceFile:36)
+ ... 38 more
+
+
+FAILURE: Build failed with an exception.
+
+* What went wrong:
+Execution failed for task ':app:mergeDexDebug'.
+> A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
+ > com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:
+ The number of method references in a .dex file cannot exceed 64K.
+ Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html''';
+
+ final File manifest = globals.fs.currentDirectory
+ .childDirectory('android')
+ .childDirectory('app')
+ .childDirectory('src')
+ .childDirectory('main')
+ .childFile('AndroidManifest.xml');
+ manifest.createSync(recursive: true);
+ manifest.writeAsStringSync(r'''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.multidexapp">
+ <application
+ android:label="multidextest2"
+ android:name="${applicationName}"
+ android:icon="@mipmap/ic_launcher">
+ </application>
+</manifest>
+''', flush: true);
+
+ expect(formatTestErrorMessage(errorMessage, multidexErrorHandler), isTrue);
+ expect(await multidexErrorHandler.handler(project: FlutterProject.fromDirectory(globals.fs.currentDirectory), multidexEnabled: true), equals(GradleBuildStatus.retry));
+
+ expect(testLogger.statusText,
+ contains(
+ 'Multidex support is required for your android app to build since the number of methods has exceeded 64k.'
+ )
+ );
+ expect(testLogger.statusText,
+ contains(
+ 'android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java'
+ )
+ );
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem.test(),
+ ProcessManager: () => FakeProcessManager.any(),
+ AnsiTerminal: () => _TestPromptTerminal('y')
+ });
+
+ testUsingContext('exits if multidex support skipped', () async {
+ const String errorMessage = r'''
+Caused by: com.android.tools.r8.utils.b: Cannot fit requested classes in a single dex file (# methods: 85091 > 65536)
+ at com.android.tools.r8.utils.T0.error(SourceFile:1)
+ at com.android.tools.r8.utils.T0.a(SourceFile:2)
+ at com.android.tools.r8.dex.P.a(SourceFile:740)
+ at com.android.tools.r8.dex.P$h.a(SourceFile:7)
+ at com.android.tools.r8.dex.b.a(SourceFile:14)
+ at com.android.tools.r8.dex.b.b(SourceFile:25)
+ at com.android.tools.r8.D8.d(D8.java:133)
+ at com.android.tools.r8.D8.b(D8.java:1)
+ at com.android.tools.r8.utils.Y.a(SourceFile:36)
+ ... 38 more
+
+
+FAILURE: Build failed with an exception.
+
+* What went wrong:
+Execution failed for task ':app:mergeDexDebug'.
+> A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
+ > com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:
+ The number of method references in a .dex file cannot exceed 64K.
+ Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html''';
+
+ final File manifest = globals.fs.currentDirectory
+ .childDirectory('android')
+ .childDirectory('app')
+ .childDirectory('src')
+ .childDirectory('main')
+ .childFile('AndroidManifest.xml');
+ manifest.createSync(recursive: true);
+ manifest.writeAsStringSync(r'''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.multidexapp">
+ <application
+ android:label="multidextest2"
+ android:name="${applicationName}"
+ android:icon="@mipmap/ic_launcher">
+ </application>
+</manifest>
+''', flush: true);
+
+ expect(formatTestErrorMessage(errorMessage, multidexErrorHandler), isTrue);
+ expect(await multidexErrorHandler.handler(project: FlutterProject.fromDirectory(globals.fs.currentDirectory), multidexEnabled: true), equals(GradleBuildStatus.exit));
+
+ expect(testLogger.statusText,
+ contains(
+ 'Multidex support is required for your android app to build since the number of methods has exceeded 64k.'
+ )
+ );
+ expect(testLogger.statusText,
+ contains(
+ 'Flutter tool can add multidex support. The following file will be added by flutter:'
+ )
+ );
+ expect(testLogger.statusText,
+ contains(
+ 'android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java'
+ )
+ );
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem.test(),
+ ProcessManager: () => FakeProcessManager.any(),
+ AnsiTerminal: () => _TestPromptTerminal('n')
+ });
+
+ testUsingContext('exits if multidex support disabled', () async {
+ const String errorMessage = r'''
+Caused by: com.android.tools.r8.utils.b: Cannot fit requested classes in a single dex file (# methods: 85091 > 65536)
+ at com.android.tools.r8.utils.T0.error(SourceFile:1)
+ at com.android.tools.r8.utils.T0.a(SourceFile:2)
+ at com.android.tools.r8.dex.P.a(SourceFile:740)
+ at com.android.tools.r8.dex.P$h.a(SourceFile:7)
+ at com.android.tools.r8.dex.b.a(SourceFile:14)
+ at com.android.tools.r8.dex.b.b(SourceFile:25)
+ at com.android.tools.r8.D8.d(D8.java:133)
+ at com.android.tools.r8.D8.b(D8.java:1)
+ at com.android.tools.r8.utils.Y.a(SourceFile:36)
+ ... 38 more
+
+
+FAILURE: Build failed with an exception.
+
+* What went wrong:
+Execution failed for task ':app:mergeDexDebug'.
+> A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
+ > com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:
+ The number of method references in a .dex file cannot exceed 64K.
+ Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html''';
+
+ expect(formatTestErrorMessage(errorMessage, multidexErrorHandler), isTrue);
+ expect(await multidexErrorHandler.handler(project: FlutterProject.fromDirectory(globals.fs.currentDirectory), multidexEnabled: false), equals(GradleBuildStatus.exit));
+
+ expect(testLogger.statusText,
+ contains(
+ 'Flutter multidex handling is disabled.'
+ )
+ );
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem.test(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+ });
+
group('permission errors', () {
testUsingContext('throws toolExit if gradle is missing execute permissions', () async {
const String errorMessage = '''
@@ -667,3 +877,21 @@
return 'gradlew';
}
}
+
+/// Simple terminal that returns the specified string when
+/// promptForCharInput is called.
+class _TestPromptTerminal extends AnsiTerminal {
+ _TestPromptTerminal(this.promptResult);
+
+ final String promptResult;
+
+ @override
+ Future<String> promptForCharInput(List<String> acceptedCharacters, {
+ Logger logger,
+ String prompt,
+ int defaultChoiceIndex,
+ bool displayAcceptedCharacters = true,
+ }) {
+ return Future<String>.value(promptResult);
+ }
+}
diff --git a/packages/flutter_tools/test/general.shard/android/multidex_test.dart b/packages/flutter_tools/test/general.shard/android/multidex_test.dart
new file mode 100644
index 0000000..a4cc8e8
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/android/multidex_test.dart
@@ -0,0 +1,189 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// @dart = 2.8
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/android/multidex.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/globals_null_migrated.dart' as globals;
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+ testUsingContext('ensureMultidexUtilsExists returns when exists', () async {
+ final Directory directory = globals.fs.currentDirectory;
+ final File applicationFile = directory.childDirectory('android')
+ .childDirectory('app')
+ .childDirectory('src')
+ .childDirectory('main')
+ .childDirectory('java')
+ .childDirectory('io')
+ .childDirectory('flutter')
+ .childDirectory('app')
+ .childFile('FlutterMultiDexApplication.java');
+ applicationFile.createSync(recursive: true);
+ applicationFile.writeAsStringSync('hello', flush: true);
+ expect(applicationFile.readAsStringSync(), 'hello');
+
+ ensureMultiDexApplicationExists(directory);
+
+ // File should remain untouched
+ expect(applicationFile.readAsStringSync(), 'hello');
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem.test(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('ensureMultiDexApplicationExists generates when does not exist', () async {
+ final Directory directory = globals.fs.currentDirectory;
+ final File applicationFile = directory.childDirectory('android')
+ .childDirectory('app')
+ .childDirectory('src')
+ .childDirectory('main')
+ .childDirectory('java')
+ .childDirectory('io')
+ .childDirectory('flutter')
+ .childDirectory('app')
+ .childFile('FlutterMultiDexApplication.java');
+
+ ensureMultiDexApplicationExists(directory);
+
+ final String contents = applicationFile.readAsStringSync();
+ expect(contents.contains('FlutterMultiDexApplication'), true);
+ expect(contents.contains('MultiDex.install(this);'), true);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem.test(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('multiDexApplicationExists false when does not exist', () async {
+ final Directory directory = globals.fs.currentDirectory;
+ expect(multiDexApplicationExists(directory), false);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem.test(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('multiDexApplicationExists true when does exist', () async {
+ final Directory directory = globals.fs.currentDirectory;
+ final File utilsFile = directory.childDirectory('android')
+ .childDirectory('app')
+ .childDirectory('src')
+ .childDirectory('main')
+ .childDirectory('java')
+ .childDirectory('io')
+ .childDirectory('flutter')
+ .childDirectory('app')
+ .childFile('FlutterMultiDexApplication.java');
+ utilsFile.createSync(recursive: true);
+
+ expect(multiDexApplicationExists(directory), true);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem.test(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('androidManifestHasNameVariable true with valid manifest', () async {
+ final Directory directory = globals.fs.currentDirectory;
+ final File applicationFile = directory.childDirectory('android')
+ .childDirectory('app')
+ .childDirectory('src')
+ .childDirectory('main')
+ .childFile('AndroidManifest.xml');
+ applicationFile.createSync(recursive: true);
+ applicationFile.writeAsStringSync(r'''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.multidexapp">
+ <application
+ android:label="multidextest2"
+ android:name="${applicationName}"
+ android:icon="@mipmap/ic_launcher">
+ </application>
+</manifest>
+''', flush: true);
+ expect(androidManifestHasNameVariable(directory), true);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem.test(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('androidManifestHasNameVariable false with no android:name attribute', () async {
+ final Directory directory = globals.fs.currentDirectory;
+ final File applicationFile = directory.childDirectory('android')
+ .childDirectory('app')
+ .childDirectory('src')
+ .childDirectory('main')
+ .childFile('AndroidManifest.xml');
+ applicationFile.createSync(recursive: true);
+ applicationFile.writeAsStringSync(r'''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.multidexapp">
+ <application
+ android:label="multidextest2"
+ android:icon="@mipmap/ic_launcher">
+ </application>
+''', flush: true);
+ expect(androidManifestHasNameVariable(directory), false);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem.test(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('androidManifestHasNameVariable false with incorrect android:name attribute', () async {
+ final Directory directory = globals.fs.currentDirectory;
+ final File applicationFile = directory.childDirectory('android')
+ .childDirectory('app')
+ .childDirectory('src')
+ .childDirectory('main')
+ .childFile('AndroidManifest.xml');
+ applicationFile.createSync(recursive: true);
+ applicationFile.writeAsStringSync(r'''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.multidexapp">
+ <application
+ android:label="multidextest2"
+ android:name="io.flutter.app.FlutterApplication"
+ android:icon="@mipmap/ic_launcher">
+ </application>
+''', flush: true);
+ expect(androidManifestHasNameVariable(directory), false);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem.test(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('androidManifestHasNameVariable false with invalid xml manifest', () async {
+ final Directory directory = globals.fs.currentDirectory;
+ final File applicationFile = directory.childDirectory('android')
+ .childDirectory('app')
+ .childDirectory('src')
+ .childDirectory('main')
+ .childFile('AndroidManifest.xml');
+ applicationFile.createSync(recursive: true);
+ applicationFile.writeAsStringSync(r'''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.multidexapp">
+ <application
+ android:label="multidextest2"
+ android:name="${applicationName}"
+ android:icon="@mipmap/ic_launcher">
+ </application>
+''', flush: true);
+ expect(androidManifestHasNameVariable(directory), false);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem.test(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('androidManifestHasNameVariable false with no manifest file', () async {
+ final Directory directory = globals.fs.currentDirectory;
+ expect(androidManifestHasNameVariable(directory), false);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem.test(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+}
diff --git a/packages/flutter_tools/test/integration.shard/multidex_build_test.dart b/packages/flutter_tools/test/integration.shard/multidex_build_test.dart
new file mode 100644
index 0000000..21c43e1
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/multidex_build_test.dart
@@ -0,0 +1,61 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// @dart = 2.8
+
+import 'package:file/file.dart';
+import 'package:flutter_tools/src/base/io.dart';
+
+import '../src/common.dart';
+import 'test_data/multidex_project.dart';
+import 'test_driver.dart';
+import 'test_utils.dart';
+
+void main() {
+ Directory tempDir;
+ FlutterRunTestDriver _flutter;
+
+ setUp(() async {
+ tempDir = createResolvedTempDirectorySync('run_test.');
+ _flutter = FlutterRunTestDriver(tempDir);
+ });
+
+ tearDown(() async {
+ await _flutter.stop();
+ tryToDelete(tempDir);
+ });
+
+ testWithoutContext('simple build apk succeeds', () async {
+ final MultidexProject project = MultidexProject(true);
+ await project.setUpIn(tempDir);
+ final String flutterBin = fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter');
+ final ProcessResult result = await processManager.run(<String>[
+ flutterBin,
+ ...getLocalEngineArguments(),
+ 'build',
+ 'apk',
+ '--debug',
+ ], workingDirectory: tempDir.path);
+
+ expect(result.exitCode, 0);
+ expect(result.stdout.toString(), contains('app-debug.apk'));
+ });
+
+ testWithoutContext('simple build apk without FlutterMultiDexApplication fails', () async {
+ final MultidexProject project = MultidexProject(false);
+ await project.setUpIn(tempDir);
+ final String flutterBin = fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter');
+ final ProcessResult result = await processManager.run(<String>[
+ flutterBin,
+ ...getLocalEngineArguments(),
+ 'build',
+ 'apk',
+ '--debug',
+ ], workingDirectory: tempDir.path);
+
+ expect(result.stderr.toString(), contains('Cannot fit requested classes in a single dex file'));
+ expect(result.stderr.toString(), contains('The number of method references in a .dex file cannot exceed 64K.'));
+ expect(result.exitCode, 1);
+ });
+}
diff --git a/packages/flutter_tools/test/integration.shard/test_data/multidex_project.dart b/packages/flutter_tools/test/integration.shard/test_data/multidex_project.dart
new file mode 100644
index 0000000..70fdf0d
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/test_data/multidex_project.dart
@@ -0,0 +1,322 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// @dart = 2.8
+
+import 'package:file/file.dart';
+
+import '../../src/common.dart';
+import '../test_utils.dart';
+import 'project.dart';
+
+class MultidexProject extends Project {
+ MultidexProject(this.includeFlutterMultiDexApplication);
+
+ @override
+ Future<void> setUpIn(Directory dir, {
+ bool useDeferredLoading = false,
+ bool useSyntheticPackage = false,
+ }) {
+ this.dir = dir;
+ if (androidSettings != null) {
+ writeFile(fileSystem.path.join(dir.path, 'android', 'settings.gradle'), androidSettings);
+ }
+ if (androidBuild != null) {
+ writeFile(fileSystem.path.join(dir.path, 'android', 'build.gradle'), androidBuild);
+ }
+ if (androidLocalProperties != null) {
+ writeFile(fileSystem.path.join(dir.path, 'android', 'local.properties'), androidLocalProperties);
+ }
+ if (androidGradleProperties != null) {
+ writeFile(fileSystem.path.join(dir.path, 'android', 'gradle.properties'), androidGradleProperties);
+ }
+ if (appBuild != null) {
+ writeFile(fileSystem.path.join(dir.path, 'android', 'app', 'build.gradle'), appBuild);
+ }
+ if (appManifest != null) {
+ writeFile(fileSystem.path.join(dir.path, 'android', 'app', 'src', 'main', 'AndroidManifest.xml'), appManifest);
+ }
+ if (appStrings != null) {
+ writeFile(fileSystem.path.join(dir.path, 'android', 'app', 'src', 'main', 'res', 'values', 'strings.xml'), appStrings);
+ }
+ if (appStyles != null) {
+ writeFile(fileSystem.path.join(dir.path, 'android', 'app', 'src', 'main', 'res', 'values', 'styles.xml'), appStyles);
+ }
+ if (appLaunchBackground != null) {
+ writeFile(fileSystem.path.join(dir.path, 'android', 'app', 'src', 'main', 'res', 'drawable', 'launch_background.xml'), appLaunchBackground);
+ }
+ if (includeFlutterMultiDexApplication && appMultidexApplication != null) {
+ writeFile(fileSystem.path.join(dir.path, 'android', 'app', 'src', 'main', 'java', fileSystem.path.join('io', 'flutter', 'app', 'FlutterMultiDexApplication.java')), appMultidexApplication);
+ }
+ return super.setUpIn(dir);
+ }
+
+ final bool includeFlutterMultiDexApplication;
+
+ @override
+ final String pubspec = '''
+ name: test
+ environment:
+ sdk: ">=2.12.0-0 <3.0.0"
+
+ dependencies:
+ flutter:
+ sdk: flutter
+ cloud_firestore: ^2.5.3
+ firebase_core: ^1.6.0
+ ''';
+
+ @override
+ final String main = r'''
+ import 'package:flutter/material.dart';
+
+ void main() {
+ runApp(MyApp());
+ }
+
+ class MyApp extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return new MaterialApp(
+ title: 'Flutter Demo',
+ home: new Container(),
+ );
+ }
+ }
+ ''';
+
+ String get androidSettings => r'''
+ include ':app'
+
+ def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
+ def properties = new Properties()
+
+ assert localPropertiesFile.exists()
+ localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+
+ def flutterSdkPath = properties.getProperty("flutter.sdk")
+ assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+ apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
+ ''';
+
+ String get androidBuild => r'''
+ buildscript {
+ ext.kotlin_version = '1.3.50'
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.1.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+ }
+
+ allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ }
+
+ rootProject.buildDir = '../build'
+ subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+ }
+ subprojects {
+ project.evaluationDependsOn(':app')
+ }
+
+ task clean(type: Delete) {
+ delete rootProject.buildDir
+ }
+ ''';
+
+ String get appBuild => r'''
+ def localProperties = new Properties()
+ def localPropertiesFile = rootProject.file('local.properties')
+ if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+ }
+
+ def flutterRoot = localProperties.getProperty('flutter.sdk')
+ if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+ }
+
+ def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+ if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+ }
+
+ def flutterVersionName = localProperties.getProperty('flutter.versionName')
+ if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+ }
+
+ apply plugin: 'com.android.application'
+ apply plugin: 'kotlin-android'
+ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+ android {
+ compileSdkVersion 30
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId "com.example.multidextest2"
+ minSdkVersion 16
+ targetSdkVersion 30
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig signingConfigs.debug
+ }
+ }
+ }
+
+ flutter {
+ source '../..'
+ }
+
+ dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ }
+ ''';
+
+ String get androidLocalProperties => '''
+ flutter.sdk=${getFlutterRoot()}
+ flutter.buildMode=debug
+ flutter.versionName=1.0.0
+ flutter.versionCode=22
+ ''';
+
+ String get androidGradleProperties => '''
+ org.gradle.jvmargs=-Xmx1536M
+ android.useAndroidX=true
+ android.enableJetifier=true
+ android.enableR8=true
+ android.experimental.enableNewResourceShrinker=true
+ ''';
+
+ String get appManifest => r'''
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.multidextest">
+ <application
+ android:label="multidextest"
+ android:name="${applicationName}">
+ <activity
+ android:name=".MainActivity"
+ android:launchMode="singleTop"
+ android:theme="@style/LaunchTheme"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+ android:hardwareAccelerated="true"
+ android:windowSoftInputMode="adjustResize">
+ <!-- Specifies an Android theme to apply to this Activity as soon as
+ the Android process has started. This theme is visible to the user
+ while the Flutter UI initializes. After that, this theme continues
+ to determine the Window background behind the Flutter UI. -->
+ <meta-data
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme"
+ />
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <!-- Don't delete the meta-data below.
+ This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+ <meta-data
+ android:name="flutterEmbedding"
+ android:value="2" />
+ </application>
+ </manifest>
+ ''';
+
+ String get appStrings => r'''
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
+ ''';
+
+ String get appStyles => r'''
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Theme applied to the Android Window while the process is starting -->
+ <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+ <!-- Show a splash screen on the activity. Automatically removed when
+ Flutter draws its first frame -->
+ <item name="android:windowBackground">@drawable/launch_background</item>
+ </style>
+ <!-- Theme applied to the Android Window as soon as the process has started.
+ This theme determines the color of the Android Window while your
+ Flutter UI initializes, as well as behind your Flutter UI while its
+ running.
+
+ This Theme is only used starting with V2 of Flutter's Android embedding. -->
+ <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+ <item name="android:windowBackground">@android:color/white</item>
+ </style>
+</resources>
+ ''';
+
+ String get appLaunchBackground => r'''
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@android:color/white" />
+
+ <!-- You can insert your own image assets here -->
+ <!-- <item>
+ <bitmap
+ android:gravity="center"
+ android:src="@mipmap/launch_image" />
+ </item> -->
+</layer-list>
+ ''';
+
+ String get appMultidexApplication => r'''
+ // Generated file.
+ // If you wish to remove Flutter's multidex support, delete this entire file.
+
+ package io.flutter.app;
+
+ import android.content.Context;
+ import androidx.annotation.CallSuper;
+ import androidx.multidex.MultiDex;
+
+ /**
+ * Extension of {@link io.flutter.app.FlutterApplication}, adding multidex support.
+ */
+ public class FlutterMultiDexApplication extends FlutterApplication {
+ @Override
+ @CallSuper
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+ MultiDex.install(this);
+ }
+ }
+ ''';
+}