From 4b914bd17c13970489a8319084b2463993297ee8 Mon Sep 17 00:00:00 2001 From: hangyu Date: Fri, 12 Jan 2024 10:36:26 -0800 Subject: [PATCH] [deep link] Update a gradle task to add flag check and intent filter check to the AppLinkSettings (#141231) These check result is used in devtool deep link validation issue: https://github.com/flutter/flutter/issues/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]. [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 --- .../gradle/src/main/groovy/flutter.groovy | 38 +++++- ...gradle_outputs_app_link_settings_test.dart | 111 ++++++++++++++++-- 2 files changed, 137 insertions(+), 12 deletions(-) diff --git a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy index 375e293bd9db..4026c0d5b1f2 100644 --- a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy +++ b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy @@ -906,11 +906,36 @@ class FlutterPlugin implements Plugin { 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 def hosts = [] as Set def paths = [] as Set + 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) { @@ -939,10 +964,10 @@ class FlutterPlugin implements Plugin { 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)) } } } @@ -1421,14 +1446,21 @@ class FlutterPlugin implements Plugin { } class AppLinkSettings { - String applicationId Set 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() diff --git a/packages/flutter_tools/test/integration.shard/android_gradle_outputs_app_link_settings_test.dart b/packages/flutter_tools/test/integration.shard/android_gradle_outputs_app_link_settings_test.dart index 0d42ddbd94f0..d33eda511581 100644 --- a/packages/flutter_tools/test/integration.shard/android_gradle_outputs_app_link_settings_test.dart +++ b/packages/flutter_tools/test/integration.shard/android_gradle_outputs_app_link_settings_test.dart @@ -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(XmlName('name', 'android'), 'flutter_deeplinking_enabled'), + XmlAttribute(XmlName('value', 'android'), 'true'), + ], +); final XmlElement pureHttpIntentFilter = XmlElement( XmlName('intent-filter'), [XmlAttribute(XmlName('autoVerify', 'android'), 'true')], @@ -123,6 +129,69 @@ final XmlElement nonAutoVerifyIntentFilter = XmlElement( ), ], ); +final XmlElement nonActionIntentFilter = XmlElement( + XmlName('intent-filter'), + [XmlAttribute(XmlName('autoVerify', 'android'), 'true')], + [ + 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-action.com'), + ], + ), + ], +); +final XmlElement nonDefaultCategoryIntentFilter = 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.BROWSABLE')], + ), + XmlElement( + XmlName('data'), + [ + XmlAttribute(XmlName('scheme', 'android'), 'http'), + XmlAttribute(XmlName('host', 'android'), 'non-default-category.com'), + ], + ), + ], +); +final XmlElement nonBrowsableCategoryIntentFilter = 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('data'), + [ + XmlAttribute(XmlName('scheme', 'android'), 'http'), + XmlAttribute(XmlName('host', 'android'), 'non-browsable-category.com'), + ], + ), + ], +); void main() { late Directory tempDir; @@ -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; expect(deeplink['scheme'], scheme); expect(deeplink['host'], host); expect(deeplink['path'], path); + final Map intentFilterCheck = deeplink['intentFilterCheck'] as Map; + expect(intentFilterCheck['hasAutoVerify'], hasAutoVerify); + expect(intentFilterCheck['hasActionView'], hasActionView); + expect(intentFilterCheck['hasDefaultCategory'], hasDefaultCategory); + expect(intentFilterCheck['hasBrowsableCategory'], hasBrowsableCategory); } + testWithoutContext( 'gradle task outputsAppLinkSettings works when a project has app links', () async { // Create a new flutter project. @@ -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. @@ -188,17 +276,21 @@ void main() { expect(fileDump.existsSync(), true); final Map json = jsonDecode(fileDump.readAsStringSync()) as Map; expect(json['applicationId'], 'com.example.testapp'); + expect(json['deeplinkingFlagEnabled'], true); final List deeplinks = json['deeplinks']! as List; - 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 outputsAppLinkSettings works when a project does not have app link', () async { + 'gradle task outputsAppLinkSettings 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'); @@ -233,6 +325,7 @@ void main() { expect(fileDump.existsSync(), true); final Map json = jsonDecode(fileDump.readAsStringSync()) as Map; expect(json['applicationId'], 'com.example.testapp'); + expect(json['deeplinkingFlagEnabled'], false); final List deeplinks = json['deeplinks']! as List; expect(deeplinks.length, 0); });