diff --git a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy index f6ad3d9515a..798c19e4cb7 100644 --- a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy +++ b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy @@ -731,6 +731,85 @@ class FlutterPlugin implements Plugin { } } + // Add a task that can be called on Flutter projects that prints application id of a build + // variant. + // + // This task prints the application id in this format: + // + // ApplicationId: com.example.my_id + // + // Format of the output of this task is used by `AndroidProject.getApplicationIdForVariant`. + private static void addTasksForPrintApplicationId(Project project) { + project.android.applicationVariants.all { variant -> + // Warning: The name of this task is used by `AndroidProject.getApplicationIdForVariant`. + project.tasks.register("print${variant.name.capitalize()}ApplicationId") { + description "Prints out application id for the given build variant of this Android project" + doLast { + println "ApplicationId: ${variant.applicationId}"; + } + } + } + } + + // Add a task that can be called on Flutter projects that prints app link domains of a build + // variant. + // + // The app link domains refer to the host attributes of data tags in the apps' intent filters + // that support http/https schemes. See + // https://developer.android.com/guide/topics/manifest/intent-filter-element. + // + // This task prints app link domains in this format: + // + // Domain: domain.com + // Domain: another-domain.dev + // + // Format of the output of this task is used by `AndroidProject.getAppLinkDomainsForVariant`. + private static void addTasksForPrintAppLinkDomains(Project project) { + project.android.applicationVariants.all { variant -> + // Warning: The name of this task is used by `AndroidProject.getAppLinkDomainsForVariant`. + project.tasks.register("print${variant.name.capitalize()}AppLinkDomains") { + description "Prints out app links domain for the given build variant of this Android project" + variant.outputs.all { output -> + def processResources = output.hasProperty("processResourcesProvider") ? + output.processResourcesProvider.get() : output.processResources + dependsOn processResources.name + } + doLast { + variant.outputs.all { output -> + def processResources = output.hasProperty("processResourcesProvider") ? + output.processResourcesProvider.get() : output.processResources + def manifest = new XmlParser().parse(processResources.manifestFile) + manifest.application.activity.each { activity -> + // Find intent filters that have autoVerify = true and support http/https + // scheme. + activity.'intent-filter'.findAll { filter -> + def hasAutoVerify = filter.attributes().any { entry -> + return entry.key.getLocalPart() == "autoVerify" && entry.value + } + def hasHttpOrHttps = filter.data.any { data -> + data.attributes().any { entry -> + return entry.key.getLocalPart() == "scheme" && + (entry.value == "http" || entry.value == "https") + } + } + return hasAutoVerify && hasHttpOrHttps + }.each { appLinkIntent -> + // Print out the host attributes in data tags. + appLinkIntent.data.each { data -> + data.attributes().each { entry -> + if (entry.key.getLocalPart() == "host") { + println "Domain: ${entry.value}" + } + } + } + } + } + } + } + } + } + } + /** * Returns a Flutter build mode suitable for the specified Android buildType. * @@ -904,7 +983,11 @@ class FlutterPlugin implements Plugin { validateDeferredComponentsValue = project.property('validate-deferred-components').toBoolean() } addTaskForJavaVersion(project) - addTaskForPrintBuildVariants(project) + if(isFlutterAppProject()) { + addTaskForPrintBuildVariants(project) + addTasksForPrintApplicationId(project) + addTasksForPrintAppLinkDomains(project) + } def targetPlatforms = getTargetPlatforms() def addFlutterDeps = { variant -> if (shouldSplitPerAbi()) { diff --git a/packages/flutter_tools/lib/src/android/android_app_link_settings.dart b/packages/flutter_tools/lib/src/android/android_app_link_settings.dart new file mode 100644 index 00000000000..8769fd883f3 --- /dev/null +++ b/packages/flutter_tools/lib/src/android/android_app_link_settings.dart @@ -0,0 +1,22 @@ +// 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:meta/meta.dart'; + +/// A data class for app links related project settings. +/// +/// See https://developer.android.com/training/app-links. +@immutable +class AndroidAppLinkSettings { + const AndroidAppLinkSettings({ + required this.applicationId, + required this.domains, + }); + + /// The application id of the android sub-project. + final String applicationId; + + /// The associated web domains of the android sub-project. + final List domains; +} diff --git a/packages/flutter_tools/lib/src/android/android_builder.dart b/packages/flutter_tools/lib/src/android/android_builder.dart index 199a21778db..70a7d9ff3bd 100644 --- a/packages/flutter_tools/lib/src/android/android_builder.dart +++ b/packages/flutter_tools/lib/src/android/android_builder.dart @@ -42,4 +42,16 @@ abstract class AndroidBuilder { /// Returns a list of available build variant from the Android project. Future> getBuildVariants({required FlutterProject project}); + + /// Returns the application id for the given build variant. + Future getApplicationIdForVariant( + String buildVariant, { + required FlutterProject project, + }); + + /// Returns a list of app link domains for the given build variant. + Future> getAppLinkDomainsForVariant( + String buildVariant, { + required FlutterProject project, + }); } diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 9b3d9f875f9..6d1e3e7345b 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -38,9 +38,9 @@ import 'migrations/android_studio_java_gradle_conflict_migration.dart'; import 'migrations/top_level_gradle_build_file_migration.dart'; import 'multidex.dart'; -/// The regex to grab variant names from printVariants gradle task +/// The regex to grab variant names from printBuildVariants gradle task /// -/// The task is defined in flutter/packages/flutter_tools/gradle/flutter.gradle. +/// The task is defined in flutter/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy /// /// The expected output from the task should be similar to: /// @@ -49,6 +49,34 @@ import 'multidex.dart'; /// BuildVariant: profile final RegExp _kBuildVariantRegex = RegExp('^BuildVariant: (?<$_kBuildVariantRegexGroupName>.*)\$'); const String _kBuildVariantRegexGroupName = 'variant'; +const String _kBuildVariantTaskName = 'printBuildVariants'; + +/// The regex to grab variant names from print${BuildVariant}ApplicationId gradle task +/// +/// The task is defined in flutter/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy +/// +/// The expected output from the task should be similar to: +/// +/// ApplicationId: com.example.my_id +final RegExp _kApplicationIdRegex = RegExp('^ApplicationId: (?<$_kApplicationIdRegexGroupName>.*)\$'); +const String _kApplicationIdRegexGroupName = 'applicationId'; +String _getPrintApplicationIdTaskFor(String buildVariant) { + return _taskForBuildVariant('print', buildVariant, 'ApplicationId'); +} + +/// The regex to grab app link domains from print${BuildVariant}AppLinkDomains gradle task +/// +/// The task is defined in flutter/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy +/// +/// The expected output from the task should be similar to: +/// +/// Domain: domain.com +/// Domain: another-domain.dev +final RegExp _kAppLinkDomainsRegex = RegExp('^Domain: (?<$_kAppLinkDomainsGroupName>.*)\$'); +const String _kAppLinkDomainsGroupName = 'domain'; +String _getPrintAppLinkDomainsTaskFor(String buildVariant) { + return _taskForBuildVariant('print', buildVariant, 'AppLinkDomains'); +} /// The directory where the APK artifact is generated. Directory getApkDirectory(FlutterProject project) { @@ -89,9 +117,14 @@ Directory getRepoDirectory(Directory buildDirectory) { String _taskFor(String prefix, BuildInfo buildInfo) { final String buildType = camelCase(buildInfo.modeName); final String productFlavor = buildInfo.flavor ?? ''; - return '$prefix${sentenceCase(productFlavor)}${sentenceCase(buildType)}'; + return _taskForBuildVariant(prefix, '$productFlavor${sentenceCase(buildType)}'); } +String _taskForBuildVariant(String prefix, String buildVariant, [String suffix = '']) { + return '$prefix${sentenceCase(buildVariant)}$suffix'; +} + + /// Returns the task to build an APK. @visibleForTesting String getAssembleTaskFor(BuildInfo buildInfo) { @@ -240,6 +273,34 @@ class AndroidGradleBuilder implements AndroidBuilder { ); } + Future _runGradleTask( + String taskName, { + List options = const [], + required FlutterProject project + }) async { + final Status status = _logger.startProgress( + "Running Gradle task '$taskName'...", + ); + final List command = [ + _gradleUtils.getExecutable(project), + ...options, // suppresses gradle output. + taskName, + ]; + + RunResult result; + try { + result = await _processUtils.run( + command, + workingDirectory: project.android.hostAppGradleRoot.path, + allowReentrantFlutter: true, + environment: _java?.environment, + ); + } finally { + status.stop(); + } + return result; + } + /// Builds an app. /// /// * [project] is typically [FlutterProject.current()]. @@ -727,28 +788,14 @@ class AndroidGradleBuilder implements AndroidBuilder { @override Future> getBuildVariants({required FlutterProject project}) async { - final Status status = _logger.startProgress( - "Running Gradle task 'printBuildVariants'...", - ); - final List command = [ - _gradleUtils.getExecutable(project), - '-q', // suppresses gradle output. - 'printBuildVariants', - ]; - final Stopwatch sw = Stopwatch() ..start(); - RunResult result; - try { - result = await _processUtils.run( - command, - workingDirectory: project.android.hostAppGradleRoot.path, - allowReentrantFlutter: true, - environment: _java?.environment, - ); - } finally { - status.stop(); - } + final RunResult result = await _runGradleTask( + _kBuildVariantTaskName, + options: const ['-q'], + project: project, + ); + _usage.sendTiming('print', 'android build variants', sw.elapsed); if (result.exitCode != 0) { @@ -765,6 +812,65 @@ class AndroidGradleBuilder implements AndroidBuilder { } return options; } + + @override + Future getApplicationIdForVariant( + String buildVariant, { + required FlutterProject project, + }) async { + final String taskName = _getPrintApplicationIdTaskFor(buildVariant); + final Stopwatch sw = Stopwatch() + ..start(); + final RunResult result = await _runGradleTask( + taskName, + options: const ['-q'], + project: project, + ); + _usage.sendTiming('print', 'application id', sw.elapsed); + + if (result.exitCode != 0) { + _logger.printStatus(result.stdout, wrap: false); + _logger.printError(result.stderr, wrap: false); + return ''; + } + for (final String line in LineSplitter.split(result.stdout)) { + final RegExpMatch? match = _kApplicationIdRegex.firstMatch(line); + if (match != null) { + return match.namedGroup(_kApplicationIdRegexGroupName)!; + } + } + return ''; + } + + @override + Future> getAppLinkDomainsForVariant( + String buildVariant, { + required FlutterProject project, + }) async { + final String taskName = _getPrintAppLinkDomainsTaskFor(buildVariant); + final Stopwatch sw = Stopwatch() + ..start(); + final RunResult result = await _runGradleTask( + taskName, + options: const ['-q'], + project: project, + ); + _usage.sendTiming('print', 'application id', sw.elapsed); + + if (result.exitCode != 0) { + _logger.printStatus(result.stdout, wrap: false); + _logger.printError(result.stderr, wrap: false); + return const []; + } + final List domains = []; + for (final String line in LineSplitter.split(result.stdout)) { + final RegExpMatch? match = _kAppLinkDomainsRegex.firstMatch(line); + if (match != null) { + domains.add(match.namedGroup(_kAppLinkDomainsGroupName)!); + } + } + return domains; + } } /// Prints how to consume the AAR from a host app. diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 612620354eb..38a63ff55df 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -7,6 +7,7 @@ import 'package:xml/xml.dart'; import 'package:yaml/yaml.dart'; import '../src/convert.dart'; +import 'android/android_app_link_settings.dart'; import 'android/android_builder.dart'; import 'android/gradle_utils.dart' as gradle; import 'base/common.dart'; @@ -477,6 +478,7 @@ class AndroidProject extends FlutterProjectPlatform { /// Returns true if the current version of the Gradle plugin is supported. late final bool isSupportedVersion = _computeSupportedVersion(); + /// Gets all build variants of this project. Future> getBuildVariants() async { if (!existsSync() || androidBuilder == null) { return const []; @@ -484,6 +486,22 @@ class AndroidProject extends FlutterProjectPlatform { return androidBuilder!.getBuildVariants(project: parent); } + /// Returns app link related project settings for a given build variant. + /// + /// Use [getBuildVariants] to get all of the available build variants. + Future getAppLinksSettings({required String variant}) async { + if (!existsSync() || androidBuilder == null) { + return const AndroidAppLinkSettings( + applicationId: '', + domains: [], + ); + } + return AndroidAppLinkSettings( + applicationId: await androidBuilder!.getApplicationIdForVariant(variant, project: parent), + domains: await androidBuilder!.getAppLinkDomainsForVariant(variant, project: parent), + ); + } + bool _computeSupportedVersion() { final FileSystem fileSystem = hostAppGradleRoot.fileSystem; final File plugin = hostAppGradleRoot.childFile( diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index b7f09b82d4a..9f9d5966325 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:meta/meta.dart' show visibleForTesting; import 'package:vm_service/vm_service.dart' as vm_service; +import 'android/android_app_link_settings.dart'; import 'base/common.dart'; import 'base/context.dart'; import 'base/io.dart' as io; @@ -42,6 +43,7 @@ const String kFlutterGetSkSLServiceName = 'flutterGetSkSL'; const String kFlutterGetIOSBuildOptionsServiceName = 'flutterGetIOSBuildOptions'; const String kFlutterGetAndroidBuildVariantsServiceName = 'flutterGetAndroidBuildVariants'; const String kFlutterGetIOSUniversalLinkSettingsServiceName = 'flutterGetIOSUniversalLinkSettings'; +const String kFlutterGetAndroidAppLinkSettingsName = 'flutterGetAndroidAppLinkSettings'; /// The error response code from an unrecoverable compilation failure. const int kIsolateReloadBarred = 1005; @@ -365,6 +367,21 @@ Future setUpVmService({ registrationRequests.add( vmService.registerService(kFlutterGetIOSUniversalLinkSettingsServiceName, kFlutterToolAlias), ); + + vmService.registerServiceCallback(kFlutterGetAndroidAppLinkSettingsName, (Map params) async { + final String variant = params['variant']! as String; + final AndroidAppLinkSettings settings = await flutterProject.android.getAppLinksSettings(variant: variant); + return { + 'result': { + kResultType: kResultTypeSuccess, + 'applicationId': settings.applicationId, + 'domains': settings.domains, + }, + }; + }); + registrationRequests.add( + vmService.registerService(kFlutterGetAndroidAppLinkSettingsName, kFlutterToolAlias), + ); } if (printStructuredErrorLogMethod != null) { 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 92cb851de1b..b989d371f46 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 @@ -869,6 +869,133 @@ Gradle Crashed AndroidStudio: () => FakeAndroidStudio(), }); + testUsingContext('can call custom gradle task getApplicationIdForVariant and parse the result', () async { + final AndroidGradleBuilder builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + usage: testUsage, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand(const FakeCommand( + command: [ + 'gradlew', + '-q', + 'printFreeDebugApplicationId', + ], + stdout: ''' +ApplicationId: com.example.id + ''', + )); + final String actual = await builder.getApplicationIdForVariant( + 'freeDebug', + project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + ); + expect(actual, 'com.example.id'); + }, overrides: { + AndroidStudio: () => FakeAndroidStudio(), + }); + + testUsingContext('can call custom gradle task getApplicationIdForVariant with unknown crash', () async { + final AndroidGradleBuilder builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + usage: testUsage, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand(const FakeCommand( + command: [ + 'gradlew', + '-q', + 'printFreeDebugApplicationId', + ], + stdout: ''' +unknown crash + ''', + )); + final String actual = await builder.getApplicationIdForVariant( + 'freeDebug', + project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + ); + expect(actual, ''); + }, overrides: { + AndroidStudio: () => FakeAndroidStudio(), + }); + + testUsingContext('can call custom gradle task getAppLinkDomainsForVariant and parse the result', () async { + final AndroidGradleBuilder builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + usage: testUsage, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + + processManager.addCommand(const FakeCommand( + command: [ + 'gradlew', + '-q', + 'printFreeDebugAppLinkDomains', + ], + stdout: ''' +Domain: example.com +Domain: example2.com + ''', + )); + final List actual = await builder.getAppLinkDomainsForVariant( + 'freeDebug', + project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + ); + expect(actual, ['example.com', 'example2.com']); + }, overrides: { + AndroidStudio: () => FakeAndroidStudio(), + }); + + testUsingContext('can call custom gradle task getAppLinkDomainsForVariant with unknown crash', () async { + final AndroidGradleBuilder builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + usage: testUsage, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + + processManager.addCommand(const FakeCommand( + command: [ + 'gradlew', + '-q', + 'printFreeDebugAppLinkDomains', + ], + stdout: ''' +unknown crash + ''', + )); + final List actual = await builder.getAppLinkDomainsForVariant( + 'freeDebug', + project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + ); + expect(actual.isEmpty, isTrue); + }, overrides: { + AndroidStudio: () => FakeAndroidStudio(), + }); + testUsingContext("doesn't indicate how to consume an AAR when printHowToConsumeAar is false", () async { final AndroidGradleBuilder builder = AndroidGradleBuilder( java: FakeJava(), diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index aa50d13c406..9a307d567fc 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -86,6 +86,10 @@ const List kAttachIsolateExpectations = 'service': kFlutterGetIOSUniversalLinkSettingsServiceName, 'alias': kFlutterToolAlias, }), + FakeVmServiceRequest(method: 'registerService', args: { + 'service': kFlutterGetAndroidAppLinkSettingsName, + 'alias': kFlutterToolAlias, + }), FakeVmServiceRequest( method: 'streamListen', args: { diff --git a/packages/flutter_tools/test/integration.shard/android_gradle_print_app_link_domains_test.dart b/packages/flutter_tools/test/integration.shard/android_gradle_print_app_link_domains_test.dart new file mode 100644 index 00000000000..eed136cc72c --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/android_gradle_print_app_link_domains_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. + +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:collection/collection.dart'; +import 'package:file/file.dart'; +import 'package:flutter_tools/src/android/gradle_utils.dart' + show getGradlewFileName; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:xml/xml.dart'; + +import '../src/common.dart'; +import 'test_utils.dart'; + +final XmlElement pureHttpIntentFilter = XmlElement( + XmlName('intent-filter'), + [XmlAttribute(XmlName('autoVerify', 'android'), 'true')], + [ + XmlElement( + XmlName('action'), + [XmlAttribute(XmlName('name', 'android'), 'android.intent.action.VIEW')], + ), + XmlElement( + XmlName('category'), + [XmlAttribute(XmlName('name', 'android'), 'android.intent.category.DEFAULT')], + ), + XmlElement( + XmlName('category'), + [XmlAttribute(XmlName('name', 'android'), 'android.intent.category.BROWSABLE')], + ), + XmlElement( + XmlName('data'), + [ + XmlAttribute(XmlName('scheme', 'android'), 'http'), + XmlAttribute(XmlName('host', 'android'), 'pure-http.com'), + ], + ), + ], +); + +final XmlElement nonHttpIntentFilter = XmlElement( + XmlName('intent-filter'), + [XmlAttribute(XmlName('autoVerify', 'android'), 'true')], + [ + XmlElement( + XmlName('action'), + [XmlAttribute(XmlName('name', 'android'), 'android.intent.action.VIEW')], + ), + XmlElement( + XmlName('category'), + [XmlAttribute(XmlName('name', 'android'), 'android.intent.category.DEFAULT')], + ), + XmlElement( + XmlName('category'), + [XmlAttribute(XmlName('name', 'android'), 'android.intent.category.BROWSABLE')], + ), + XmlElement( + XmlName('data'), + [ + XmlAttribute(XmlName('scheme', 'android'), 'custom'), + XmlAttribute(XmlName('host', 'android'), 'custom.com'), + ], + ), + ], +); + +final XmlElement hybridIntentFilter = XmlElement( + XmlName('intent-filter'), + [XmlAttribute(XmlName('autoVerify', 'android'), 'true')], + [ + XmlElement( + XmlName('action'), + [XmlAttribute(XmlName('name', 'android'), 'android.intent.action.VIEW')], + ), + XmlElement( + XmlName('category'), + [XmlAttribute(XmlName('name', 'android'), 'android.intent.category.DEFAULT')], + ), + XmlElement( + XmlName('category'), + [XmlAttribute(XmlName('name', 'android'), 'android.intent.category.BROWSABLE')], + ), + XmlElement( + XmlName('data'), + [ + XmlAttribute(XmlName('scheme', 'android'), 'custom'), + XmlAttribute(XmlName('host', 'android'), 'hybrid.com'), + ], + ), + XmlElement( + XmlName('data'), + [ + XmlAttribute(XmlName('scheme', 'android'), 'http'), + ], + ), + ], +); + +final XmlElement nonAutoVerifyIntentFilter = XmlElement( + XmlName('intent-filter'), + [], + [ + XmlElement( + XmlName('action'), + [XmlAttribute(XmlName('name', 'android'), 'android.intent.action.VIEW')], + ), + XmlElement( + XmlName('category'), + [XmlAttribute(XmlName('name', 'android'), 'android.intent.category.DEFAULT')], + ), + XmlElement( + XmlName('category'), + [XmlAttribute(XmlName('name', 'android'), 'android.intent.category.BROWSABLE')], + ), + XmlElement( + XmlName('data'), + [ + XmlAttribute(XmlName('scheme', 'android'), 'http'), + XmlAttribute(XmlName('host', 'android'), 'non-auto-verify.com'), + ], + ), + ], +); + +void main() { + late Directory tempDir; + + setUp(() async { + tempDir = createResolvedTempDirectorySync('run_test.'); + }); + + tearDown(() async { + tryToDelete(tempDir); + }); + + testWithoutContext( + 'gradle task exists named printAppLinkDomains that prints app link domains', () async { + // Create a new flutter project. + final String flutterBin = + fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter'); + ProcessResult result = await processManager.run([ + flutterBin, + 'create', + tempDir.path, + '--project-name=testapp', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0); + // Adds intent filters for app links + final String androidManifestPath = fileSystem.path.join(tempDir.path, 'android', 'app', 'src', 'main', 'AndroidManifest.xml'); + final io.File androidManifestFile = io.File(androidManifestPath); + final XmlDocument androidManifest = XmlDocument.parse(androidManifestFile.readAsStringSync()); + final XmlElement activity = androidManifest.findAllElements('activity').first; + activity.children.add(pureHttpIntentFilter); + activity.children.add(nonHttpIntentFilter); + activity.children.add(hybridIntentFilter); + activity.children.add(nonAutoVerifyIntentFilter); + androidManifestFile.writeAsStringSync(androidManifest.toString(), flush: true); + + // Ensure that gradle files exists from templates. + result = await processManager.run([ + flutterBin, + 'build', + 'apk', + '--config-only', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0); + + final Directory androidApp = tempDir.childDirectory('android'); + result = await processManager.run([ + '.${platform.pathSeparator}${getGradlewFileName(platform)}', + ...getLocalEngineArguments(), + '-q', // quiet output. + 'printDebugAppLinkDomains', + ], workingDirectory: androidApp.path); + + expect(result.exitCode, 0); + + const List expectedLines = [ + // Should only pick up the pure and hybrid intent filters + 'Domain: pure-http.com', + 'Domain: hybrid.com', + ]; + final List actualLines = LineSplitter.split(result.stdout.toString()).toList(); + expect(const ListEquality().equals(actualLines, expectedLines), isTrue); + }); +} diff --git a/packages/flutter_tools/test/integration.shard/android_gradle_print_application_id_test.dart b/packages/flutter_tools/test/integration.shard/android_gradle_print_application_id_test.dart new file mode 100644 index 00000000000..563344660e0 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/android_gradle_print_application_id_test.dart @@ -0,0 +1,64 @@ +// 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 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:file/file.dart'; +import 'package:flutter_tools/src/android/gradle_utils.dart' + show getGradlewFileName; +import 'package:flutter_tools/src/base/io.dart'; + +import '../src/common.dart'; +import 'test_utils.dart'; + +void main() { + late Directory tempDir; + + setUp(() async { + tempDir = createResolvedTempDirectorySync('run_test.'); + }); + + tearDown(() async { + tryToDelete(tempDir); + }); + + testWithoutContext( + 'gradle task exists named printApplicationId that prints application id', () async { + // Create a new flutter project. + final String flutterBin = + fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter'); + ProcessResult result = await processManager.run([ + flutterBin, + 'create', + tempDir.path, + '--project-name=testapp', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0); + // Ensure that gradle files exists from templates. + result = await processManager.run([ + flutterBin, + 'build', + 'apk', + '--config-only', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0); + + final Directory androidApp = tempDir.childDirectory('android'); + result = await processManager.run([ + '.${platform.pathSeparator}${getGradlewFileName(platform)}', + ...getLocalEngineArguments(), + '-q', // quiet output. + 'printDebugApplicationId', + ], workingDirectory: androidApp.path); + // Verify that gradlew has a javaVersion task. + expect(result.exitCode, 0); + // Verify the format is a number on its own line. + const List expectedLines = [ + 'ApplicationId: com.example.testapp', + ]; + final List actualLines = LineSplitter.split(result.stdout.toString()).toList(); + expect(const ListEquality().equals(actualLines, expectedLines), isTrue); + }); +} diff --git a/packages/flutter_tools/test/src/android_common.dart b/packages/flutter_tools/test/src/android_common.dart index b0c90c01020..109eb0ef6d3 100644 --- a/packages/flutter_tools/test/src/android_common.dart +++ b/packages/flutter_tools/test/src/android_common.dart @@ -39,6 +39,12 @@ class FakeAndroidBuilder implements AndroidBuilder { @override Future> getBuildVariants({required FlutterProject project}) async => const []; + + @override + Future> getAppLinkDomainsForVariant(String buildVariant, {required FlutterProject project}) async => const []; + + @override + Future getApplicationIdForVariant(String buildVariant, {required FlutterProject project}) async => ''; } /// Creates a [FlutterProject] in a directory named [flutter_project]