Skip to content

Commit

Permalink
[deep link] Update a gradle task to add flag check and intent filter …
Browse files Browse the repository at this point in the history
…check to the AppLinkSettings (#141231)

These check result is used in devtool deep link validation
issue: flutter/flutter#120408

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
  • Loading branch information
hannah-hyj authored Jan 12, 2024
1 parent fd827e3 commit 4b914bd
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 12 deletions.
38 changes: 35 additions & 3 deletions packages/flutter_tools/gradle/src/main/groovy/flutter.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -906,11 +906,36 @@ class FlutterPlugin implements Plugin<Project> {
output.processResourcesProvider.get() : output.processResources
def manifest = new XmlParser().parse(processResources.manifestFile)
manifest.application.activity.each { activity ->
activity."meta-data".each { metadata ->
def nameAttribute = metadata.attributes().find { it.key == 'android:name' }?.value == 'flutter_deeplinking_enabled'
def valueAttribute = metadata.attributes().find { it.key == 'android:value' }?.value == 'true'
if (nameAttribute && valueAttribute) {
appLinkSettings.deeplinkingFlagEnabled = true
}
}
activity."intent-filter".each { appLinkIntent ->
// Print out the host attributes in data tags.
def schemes = [] as Set<String>
def hosts = [] as Set<String>
def paths = [] as Set<String>
def intentFilterCheck = new IntentFilterCheck()

if (appLinkIntent.attributes().find { it.key == 'android:autoVerify' }?.value == 'true') {
intentFilterCheck.hasAutoVerify = true
}
appLinkIntent.'action'.each { action ->
if (action.attributes().find { it.key == 'android:name' }?.value == 'android.intent.action.VIEW') {
intentFilterCheck.hasActionView = true
}
}
appLinkIntent.'category'.each { category ->
if (category.attributes().find { it.key == 'android:name' }?.value == 'android.intent.category.DEFAULT') {
intentFilterCheck.hasDefaultCategory = true
}
if (category.attributes().find { it.key == 'android:name' }?.value == 'android.intent.category.BROWSABLE') {
intentFilterCheck.hasBrowsableCategory = true
}
}
appLinkIntent.data.each { data ->
data.attributes().each { entry ->
if (entry.key instanceof QName) {
Expand Down Expand Up @@ -939,10 +964,10 @@ class FlutterPlugin implements Plugin<Project> {
schemes.each {scheme ->
hosts.each { host ->
if (!paths) {
appLinkSettings.deeplinks.add(new Deeplink(scheme: scheme, host: host, path: ".*"))
appLinkSettings.deeplinks.add(new Deeplink(scheme: scheme, host: host, path: ".*", intentFilterCheck: intentFilterCheck))
} else {
paths.each { path ->
appLinkSettings.deeplinks.add(new Deeplink(scheme: scheme, host: host, path: path))
appLinkSettings.deeplinks.add(new Deeplink(scheme: scheme, host: host, path: path, intentFilterCheck: intentFilterCheck))
}
}
}
Expand Down Expand Up @@ -1421,14 +1446,21 @@ class FlutterPlugin implements Plugin<Project> {
}

class AppLinkSettings {

String applicationId
Set<Deeplink> deeplinks
boolean deeplinkingFlagEnabled
}

class IntentFilterCheck {
boolean hasAutoVerify
boolean hasActionView
boolean hasDefaultCategory
boolean hasBrowsableCategory
}

class Deeplink {
String scheme, host, path
IntentFilterCheck intentFilterCheck
boolean equals(o) {
if (o == null)
throw new NullPointerException()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import 'package:xml/xml.dart';

import '../src/common.dart';
import 'test_utils.dart';

final XmlElement deeplinkFlagMetaData = XmlElement(
XmlName('meta-data'),
<XmlAttribute>[
XmlAttribute(XmlName('name', 'android'), 'flutter_deeplinking_enabled'),
XmlAttribute(XmlName('value', 'android'), 'true'),
],
);
final XmlElement pureHttpIntentFilter = XmlElement(
XmlName('intent-filter'),
<XmlAttribute>[XmlAttribute(XmlName('autoVerify', 'android'), 'true')],
Expand Down Expand Up @@ -123,6 +129,69 @@ final XmlElement nonAutoVerifyIntentFilter = XmlElement(
),
],
);
final XmlElement nonActionIntentFilter = XmlElement(
XmlName('intent-filter'),
<XmlAttribute>[XmlAttribute(XmlName('autoVerify', 'android'), 'true')],
<XmlElement>[
XmlElement(
XmlName('category'),
<XmlAttribute>[XmlAttribute(XmlName('name', 'android'), 'android.intent.category.DEFAULT')],
),
XmlElement(
XmlName('category'),
<XmlAttribute>[XmlAttribute(XmlName('name', 'android'), 'android.intent.category.BROWSABLE')],
),
XmlElement(
XmlName('data'),
<XmlAttribute>[
XmlAttribute(XmlName('scheme', 'android'), 'http'),
XmlAttribute(XmlName('host', 'android'), 'non-action.com'),
],
),
],
);
final XmlElement nonDefaultCategoryIntentFilter = XmlElement(
XmlName('intent-filter'),
<XmlAttribute>[XmlAttribute(XmlName('autoVerify', 'android'), 'true')],
<XmlElement>[
XmlElement(
XmlName('action'),
<XmlAttribute>[XmlAttribute(XmlName('name', 'android'), 'android.intent.action.VIEW')],
),
XmlElement(
XmlName('category'),
<XmlAttribute>[XmlAttribute(XmlName('name', 'android'), 'android.intent.category.BROWSABLE')],
),
XmlElement(
XmlName('data'),
<XmlAttribute>[
XmlAttribute(XmlName('scheme', 'android'), 'http'),
XmlAttribute(XmlName('host', 'android'), 'non-default-category.com'),
],
),
],
);
final XmlElement nonBrowsableCategoryIntentFilter = XmlElement(
XmlName('intent-filter'),
<XmlAttribute>[XmlAttribute(XmlName('autoVerify', 'android'), 'true')],
<XmlElement>[
XmlElement(
XmlName('action'),
<XmlAttribute>[XmlAttribute(XmlName('name', 'android'), 'android.intent.action.VIEW')],
),
XmlElement(
XmlName('category'),
<XmlAttribute>[XmlAttribute(XmlName('name', 'android'), 'android.intent.category.DEFAULT')],
),
XmlElement(
XmlName('data'),
<XmlAttribute>[
XmlAttribute(XmlName('scheme', 'android'), 'http'),
XmlAttribute(XmlName('host', 'android'), 'non-browsable-category.com'),
],
),
],
);

void main() {
late Directory tempDir;
Expand All @@ -135,13 +204,28 @@ void main() {
tryToDelete(tempDir);
});

void testDeeplink(dynamic deeplink, String scheme, String host, String path) {
void testDeeplink(
dynamic deeplink,
String scheme,
String host,
String path, {
required bool hasAutoVerify,
required bool hasActionView,
required bool hasDefaultCategory,
required bool hasBrowsableCategory,
}) {
deeplink as Map<String, dynamic>;
expect(deeplink['scheme'], scheme);
expect(deeplink['host'], host);
expect(deeplink['path'], path);
final Map<String, dynamic> intentFilterCheck = deeplink['intentFilterCheck'] as Map<String, dynamic>;
expect(intentFilterCheck['hasAutoVerify'], hasAutoVerify);
expect(intentFilterCheck['hasActionView'], hasActionView);
expect(intentFilterCheck['hasDefaultCategory'], hasDefaultCategory);
expect(intentFilterCheck['hasBrowsableCategory'], hasBrowsableCategory);
}


testWithoutContext(
'gradle task outputs<mode>AppLinkSettings works when a project has app links', () async {
// Create a new flutter project.
Expand All @@ -159,10 +243,14 @@ void main() {
final io.File androidManifestFile = io.File(androidManifestPath);
final XmlDocument androidManifest = XmlDocument.parse(androidManifestFile.readAsStringSync());
final XmlElement activity = androidManifest.findAllElements('activity').first;
activity.children.add(deeplinkFlagMetaData);
activity.children.add(pureHttpIntentFilter);
activity.children.add(nonHttpIntentFilter);
activity.children.add(hybridIntentFilter);
activity.children.add(nonAutoVerifyIntentFilter);
activity.children.add(nonActionIntentFilter);
activity.children.add(nonDefaultCategoryIntentFilter);
activity.children.add(nonBrowsableCategoryIntentFilter);
androidManifestFile.writeAsStringSync(androidManifest.toString(), flush: true);

// Ensure that gradle files exists from templates.
Expand All @@ -188,17 +276,21 @@ void main() {
expect(fileDump.existsSync(), true);
final Map<String, dynamic> json = jsonDecode(fileDump.readAsStringSync()) as Map<String, dynamic>;
expect(json['applicationId'], 'com.example.testapp');
expect(json['deeplinkingFlagEnabled'], true);
final List<dynamic> deeplinks = json['deeplinks']! as List<dynamic>;
expect(deeplinks.length, 5);
testDeeplink(deeplinks[0], 'http', 'pure-http.com', '.*');
testDeeplink(deeplinks[1], 'custom', 'custom.com', '.*');
testDeeplink(deeplinks[2], 'custom', 'hybrid.com', '.*');
testDeeplink(deeplinks[3], 'http', 'hybrid.com', '.*');
testDeeplink(deeplinks[4], 'http', 'non-auto-verify.com', '.*');
expect(deeplinks.length, 8);
testDeeplink(deeplinks[0], 'http', 'pure-http.com', '.*', hasAutoVerify:true, hasActionView: true, hasDefaultCategory:true, hasBrowsableCategory: true);
testDeeplink(deeplinks[1], 'custom', 'custom.com', '.*', hasAutoVerify:true, hasActionView: true, hasDefaultCategory:true, hasBrowsableCategory: true);
testDeeplink(deeplinks[2], 'custom', 'hybrid.com', '.*', hasAutoVerify:true, hasActionView: true, hasDefaultCategory:true, hasBrowsableCategory: true);
testDeeplink(deeplinks[3], 'http', 'hybrid.com', '.*', hasAutoVerify:true, hasActionView: true, hasDefaultCategory:true, hasBrowsableCategory: true);
testDeeplink(deeplinks[4], 'http', 'non-auto-verify.com', '.*', hasAutoVerify:false, hasActionView: true, hasDefaultCategory:true, hasBrowsableCategory: true);
testDeeplink(deeplinks[5], 'http', 'non-action.com', '.*', hasAutoVerify:true, hasActionView: false, hasDefaultCategory:true, hasBrowsableCategory: true);
testDeeplink(deeplinks[6], 'http', 'non-default-category.com', '.*', hasAutoVerify:true, hasActionView: true, hasDefaultCategory:false, hasBrowsableCategory: true);
testDeeplink(deeplinks[7], 'http', 'non-browsable-category.com', '.*', hasAutoVerify:true, hasActionView: true, hasDefaultCategory:true, hasBrowsableCategory: false);
});

testWithoutContext(
'gradle task outputs<mode>AppLinkSettings works when a project does not have app link', () async {
'gradle task outputs<mode>AppLinkSettings works when a project does not have app link and the flutter_deeplinking_enabled flag', () async {
// Create a new flutter project.
final String flutterBin =
fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter');
Expand Down Expand Up @@ -233,6 +325,7 @@ void main() {
expect(fileDump.existsSync(), true);
final Map<String, dynamic> json = jsonDecode(fileDump.readAsStringSync()) as Map<String, dynamic>;
expect(json['applicationId'], 'com.example.testapp');
expect(json['deeplinkingFlagEnabled'], false);
final List<dynamic> deeplinks = json['deeplinks']! as List<dynamic>;
expect(deeplinks.length, 0);
});
Expand Down

0 comments on commit 4b914bd

Please sign in to comment.