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"];
diff --git a/tools/aapt2/link/ManifestFixer_test.cpp b/tools/aapt2/link/ManifestFixer_test.cpp
index 098d0be..cec9a1a 100644
--- a/tools/aapt2/link/ManifestFixer_test.cpp
+++ b/tools/aapt2/link/ManifestFixer_test.cpp
@@ -1068,4 +1068,345 @@
</manifest>)";
EXPECT_THAT(Verify(input), NotNull());
}
+
+TEST_F(ManifestFixerTest, IntentFilterActionMustHaveNonEmptyName) {
+ std::string input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), IsNull());
+
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), IsNull());
+
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), NotNull());
+}
+
+TEST_F(ManifestFixerTest, IntentFilterCategoryMustHaveNonEmptyName) {
+ std::string input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <category android:name="" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), IsNull());
+
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <category />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), IsNull());
+
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), NotNull());
+}
+
+TEST_F(ManifestFixerTest, IntentFilterPathMustStartWithLeadingSlashOnDeepLinks) {
+ // No DeepLink.
+ std::string input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <data />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), NotNull());
+
+ // No DeepLink, missing ACTION_VIEW.
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http"
+ android:host="www.example.com"
+ android:pathPrefix="pathPattern" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), NotNull());
+
+ // DeepLink, missing DEFAULT category while DEFAULT is recommended but not required.
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http"
+ android:host="www.example.com"
+ android:pathPrefix="pathPattern" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), IsNull());
+
+ // No DeepLink, missing BROWSABLE category.
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="http"
+ android:host="www.example.com"
+ android:pathPrefix="pathPattern" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), NotNull());
+
+ // No DeepLink, missing 'android:scheme' in <data> tag.
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:host="www.example.com"
+ android:pathPrefix="pathPattern" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), NotNull());
+
+ // No DeepLink, <action> is ACTION_MAIN not ACTION_VIEW.
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http"
+ android:host="www.example.com"
+ android:pathPrefix="pathPattern" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), NotNull());
+
+ // DeepLink with no leading slash in android:path.
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http"
+ android:host="www.example.com"
+ android:path="path" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), IsNull());
+
+ // DeepLink with leading slash in android:path.
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http"
+ android:host="www.example.com"
+ android:path="/path" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), NotNull());
+
+ // DeepLink with no leading slash in android:pathPrefix.
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http"
+ android:host="www.example.com"
+ android:pathPrefix="pathPrefix" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), IsNull());
+
+ // DeepLink with leading slash in android:pathPrefix.
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http"
+ android:host="www.example.com"
+ android:pathPrefix="/pathPrefix" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), NotNull());
+
+ // DeepLink with no leading slash in android:pathPattern.
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http"
+ android:host="www.example.com"
+ android:pathPattern="pathPattern" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), IsNull());
+
+ // DeepLink with leading slash in android:pathPattern.
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http"
+ android:host="www.example.com"
+ android:pathPattern="/pathPattern" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), NotNull());
+
+ // DeepLink with '.' start in pathPattern.
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http"
+ android:host="www.example.com"
+ android:pathPattern=".*\\.pathPattern" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), NotNull());
+
+ // DeepLink with '*' start in pathPattern.
+ input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http"
+ android:host="www.example.com"
+ android:pathPattern="*" />
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>)";
+ EXPECT_THAT(Verify(input), NotNull());
+}
} // namespace aapt