Add multidex flag and automatic multidex support (#90944)
This commit is contained in:
parent
abfcc84e58
commit
1d9edde079
@ -215,6 +215,38 @@ class FlutterPlugin implements Plugin<Project> {
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
|
@ -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
|
@ -595,7 +595,8 @@ class AndroidDevice extends Device {
|
||||
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
|
||||
|
@ -28,6 +28,7 @@ import 'android_builder.dart';
|
||||
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 @@ class AndroidGradleBuilder implements AndroidBuilder {
|
||||
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 @@ class AndroidGradleBuilder implements AndroidBuilder {
|
||||
line: detectedGradleErrorLine!,
|
||||
project: project,
|
||||
usesAndroidX: usesAndroidX,
|
||||
multidexEnabled: androidBuildInfo.multidexEnabled,
|
||||
);
|
||||
|
||||
if (retries >= 1) {
|
||||
|
@ -7,10 +7,12 @@ import 'package:meta/meta.dart';
|
||||
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 @@ class GradleHandledError {
|
||||
required String line,
|
||||
required FlutterProject project,
|
||||
required bool usesAndroidX,
|
||||
required bool multidexEnabled,
|
||||
}) handler;
|
||||
|
||||
/// The [BuildEvent] label is named gradle-[eventLabel].
|
||||
@ -71,8 +74,104 @@ final List<GradleHandledError> gradleErrors = <GradleHandledError>[
|
||||
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 @@ final GradleHandledError permissionDeniedErrorHandler = GradleHandledError(
|
||||
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 @@ final GradleHandledError networkErrorHandler = GradleHandledError(
|
||||
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 @@ final GradleHandledError r8FailureHandler = GradleHandledError(
|
||||
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 @@ final GradleHandledError licenseNotAcceptedHandler = GradleHandledError(
|
||||
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 @@ final GradleHandledError flavorUndefinedHandler = GradleHandledError(
|
||||
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 @@ final GradleHandledError minSdkVersion = GradleHandledError(
|
||||
required String line,
|
||||
required FlutterProject project,
|
||||
required bool usesAndroidX,
|
||||
required bool multidexEnabled,
|
||||
}) async {
|
||||
final File gradleFile = project.directory
|
||||
.childDirectory('android')
|
||||
@ -314,6 +419,7 @@ final GradleHandledError transformInputIssue = GradleHandledError(
|
||||
required String line,
|
||||
required FlutterProject project,
|
||||
required bool usesAndroidX,
|
||||
required bool multidexEnabled,
|
||||
}) async {
|
||||
final File gradleFile = project.directory
|
||||
.childDirectory('android')
|
||||
@ -347,6 +453,7 @@ final GradleHandledError lockFileDepMissing = GradleHandledError(
|
||||
required String line,
|
||||
required FlutterProject project,
|
||||
required bool usesAndroidX,
|
||||
required bool multidexEnabled,
|
||||
}) async {
|
||||
final File gradleFile = project.directory
|
||||
.childDirectory('android')
|
||||
|
99
packages/flutter_tools/lib/src/android/multidex.dart
Normal file
99
packages/flutter_tools/lib/src/android/multidex.dart
Normal file
@ -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;
|
||||
}
|
@ -304,6 +304,7 @@ class AndroidBuildInfo {
|
||||
],
|
||||
this.splitPerAbi = false,
|
||||
this.fastStart = false,
|
||||
this.multidexEnabled = false,
|
||||
});
|
||||
|
||||
// The build info containing the mode and flavor.
|
||||
@ -321,6 +322,9 @@ class AndroidBuildInfo {
|
||||
|
||||
/// 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.
|
||||
|
@ -35,6 +35,7 @@ class BuildApkCommand extends BuildSubCommand {
|
||||
addNullSafetyModeOptions(hide: !verboseHelp);
|
||||
usesAnalyzeSizeFlag();
|
||||
addAndroidSpecificBuildOptions(hide: !verboseHelp);
|
||||
addMultidexOption();
|
||||
argParser
|
||||
..addFlag('split-per-abi',
|
||||
negatable: false,
|
||||
@ -99,9 +100,11 @@ class BuildApkCommand extends BuildSubCommand {
|
||||
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,
|
||||
|
@ -39,6 +39,7 @@ class BuildAppBundleCommand extends BuildSubCommand {
|
||||
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 @@ class BuildAppBundleCommand extends BuildSubCommand {
|
||||
|
||||
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 @@ class BuildAppBundleCommand extends BuildSubCommand {
|
||||
|
||||
validateBuild(androidBuildInfo);
|
||||
displayNullSafetyMode(androidBuildInfo.buildInfo);
|
||||
globals.terminal.usesTerminalUi = true;
|
||||
await androidBuilder.buildAab(
|
||||
project: FlutterProject.current(),
|
||||
target: targetFile,
|
||||
|
@ -65,6 +65,7 @@ class DriveCommand extends RunCommandBase {
|
||||
// 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 @@ class DriveCommand extends RunCommandBase {
|
||||
'trace-startup': traceStartup,
|
||||
if (web)
|
||||
'--no-launch-chrome': true,
|
||||
if (boolArg('multidex'))
|
||||
'multidex': true,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
|
@ -249,6 +249,7 @@ class RunCommand extends RunCommandBase {
|
||||
// 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 @@ class RunCommand extends RunCommandBase {
|
||||
dillOutputPath: stringArg('output-dill'),
|
||||
stayResident: stayResident,
|
||||
ipv6: ipv6,
|
||||
multidexEnabled: boolArg('multidex'),
|
||||
);
|
||||
} else if (webMode) {
|
||||
return webRunnerFactory.createWebRunner(
|
||||
|
@ -416,7 +416,9 @@ class FlutterDevice {
|
||||
}
|
||||
devFSWriter = device.createDevFSWriter(package, userIdentifier);
|
||||
|
||||
final Map<String, dynamic> platformArgs = <String, dynamic>{};
|
||||
final Map<String, dynamic> platformArgs = <String, dynamic>{
|
||||
'multidex': hotRunner.multidexEnabled,
|
||||
};
|
||||
|
||||
await startEchoingDeviceLog();
|
||||
|
||||
|
@ -93,6 +93,7 @@ class HotRunner extends ResidentRunner {
|
||||
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 @@ class HotRunner extends ResidentRunner {
|
||||
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
|
||||
|
@ -818,6 +818,16 @@ abstract class FlutterCommand extends Command<void> {
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -2,6 +2,7 @@
|
||||
package="{{androidIdentifier}}">
|
||||
<application
|
||||
android:label="{{projectName}}"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
@ -157,6 +157,7 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
'-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',
|
||||
|
@ -56,6 +56,7 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
String line,
|
||||
FlutterProject project,
|
||||
bool usesAndroidX,
|
||||
bool multidexEnabled
|
||||
}) async {
|
||||
handlerCalled = true;
|
||||
return GradleBuildStatus.exit;
|
||||
@ -142,6 +144,7 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
String line,
|
||||
FlutterProject project,
|
||||
bool usesAndroidX,
|
||||
bool multidexEnabled
|
||||
}) async {
|
||||
return GradleBuildStatus.retry;
|
||||
},
|
||||
@ -243,6 +248,7 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
String line,
|
||||
FlutterProject project,
|
||||
bool usesAndroidX,
|
||||
bool multidexEnabled
|
||||
}) async {
|
||||
handlerCalled = true;
|
||||
return GradleBuildStatus.exit;
|
||||
@ -329,6 +336,7 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
String line,
|
||||
FlutterProject project,
|
||||
bool usesAndroidX,
|
||||
bool multidexEnabled
|
||||
}) async {
|
||||
return GradleBuildStatus.retry;
|
||||
},
|
||||
@ -488,6 +499,7 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
'-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 @@ void main() {
|
||||
'--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',
|
||||
|
@ -9,7 +9,9 @@ import 'package:file_testing/file_testing.dart';
|
||||
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 @@ void main() {
|
||||
minSdkVersion,
|
||||
transformInputIssue,
|
||||
lockFileDepMissing,
|
||||
multidexErrorHandler,
|
||||
])
|
||||
);
|
||||
});
|
||||
@ -329,6 +332,213 @@ A problem occurred configuring root project 'android'.
|
||||
});
|
||||
});
|
||||
|
||||
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 @@ class FakeGradleUtils extends GradleUtils {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
});
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
''';
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user