Force path parts in intent filter with deeplinks to have leading slash.
Bug: 241114745
Test: Added and verified affected atests pass
Change-Id: I1aa5148b1b0722e9fde33be6cbb6277ebfb6e10d
diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp
index 5cee17e..df09e47 100644
--- a/tools/aapt2/link/ManifestFixer.cpp
+++ b/tools/aapt2/link/ManifestFixer.cpp
@@ -30,6 +30,91 @@
namespace aapt {
+// This is to detect whether an <intent-filter> contains deeplink.
+// See https://developer.android.com/training/app-links/deep-linking.
+static bool HasDeepLink(xml::Element* intent_filter_el) {
+ xml::Element* action_el = intent_filter_el->FindChild({}, "action");
+ xml::Element* category_el = intent_filter_el->FindChild({}, "category");
+ xml::Element* data_el = intent_filter_el->FindChild({}, "data");
+ if (action_el == nullptr || category_el == nullptr || data_el == nullptr) {
+ return false;
+ }
+
+ // Deeplinks must specify the ACTION_VIEW intent action.
+ constexpr const char* action_view = "android.intent.action.VIEW";
+ if (intent_filter_el->FindChildWithAttribute({}, "action", xml::kSchemaAndroid, "name",
+ action_view) == nullptr) {
+ return false;
+ }
+
+ // Deeplinks must have scheme included in <data> tag.
+ xml::Attribute* data_scheme_attr = data_el->FindAttribute(xml::kSchemaAndroid, "scheme");
+ if (data_scheme_attr == nullptr || data_scheme_attr->value.empty()) {
+ return false;
+ }
+
+ // Deeplinks must include BROWSABLE category.
+ constexpr const char* category_browsable = "android.intent.category.BROWSABLE";
+ if (intent_filter_el->FindChildWithAttribute({}, "category", xml::kSchemaAndroid, "name",
+ category_browsable) == nullptr) {
+ return false;
+ }
+ return true;
+}
+
+static bool VerifyDeeplinkPathAttribute(xml::Element* data_el, android::SourcePathDiagnostics* diag,
+ const std::string& attr_name) {
+ xml::Attribute* attr = data_el->FindAttribute(xml::kSchemaAndroid, attr_name);
+ if (attr != nullptr && !attr->value.empty()) {
+ StringPiece attr_value = attr->value;
+ const char* startChar = attr_value.begin();
+ if (attr_name == "pathPattern") {
+ if (*startChar == '/' || *startChar == '.' || *startChar == '*') {
+ return true;
+ } else {
+ diag->Error(android::DiagMessage(data_el->line_number)
+ << "attribute 'android:" << attr_name << "' in <" << data_el->name
+ << "> tag has value of '" << attr_value
+ << "', it must be in a pattern start with '.' or '*', otherwise must start "
+ "with a leading slash '/'");
+ return false;
+ }
+ } else {
+ if (*startChar == '/') {
+ return true;
+ } else {
+ diag->Error(android::DiagMessage(data_el->line_number)
+ << "attribute 'android:" << attr_name << "' in <" << data_el->name
+ << "> tag has value of '" << attr_value
+ << "', it must start with a leading slash '/'");
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+static bool VerifyDeepLinkIntentAction(xml::Element* intent_filter_el,
+ android::SourcePathDiagnostics* diag) {
+ if (!HasDeepLink(intent_filter_el)) {
+ return true;
+ }
+
+ xml::Element* data_el = intent_filter_el->FindChild({}, "data");
+ if (data_el != nullptr) {
+ if (!VerifyDeeplinkPathAttribute(data_el, diag, "path")) {
+ return false;
+ }
+ if (!VerifyDeeplinkPathAttribute(data_el, diag, "pathPrefix")) {
+ return false;
+ }
+ if (!VerifyDeeplinkPathAttribute(data_el, diag, "pathPattern")) {
+ return false;
+ }
+ }
+ return true;
+}
+
static bool RequiredNameIsNotEmpty(xml::Element* el, android::SourcePathDiagnostics* diag) {
xml::Attribute* attr = el->FindAttribute(xml::kSchemaAndroid, "name");
if (attr == nullptr) {
@@ -323,6 +408,7 @@
// Common <intent-filter> actions.
xml::XmlNodeAction intent_filter_action;
+ intent_filter_action.Action(VerifyDeepLinkIntentAction);
intent_filter_action["action"].Action(RequiredNameIsNotEmpty);
intent_filter_action["category"].Action(RequiredNameIsNotEmpty);
intent_filter_action["data"];