I'm writing a custom lint rule to catch if android:src namespace is being used for displaying vector drawables which have gradient tag in them instead of app:scrCompat namespace.
Android Studio is able to detect the errors after writing this rule.
Example
When the below vector drawable is used with android namespace on API level < 22 will lead to crash!
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="9dp"
android:height="17dp"
android:viewportWidth="9"
android:viewportHeight="17">
<path
android:pathData="M7.1921,17L8.0072,17L8.0072,0L0,0L0,9.8079C-0,11.8049 0.2818,12.9046 0.8109,13.894C1.3401,14.8834 2.1166,15.6599 3.106,16.1891C4.0954,16.7182 5.1951,17 7.1921,17Z"
android:strokeWidth="1"
android:fillType="evenOdd"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:startY="17"
android:startX="3.3198788"
android:endY="17"
android:endX="5.201214"
android:type="linear">
<item android:offset="0" android:color="#000000"/>
<item android:offset="1" android:color="#FFFFFF"/>
</gradient>
</aapt:attr>
</path>
</vector>
------------
Incorrect use of the above drawable
<ImageView
android:id="@+id/imageView"
❌ android:src="@drawable/hero_background"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Correct usage
<ImageView
android:id="@+id/imageView"
✅ app:srcCompat="@drawable/hero_background"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
------------
This is the rule
class VectorUsageDetector: ResourceXmlDetector() {
companion object {
val VECTOR_ISSUE = Issue.create(
id = "VectorGradientIssue",
briefDescription = "Warns usage of vectors with `android` namespace",
explanation = "Vectors with `android` are not supported in API level below 21",
category = Category.CORRECTNESS,
severity = Severity.FATAL,
implementation = Implementation(
VectorUsageDetector::class.java,
Scope.RESOURCE_FILE_SCOPE
)
)
}
override fun appliesTo(folderType: ResourceFolderType): Boolean {
return folderType == ResourceFolderType.LAYOUT
}
override fun getApplicableAttributes(): Collection<String>? {
return listOf(SdkConstants.ATTR_SRC)
}
override fun visitAttribute(context: XmlContext, attribute: Attr) {
val name = attribute.name
val value = attribute.value
if (name.contains("android:") and value.contains("drawable")) {
val isVectorDrawable = isVectorDrawable(value.replace("@drawable/", ""), context)
if (isVectorDrawable) {
context.report(
VECTOR_ISSUE,
attribute,
context.getValueLocation(attribute),
"Vector is used without `app` namespace. This can cause a crash on API levels below 21.")
} else {
return
}
} else {
return
}
}
private fun isVectorDrawable(name: String, context: XmlContext): Boolean {
var hasVectorTag = false
var hasGradientTag = false
context.mainProject.resourceFolders.forEach { folder ->
val path = folder.path
val drawableFolder = "$path/drawable/"
val drawableFile = File("$drawableFolder/$name.xml")
if (drawableFile.exists()) {
drawableFile.forEachLine { lineString ->
if (lineString.contains(TAG_VECTOR)) {
hasVectorTag = true
}
if (lineString.contains(TAG_GRADIENT)) {
hasGradientTag = true
}
}
}
}
return hasVectorTag && hasGradientTag
}
}
When I run ./gradlew app:lint if there are any violations, they are shown as errors.
But when all errors are fixed and run the same command (./gradlew app:lint) this time I see the following error pointing to some random xml file.
❌
The lint detector
com.xyz.rules.VectorUsageDetector
called context.getMainProject() during module analysis.
This does not work correctly when running in AGP (8.1.0).
In particular, there may be false positives or false negatives because
the lint check may be using the minSdkVersion or manifest information
from the library instead of any consuming app module.
"VectorGradientIssue"
Issue Vendors:
Identifier: com.xyz.rules
Call stack: Context.getMainProject(Context.kt:102)
←VectorUsageDetector.isVectorDrawable(VectorUsageDetector.kt:64)
←VectorUsageDetector.visitAttribute(VectorUsageDetector.kt:46)
←ResourceVisitor.visitElement(ResourceVisitor.java:161)
←ResourceVisitor.visitElement(ResourceVisitor.java:172)
←ResourceVisitor.visitElement(ResourceVisitor.java:172)
←ResourceVisitor.visitElement(ResourceVisitor.java:172)
←ResourceVisitor.visitElement(ResourceVisitor.java:172)
←ResourceVisitor.visitFile(ResourceVisitor.java:120)
←LintDriver$checkResourceFolder$1.run(LintDriver.kt:2400)
←LintClient.runReadAction(LintClient.kt:1700)
←LintDriver$LintClientWrapper.runReadAction(LintDriver.kt:2871)
←LintDriver.checkResourceFolder(LintDriver.kt:2396)
←LintDriver.checkResFolder(LintDriver.kt:2349)
←LintDriver.runFileDetectors(LintDriver.kt:1362)
←LintDriver.checkProject(LintDriver.kt:1148)
←LintDriver.checkProjectRoot(LintDriver.kt:619)
←LintDriver.access$checkProjectRoot(LintDriver.kt:170)
←LintDriver$analyzeOnly$1.invoke(LintDriver.kt:444)
←LintDriver$analyzeOnly$1.invoke(LintDriver.kt:441)
Config
Min SDK - 22
Target SDK - 33
AGP - 8.1.0
Gradle Wrapper - gradle-8.0
Kotlin Version - 1.9
Build tools - 34.0.0
Gradle for the rules module
apply plugin: 'java-library'
apply plugin: 'kotlin'
jar {
manifest{
attributes 'Lint-Registry-V2': 'com.xyz.rules.IssueRegistry'
}
}
dependencies {
implementation "com.android.tools.lint:lint-api:31.1.0"
implementation "com.android.tools.lint:lint-checks:31.1.0"
}
What maybe the problem here? Any alternatives to write the same lint check?
Expecting it to not error out if all errors are fixed!
Seems it's not allowed to use
Context#getMainProject()in partial runs: https://googlesamples.github.io/android-custom-lint-rules/api-guide.htmlCan't you always use
app:srcCompat? Reading files to check existense ofvectorandgradienttags seems like an expensive operation for me. Also, you're checking only one resource directory while an android project may have multiple.