// 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:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/android/android_studio.dart'; import 'package:flutter_tools/src/android/gradle_utils.dart'; import 'package:flutter_tools/src/android/migrations/android_studio_java_gradle_conflict_migration.dart'; import 'package:flutter_tools/src/android/migrations/min_sdk_version_migration.dart'; import 'package:flutter_tools/src/android/migrations/top_level_gradle_build_file_migration.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fakes.dart'; const String otherGradleVersionWrapper = r''' distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists '''; const String gradleWrapperToMigrate = r''' distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists '''; const String gradleWrapperToMigrateTo = r''' distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists '''; String sampleModuleGradleBuildFile(String minSdkVersionString) { return r''' plugins { id "com.android.application" id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" } def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } android { namespace "com.example.asset_sample" compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion 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.asset_sample" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. ''' + minSdkVersionString + r''' targetSdkVersion flutter.targetSdkVersion 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 {} '''; } final Version androidStudioDolphin = Version(2021, 3, 1); const Version _javaVersion17 = Version.withText(17, 0, 2, 'openjdk 17.0.2'); const Version _javaVersion16 = Version.withText(16, 0, 2, 'openjdk 16.0.2'); void main() { group('Android migration', () { group('migrate the Gradle "clean" task to lazy declaration', () { late MemoryFileSystem memoryFileSystem; late BufferLogger bufferLogger; late FakeAndroidProject project; late File topLevelGradleBuildFile; setUp(() { memoryFileSystem = MemoryFileSystem.test(); bufferLogger = BufferLogger.test(); project = FakeAndroidProject( root: memoryFileSystem.currentDirectory.childDirectory('android')..createSync(), ); topLevelGradleBuildFile = project.hostAppGradleRoot.childFile('build.gradle'); }); testUsingContext('skipped if files are missing', () { final TopLevelGradleBuildFileMigration androidProjectMigration = TopLevelGradleBuildFileMigration( project, bufferLogger, ); androidProjectMigration.migrate(); expect(topLevelGradleBuildFile.existsSync(), isFalse); expect(bufferLogger.traceText, contains('Top-level Gradle build file not found, skipping migration of task "clean".')); }); testUsingContext('skipped if nothing to upgrade', () { topLevelGradleBuildFile.writeAsStringSync(''' tasks.register("clean", Delete) { delete rootProject.buildDir } '''); final TopLevelGradleBuildFileMigration androidProjectMigration = TopLevelGradleBuildFileMigration( project, bufferLogger, ); final DateTime previousLastModified = topLevelGradleBuildFile.lastModifiedSync(); androidProjectMigration.migrate(); expect(topLevelGradleBuildFile.lastModifiedSync(), previousLastModified); }); testUsingContext('top-level build.gradle is migrated', () { topLevelGradleBuildFile.writeAsStringSync(''' task clean(type: Delete) { delete rootProject.buildDir } '''); final TopLevelGradleBuildFileMigration androidProjectMigration = TopLevelGradleBuildFileMigration( project, bufferLogger, ); androidProjectMigration.migrate(); expect(bufferLogger.traceText, contains('Migrating "clean" Gradle task to lazy declaration style.')); expect(topLevelGradleBuildFile.readAsStringSync(), equals(''' tasks.register("clean", Delete) { delete rootProject.buildDir } ''')); }); }); group('migrate the gradle version to one that does not conflict with the ' 'Android Studio-provided java version', () { late MemoryFileSystem memoryFileSystem; late BufferLogger bufferLogger; late FakeAndroidProject project; late File gradleWrapperPropertiesFile; setUp(() { memoryFileSystem = MemoryFileSystem.test(); bufferLogger = BufferLogger.test(); project = FakeAndroidProject( root: memoryFileSystem.currentDirectory.childDirectory('android')..createSync(), ); project.hostAppGradleRoot.childDirectory(gradleDirectoryName) .childDirectory(gradleWrapperDirectoryName) .createSync(recursive: true); gradleWrapperPropertiesFile = project.hostAppGradleRoot .childDirectory(gradleDirectoryName) .childDirectory(gradleWrapperDirectoryName) .childFile(gradleWrapperPropertiesFilename); }); testWithoutContext('skipped if files are missing', () { final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration( java: FakeJava(version: _javaVersion17), bufferLogger, project: project, androidStudio: FakeAndroidStudio(version: androidStudioDolphin), ); migration.migrate(); expect(gradleWrapperPropertiesFile.existsSync(), isFalse); expect(bufferLogger.traceText, contains(gradleWrapperNotFound)); }); testWithoutContext('skipped if android studio is null', () { final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration( java: FakeJava(version: _javaVersion17), bufferLogger, project: project, ); gradleWrapperPropertiesFile.writeAsStringSync(gradleWrapperToMigrate); migration.migrate(); expect(bufferLogger.traceText, contains(androidStudioNotFound)); expect(gradleWrapperPropertiesFile.readAsStringSync(), gradleWrapperToMigrate); }); testWithoutContext('skipped if android studio version is null', () { final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration( java: FakeJava(version: _javaVersion17), bufferLogger, project: project, androidStudio: FakeAndroidStudio(version: null), ); gradleWrapperPropertiesFile.writeAsStringSync(gradleWrapperToMigrate); migration.migrate(); expect(bufferLogger.traceText, contains(androidStudioNotFound)); expect(gradleWrapperPropertiesFile.readAsStringSync(), gradleWrapperToMigrate); }); testWithoutContext('skipped if error is encountered in migrate()', () { final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration( java: FakeErroringJava(), bufferLogger, project: project, androidStudio: FakeAndroidStudio(version: androidStudioFlamingo), ); gradleWrapperPropertiesFile.writeAsStringSync(gradleWrapperToMigrate); migration.migrate(); expect(bufferLogger.traceText, contains(errorWhileMigrating)); expect(gradleWrapperPropertiesFile.readAsStringSync(), gradleWrapperToMigrate); }); testWithoutContext('skipped if android studio version is less than flamingo', () { final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration( java: FakeJava(), bufferLogger, project: project, androidStudio: FakeAndroidStudio(version: androidStudioDolphin), ); gradleWrapperPropertiesFile.writeAsStringSync(gradleWrapperToMigrate); migration.migrate(); expect(gradleWrapperPropertiesFile.readAsStringSync(), gradleWrapperToMigrate); expect(bufferLogger.traceText, contains(androidStudioVersionBelowFlamingo)); }); testWithoutContext('skipped if bundled java version is less than 17', () { final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration( java: FakeJava(version: _javaVersion16), bufferLogger, project: project, androidStudio: FakeAndroidStudio(version: androidStudioFlamingo), ); gradleWrapperPropertiesFile.writeAsStringSync(gradleWrapperToMigrate); migration.migrate(); expect(gradleWrapperPropertiesFile.readAsStringSync(), gradleWrapperToMigrate); expect(bufferLogger.traceText, contains(javaVersionNot17)); }); testWithoutContext('nothing is changed if gradle version not one that was ' 'used by flutter create', () { final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration( java: FakeJava(version: _javaVersion17), bufferLogger, project: project, androidStudio: FakeAndroidStudio(version: androidStudioFlamingo), ); gradleWrapperPropertiesFile.writeAsStringSync(otherGradleVersionWrapper); migration.migrate(); expect(gradleWrapperPropertiesFile.readAsStringSync(), otherGradleVersionWrapper); expect(bufferLogger.traceText, isEmpty); }); testWithoutContext('change is made with one of the specific gradle versions' ' we migrate for', () { final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration( java: FakeJava(version: _javaVersion17), bufferLogger, project: project, androidStudio: FakeAndroidStudio(version: androidStudioFlamingo), ); gradleWrapperPropertiesFile.writeAsStringSync(gradleWrapperToMigrate); migration.migrate(); expect(gradleWrapperPropertiesFile.readAsStringSync(), gradleWrapperToMigrateTo); expect(bufferLogger.statusText, contains('Conflict detected between ' 'Android Studio Java version and Gradle version, upgrading Gradle ' 'version from 6.7 to $gradleVersion7_6_1.')); }); testWithoutContext('change is not made when opt out flag is set', () { final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration( java: FakeJava(version: _javaVersion17), bufferLogger, project: project, androidStudio: FakeAndroidStudio(version: androidStudioFlamingo), ); gradleWrapperPropertiesFile.writeAsStringSync(gradleWrapperToMigrate + optOutFlag); migration.migrate(); expect(gradleWrapperPropertiesFile.readAsStringSync(), gradleWrapperToMigrate + optOutFlag); expect(bufferLogger.traceText, contains(optOutFlagEnabled)); }); }); group('migrate min sdk versions less than 19 to flutter.minSdkVersion ' 'when in a FlutterProject that is an app', () { late MemoryFileSystem memoryFileSystem; late BufferLogger bufferLogger; late FakeAndroidProject project; late MinSdkVersionMigration migration; setUp(() { memoryFileSystem = MemoryFileSystem.test(); memoryFileSystem.currentDirectory.childDirectory('android').createSync(); bufferLogger = BufferLogger.test(); project = FakeAndroidProject( root: memoryFileSystem.currentDirectory.childDirectory('android'), ); project.appGradleFile.parent.createSync(recursive: true); migration = MinSdkVersionMigration( project, bufferLogger ); }); testWithoutContext('do nothing when files missing', () { migration.migrate(); expect(bufferLogger.traceText, contains(appGradleNotFoundWarning)); }); testWithoutContext('replace when api 16', () { const String minSdkVersion16 = 'minSdkVersion 16'; project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion16)); migration.migrate(); expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText)); }); testWithoutContext('replace when api 17', () { const String minSdkVersion17 = 'minSdkVersion 17'; project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion17)); migration.migrate(); expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText)); }); testWithoutContext('replace when api 18', () { const String minSdkVersion18 = 'minSdkVersion 18'; project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion18)); migration.migrate(); expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText)); }); testWithoutContext('do nothing when >=api 19', () { const String minSdkVersion19 = 'minSdkVersion 19'; project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion19)); migration.migrate(); expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(minSdkVersion19)); }); testWithoutContext('do nothing when already using ' 'flutter.minSdkVersion', () { project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(replacementMinSdkText)); migration.migrate(); expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText)); }); testWithoutContext('avoid rewriting comments', () { const String code = '// minSdkVersion 16 // old default\n' ' minSdkVersion 23 // new version'; project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(code)); migration.migrate(); expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(code)); }); testWithoutContext('do nothing when project is a module', () { project = FakeAndroidProject( root: memoryFileSystem.currentDirectory.childDirectory('android'), module: true, ); migration = MinSdkVersionMigration( project, bufferLogger ); const String minSdkVersion16 = 'minSdkVersion 16'; project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion16)); migration.migrate(); expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(minSdkVersion16)); }); testWithoutContext('do nothing when minSdkVersion is set ' 'to a constant', () { const String minSdkVersionConstant = 'minSdkVersion kMinSdkversion'; project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersionConstant)); migration.migrate(); expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(minSdkVersionConstant)); }); testWithoutContext('do nothing when minSdkVersion is set ' 'using = syntax', () { const String equalsSyntaxMinSdkVersion16 = 'minSdkVersion = 16'; project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(equalsSyntaxMinSdkVersion16)); migration.migrate(); expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(equalsSyntaxMinSdkVersion16)); }); }); }); } class FakeAndroidProject extends Fake implements AndroidProject { FakeAndroidProject({required Directory root, this.module, this.plugin}) : hostAppGradleRoot = root; @override Directory hostAppGradleRoot; final bool? module; final bool? plugin; @override bool get isPlugin => plugin ?? false; @override bool get isModule => module ?? false; @override File get appGradleFile => hostAppGradleRoot.childDirectory('app').childFile('build.gradle'); } class FakeAndroidStudio extends Fake implements AndroidStudio { FakeAndroidStudio({required Version? version}) { _version = version; } late Version? _version; @override Version? get version => _version; } class FakeErroringJava extends FakeJava { @override Version get version { throw Exception('How did this happen?'); } }