Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion packages/flutter_tools/gradle/src/main/groovy/flutter.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,85 @@ class FlutterPlugin implements Plugin<Project> {
}
}

// 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") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think according to https://developer.android.com/reference/tools/gradle-api/7.4/com/android/build/api/variant/ApplicationVariant#applicationId() this depends on some task running that will merge the manifests. That means this task depends on another task (possibly as simple as assembleVariant). Given this I think you should add a dependsOn block as defined here. https://docs.gradle.org/current/userguide/tutorial_using_tasks.html#sec:task_dependencies

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you might actually want variant.outputs.each see example here https://developer.android.com/build/gradle-tips#configure-dynamic-version-codes to print the applicationId.
But I am not sure how task registration interacts differently if you use outputs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW I am trying to give good guidance here if you explore these things and they dont work you can ignore my comments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like it is not depending on any task, I ran it after a clean, and it still works. Also the application id is per applicationvariant I think? i didn't see a property on variantoutput for 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.
*
Expand Down Expand Up @@ -904,7 +983,11 @@ class FlutterPlugin implements Plugin<Project> {
validateDeferredComponentsValue = project.property('validate-deferred-components').toBoolean()
}
addTaskForJavaVersion(project)
addTaskForPrintBuildVariants(project)
if(isFlutterAppProject()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need this check to avoid crash when running gradle in flutter modules that are use in add to app scenario

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is because you are depending on steps that only exist for apps. But I would expect add to app to work and potentially problems with libraries.

Does this need to be cherry picked into either beta or stable?

Copy link
Contributor Author

@chunhtai chunhtai May 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for add to app, these tasks can only be added in the main android project.

I think in the future I should refactor this out into a separate gradle plugin to support add-to-app, but I would like to make the decision later when we have more concrete design for add-to-app support in deeplink validation tool.

This doesn't need to be cherry pick as there isn't a client using these tasks yet.

addTaskForPrintBuildVariants(project)
addTasksForPrintApplicationId(project)
addTasksForPrintAppLinkDomains(project)
}
def targetPlatforms = getTargetPlatforms()
def addFlutterDeps = { variant ->
if (shouldSplitPerAbi()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> domains;
}
12 changes: 12 additions & 0 deletions packages/flutter_tools/lib/src/android/android_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,16 @@ abstract class AndroidBuilder {

/// Returns a list of available build variant from the Android project.
Future<List<String>> getBuildVariants({required FlutterProject project});

/// Returns the application id for the given build variant.
Future<String> getApplicationIdForVariant(
String buildVariant, {
required FlutterProject project,
});

/// Returns a list of app link domains for the given build variant.
Future<List<String>> getAppLinkDomainsForVariant(
String buildVariant, {
required FlutterProject project,
});
}
152 changes: 129 additions & 23 deletions packages/flutter_tools/lib/src/android/gradle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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:
///
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -240,6 +273,34 @@ class AndroidGradleBuilder implements AndroidBuilder {
);
}

Future<RunResult> _runGradleTask(
String taskName, {
List<String> options = const <String>[],
required FlutterProject project
}) async {
final Status status = _logger.startProgress(
"Running Gradle task '$taskName'...",
);
final List<String> command = <String>[
_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()].
Expand Down Expand Up @@ -727,28 +788,14 @@ class AndroidGradleBuilder implements AndroidBuilder {

@override
Future<List<String>> getBuildVariants({required FlutterProject project}) async {
final Status status = _logger.startProgress(
"Running Gradle task 'printBuildVariants'...",
);
final List<String> command = <String>[
_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 <String>['-q'],
project: project,
);

_usage.sendTiming('print', 'android build variants', sw.elapsed);

if (result.exitCode != 0) {
Expand All @@ -765,6 +812,65 @@ class AndroidGradleBuilder implements AndroidBuilder {
}
return options;
}

@override
Future<String> 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 <String>['-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<List<String>> 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 <String>['-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 <String>[];
}
final List<String> domains = <String>[];
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.
Expand Down
18 changes: 18 additions & 0 deletions packages/flutter_tools/lib/src/project.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -477,13 +478,30 @@ 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<List<String>> getBuildVariants() async {
if (!existsSync() || androidBuilder == null) {
return const <String>[];
}
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<AndroidAppLinkSettings> getAppLinksSettings({required String variant}) async {
if (!existsSync() || androidBuilder == null) {
return const AndroidAppLinkSettings(
applicationId: '',
domains: <String>[],
);
}
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(
Expand Down
Loading