diff --git a/packages/flutter_tools/lib/src/android/application_package.dart b/packages/flutter_tools/lib/src/android/application_package.dart index 021ea10e73..eafea219a3 100644 --- a/packages/flutter_tools/lib/src/android/application_package.dart +++ b/packages/flutter_tools/lib/src/android/application_package.dart @@ -171,7 +171,10 @@ class AndroidApk extends ApplicationPackage implements PrebuiltApplicationPackag logger.printError('Please check ${manifest.path} for errors.'); return null; } - final String? packageId = manifests.first.getAttribute('package'); + + // Starting from AGP version 7.3, the `package` attribute in Manifest.xml + // can be replaced with the `namespace` attribute under the `android` section in `android/app/build.gradle`. + final String? packageId = manifests.first.getAttribute('package') ?? androidProject.namespace; String? launchActivity; for (final XmlElement activity in document.findAllElements('activity')) { diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 2d1a111717..9a69bd3bb8 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -424,6 +424,7 @@ class AndroidProject extends FlutterProjectPlatform { @override String get pluginConfigKey => AndroidPlugin.kConfigKey; + static final RegExp _androidNamespacePattern = RegExp('android {[\\S\\s]+namespace[\\s]+[\'"](.+)[\'"]'); static final RegExp _applicationIdPattern = RegExp('^\\s*applicationId\\s+[\'"](.*)[\'"]\\s*\$'); static final RegExp _kotlinPluginPattern = RegExp('^\\s*apply plugin\\:\\s+[\'"]kotlin-android[\'"]\\s*\$'); static final RegExp _groupPattern = RegExp('^\\s*group\\s+[\'"](.*)[\'"]\\s*\$'); @@ -486,9 +487,15 @@ class AndroidProject extends FlutterProjectPlatform { } File get appManifestFile { - return isUsingGradle - ? globals.fs.file(globals.fs.path.join(hostAppGradleRoot.path, 'app', 'src', 'main', 'AndroidManifest.xml')) - : hostAppGradleRoot.childFile('AndroidManifest.xml'); + if(isUsingGradle) { + return hostAppGradleRoot + .childDirectory('app') + .childDirectory('src') + .childDirectory('main') + .childFile('AndroidManifest.xml'); + } + + return hostAppGradleRoot.childFile('AndroidManifest.xml'); } File get gradleAppOutV1File => gradleAppOutV1Directory.childFile('app-debug.apk'); @@ -512,6 +519,19 @@ class AndroidProject extends FlutterProjectPlatform { return firstMatchInFile(gradleFile, _applicationIdPattern)?.group(1); } + /// Get the namespace for newer Android projects, + /// which replaces the `package` attribute in the Manifest.xml. + String? get namespace { + final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle'); + + if (!gradleFile.existsSync()) { + return null; + } + + // firstMatchInFile() reads per line but `_androidNamespacePattern` matches a multiline pattern. + return _androidNamespacePattern.firstMatch(gradleFile.readAsStringSync())?.group(1); + } + String? get group { final File gradleFile = hostAppGradleRoot.childFile('build.gradle'); return firstMatchInFile(gradleFile, _groupPattern)?.group(1); 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 54c4268e1e..f77a439d49 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 @@ -5,7 +5,9 @@ import 'package:archive/archive.dart'; import 'package:file/memory.dart'; import 'package:file_testing/file_testing.dart'; +import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/android/android_studio.dart'; +import 'package:flutter_tools/src/android/application_package.dart'; import 'package:flutter_tools/src/android/gradle.dart'; import 'package:flutter_tools/src/android/gradle_errors.dart'; import 'package:flutter_tools/src/android/gradle_utils.dart'; @@ -14,6 +16,8 @@ import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/process.dart'; +import 'package:flutter_tools/src/base/user_messages.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/project.dart'; @@ -23,6 +27,7 @@ import 'package:test/fake.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fake_process_manager.dart'; +import '../../src/fakes.dart'; void main() { group('gradle build', () { @@ -724,6 +729,64 @@ void main() { AndroidStudio: () => FakeAndroidStudio(), }); + testUsingContext('Uses namespace attribute if manifest lacks a package attribute', () async { + final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + final AndroidSdk sdk = FakeAndroidSdk(); + + fileSystem.directory(project.android.hostAppGradleRoot) + .childFile('build.gradle') + .createSync(recursive: true); + + fileSystem.directory(project.android.hostAppGradleRoot) + .childDirectory('app') + .childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync( +''' +apply from: irrelevant/flutter.gradle + +android { + namespace 'com.example.foo' +} +'''); + + fileSystem.directory(project.android.hostAppGradleRoot) + .childDirectory('app') + .childDirectory('src') + .childDirectory('main') + .childFile('AndroidManifest.xml') + ..createSync(recursive: true) + ..writeAsStringSync(r''' + + + + + + + + + + +'''); + + final AndroidApk? androidApk = await AndroidApk.fromAndroidProject( + project.android, + androidSdk: sdk, + fileSystem: fileSystem, + logger: logger, + processManager: processManager, + processUtils: ProcessUtils(processManager: processManager, logger: logger), + userMessages: UserMessages(), + buildInfo: const BuildInfo(BuildMode.debug, null, treeShakeIcons: false), + ); + + expect(androidApk?.id, 'com.example.foo'); + }); + testUsingContext("doesn't indicate how to consume an AAR when printHowToConsumeAar is false", () async { final AndroidGradleBuilder builder = AndroidGradleBuilder( logger: logger,