From db2fb15eb894954f6af3be8c9331c0cc1c5c5e94 Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 5 Nov 2025 17:00:45 +0100 Subject: [PATCH] Fix the logic for CustomSchemeDetector to comply with the documentation. --- .../lint/checks/CustomSchemeDetector.kt | 103 ++++++++++------ .../lint/checks/CustomSchemeDetectorTest.kt | 112 ++++++++++++------ 2 files changed, 144 insertions(+), 71 deletions(-) diff --git a/checks/src/main/java/com/example/lint/checks/CustomSchemeDetector.kt b/checks/src/main/java/com/example/lint/checks/CustomSchemeDetector.kt index 86f4bcb..8e21ad7 100644 --- a/checks/src/main/java/com/example/lint/checks/CustomSchemeDetector.kt +++ b/checks/src/main/java/com/example/lint/checks/CustomSchemeDetector.kt @@ -17,6 +17,9 @@ package com.example.lint.checks import com.android.SdkConstants.ANDROID_URI import com.android.SdkConstants.ATTR_AUTO_VERIFY +import com.android.SdkConstants.ATTR_SCHEME +import com.android.SdkConstants.TAG_ACTION +import com.android.SdkConstants.TAG_CATEGORY import com.android.SdkConstants.TAG_DATA import com.android.SdkConstants.TAG_INTENT_FILTER import com.android.tools.lint.detector.api.Category @@ -31,68 +34,96 @@ import com.android.tools.lint.detector.api.XmlScanner import com.android.utils.childrenIterator import org.w3c.dom.Element -/** - * Detector flagging whether the application has issues verifying custom schemes (i.e. not http/https/file/ftp/ftps). - */ class CustomSchemeDetector : Detector(), XmlScanner { override fun getApplicableElements() = setOf(TAG_INTENT_FILTER) override fun visitElement(context: XmlContext, element: Element) { - val autoVerifyAttribute = element.getAttributeNS(ANDROID_URI, ATTR_AUTO_VERIFY) + if (!hasViewAction(element) || + !hasBrowsableCategory(element) || + !hasDefaultCategory(element) + ) { + return + } - if (autoVerifyAttribute != "true") { - val incident = - Incident( + if (element.hasAttributeNS(ANDROID_URI, ATTR_AUTO_VERIFY)) { + return + } + + val dataElements = element.getElementsByTagName(TAG_DATA) + if (dataElements.length == 0) { + return + } + + for (i in 0 until dataElements.length) { + val data = dataElements.item(i) as Element + if (!data.hasAttributeNS(ANDROID_URI, ATTR_SCHEME)) { + continue + } + + val scheme = data.getAttributeNS(ANDROID_URI, ATTR_SCHEME) + if (scheme == "http" || scheme == "https") { + context.report( AUTOVERIFY_ATTRIBUTE_ISSUE, element, - context.getLocation(element), - "Custom scheme intent filters should explicitly set the `autoVerify` attribute to true", - fix().set().android().attribute(ATTR_AUTO_VERIFY).value("true").build() + context.getNameLocation(element), + "This intent filter matches App Links (VIEW, BROWSABLE, DEFAULT, http/https), " + + "but is missing the `android:autoVerify=\"true\"` attribute. " + + "See https://developer.android.com/training/app-links/verify-android-applinks", + fix().set().namespace(ANDROID_URI).attribute(ATTR_AUTO_VERIFY).value("true").build() ) + return + } + } + } - if (hasCustomSchemes(element)) { - // Only have the lint check fire if there are custom schemes present - context.report(incident) + private fun hasViewAction(element: Element): Boolean { + val actions = element.getElementsByTagName(TAG_ACTION) + for (i in 0 until actions.length) { + val action = actions.item(i) as Element + if ("android.intent.action.VIEW" == action.getAttributeNS(ANDROID_URI, "name")) { + return true } } + return false } - private fun hasCustomSchemes(element: Element): Boolean { - for (child in element.childrenIterator()) { - if (child.nodeName == TAG_DATA && child.hasAttributes()) { - for (i in 0 until child.attributes.length) { - val attribute = child.attributes.item(i) - val name = attribute.localName ?: continue - val value = attribute.nodeValue + private fun hasBrowsableCategory(element: Element): Boolean { + val categories = element.getElementsByTagName(TAG_CATEGORY) + for (i in 0 until categories.length) { + val category = categories.item(i) as Element + if ("android.intent.category.BROWSABLE" == category.getAttributeNS(ANDROID_URI, "name")) { + return true + } + } + return false + } - if (value !in REGULAR_SCHEMES) { - return true - } - } + private fun hasDefaultCategory(element: Element): Boolean { + val categories = element.getElementsByTagName(TAG_CATEGORY) + for (i in 0 until categories.length) { + val category = categories.item(i) as Element + if ("android.intent.category.DEFAULT" == category.getAttributeNS(ANDROID_URI, "name")) { + return true } } return false } companion object { - private val REGULAR_SCHEMES = listOf("http", "https", "file", "ftp", "ftps") - private const val EXPLANATION = """ - Intent filters should contain the `autoVerify` attribute and explicitly set it to true, in order \ - to signal to the system to automatically verify the associated hosts in your app's intent filters. - """ + Intent filters that handle `http` or `https` schemes and include \ + `android.intent.action.VIEW`, `android.intent.category.BROWSABLE`, and \ + `android.intent.category.DEFAULT` should also include \ + `android:autoVerify="true"`. This enables Android App Links, which securely \ + associates your app with your domain. \ + See https://developer.android.com/training/app-links/verify-android-applinks + """ - /** Issue describing the problem and pointing to the detector implementation. */ @JvmField val AUTOVERIFY_ATTRIBUTE_ISSUE: Issue = Issue.create( - // ID: used in @SuppressLint warnings etc id = "MissingAutoVerifyAttribute", - // Title -- shown in the IDE's preference dialog, as category headers in the - // Analysis results window, etc - briefDescription = "Application has custom scheme intent filters with missing `autoVerify` attributes", - // Full explanation of the issue; you can use some markdown markup such as - // `monospace`, *italic*, and **bold**. + briefDescription = "Application has http/https scheme intent filters with missing `autoVerify` attributes", explanation = EXPLANATION, category = Category.SECURITY, priority = 6, diff --git a/checks/src/test/java/com/example/lint/checks/CustomSchemeDetectorTest.kt b/checks/src/test/java/com/example/lint/checks/CustomSchemeDetectorTest.kt index 3c8a174..734347b 100644 --- a/checks/src/test/java/com/example/lint/checks/CustomSchemeDetectorTest.kt +++ b/checks/src/test/java/com/example/lint/checks/CustomSchemeDetectorTest.kt @@ -54,6 +54,8 @@ class CustomSchemeDetectorTest : LintDetectorTest() { + + @@ -65,7 +67,7 @@ class CustomSchemeDetectorTest : LintDetectorTest() { } @Test - fun testWhenNoAutoVerifyAttributeSpecifiedOnRegularSchemeIntentFilter_noWarning() { + fun testWhenHttpSchemeButMissingCategories_noWarning() { lint() .files( manifest(""" @@ -87,7 +89,30 @@ class CustomSchemeDetectorTest : LintDetectorTest() { } @Test - fun testWhenFalseAutoVerifyAttributeSpecifiedOnCustomSchemeIntentFilter_showsWarning() { + fun testWhenHttpSchemeButMissingAction_noWarning() { + lint() + .files( + manifest(""" + + + + + + + + + + + + + + """ + ).indented() + ).run().expectClean() + } + + @Test + fun testWhenFalseAutoVerifyAttributeSpecifiedOnCustomSchemeIntentFilter_noWarning() { lint().files( manifest(""" @@ -96,6 +121,8 @@ class CustomSchemeDetectorTest : LintDetectorTest() { + + @@ -103,18 +130,11 @@ class CustomSchemeDetectorTest : LintDetectorTest() { """ ).indented() - ).run().expect( - """ - AndroidManifest.xml:5: Warning: Custom scheme intent filters should explicitly set the autoVerify attribute to true [MissingAutoVerifyAttribute] - - ^ - 0 errors, 1 warnings - """ - ) + ).run().expectClean() } @Test - fun testWhenNoAutoVerifyAttributeSpecifiedOnCustomSchemeIntentFilter_showsWarning() { + fun testWhenNoAutoVerifyAttributeSpecifiedOnCustomSchemeIntentFilter_noWarning() { lint().files( manifest(""" @@ -123,6 +143,8 @@ class CustomSchemeDetectorTest : LintDetectorTest() { + + @@ -130,18 +152,41 @@ class CustomSchemeDetectorTest : LintDetectorTest() { """ ).indented() - ).run().expect( - """ - AndroidManifest.xml:5: Warning: Custom scheme intent filters should explicitly set the autoVerify attribute to true [MissingAutoVerifyAttribute] + ).run().expectClean() + } + + @Test + fun testWhenAppLinkMissingAutoVerify_showsWarning() { + lint().files( + manifest(""" + + + + - ^ - 0 errors, 1 warnings + + + + + + + + + """ - ) + ).indented() + ).run().expect( + """ + AndroidManifest.xml:5: Warning: This intent filter matches App Links (VIEW, BROWSABLE, DEFAULT, http/https), but is missing the android:autoVerify="true" attribute. See https://developer.android.com/training/app-links/verify-android-applinks [MissingAutoVerifyAttribute] + + ~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) } @Test - fun testWhenNoAutoVerifyAttributeSpecifiedOnCustomSchemeIntentFilter_showsQuickFix() { + fun testWhenAppLinkMissingAutoVerify_showsQuickFix() { lint().files( manifest(""" @@ -150,7 +195,9 @@ class CustomSchemeDetectorTest : LintDetectorTest() { - + + + @@ -158,17 +205,17 @@ class CustomSchemeDetectorTest : LintDetectorTest() { """ ).indented() ).run().expectFixDiffs( - """ - Fix for AndroidManifest.xml line 5: Set autoVerify="true": - @@ -9 +9 - - - + - """ + """ + Fix for AndroidManifest.xml line 5: Set autoVerify="true": + @@ -9 +9 + - + + + """ ) } @Test - fun testWhenFalseAutoVerifyAttributeSpecifiedOnCustomSchemeIntentFilter_showsQuickFix() { + fun testWhenAppLinkHasAutoVerifyFalse_noWarning() { lint().files( manifest(""" @@ -177,20 +224,15 @@ class CustomSchemeDetectorTest : LintDetectorTest() { - + + + """ ).indented() - ).run().expectFixDiffs( - """ - Fix for AndroidManifest.xml line 5: Set autoVerify="true": - @@ -9 +9 - - - + - """ - ) + ).run().expectClean() } }