lint: UseHelperInsteadOfWindowInsets.

This commit is contained in:
oxy
2023-10-07 01:57:30 +08:00
parent adf5d53a7d
commit c33ab31ead
15 changed files with 173 additions and 8 deletions

1
.idea/gradle.xml generated
View File

@ -24,6 +24,7 @@
<option value="$PROJECT_DIR$/features/main" />
<option value="$PROJECT_DIR$/features/scheme" />
<option value="$PROJECT_DIR$/features/setting" />
<option value="$PROJECT_DIR$/lint" />
<option value="$PROJECT_DIR$/ui" />
</set>
</option>

View File

@ -118,8 +118,7 @@ class MainActivity : ComponentActivity() {
applyConfiguration()
}
// FIXME:
// 1. window inset controller cannot take effect in orientation changing quickly.
@Helper.WindowInsetsAllowed
private fun applyConfiguration() {
val navigationBarsVisibility = helper.navigationBarsVisibility
val statusBarsVisibility = helper.statusBarsVisibility
@ -138,6 +137,7 @@ class MainActivity : ComponentActivity() {
}
}
@Helper.WindowInsetsAllowed
private fun WindowInsetsControllerCompat.default(@InsetsType types: Int) {
when (types) {
WindowInsetsCompat.Type.navigationBars() -> {

View File

@ -32,6 +32,8 @@ android {
}
dependencies {
lintPublish(project(":lint"))
implementation(libs.androidx.core.core.ktx)
implementation(libs.androidx.appcompat.appcompat)
implementation(libs.androidx.compose.ui.ui)

View File

@ -4,7 +4,7 @@ enum class UBoolean {
True, False, Unspecified
}
val Boolean.ub: UBoolean get() = if (this) UBoolean.True else UBoolean.False
val Boolean.u: UBoolean get() = if (this) UBoolean.True else UBoolean.False
val UBoolean.actual: Boolean?
get() = when (this) {

View File

@ -24,7 +24,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import com.m3u.core.annotation.ClipMode
import com.m3u.core.unspecified.ub
import com.m3u.core.unspecified.u
import com.m3u.core.util.basic.isNotEmpty
import com.m3u.core.util.context.toast
import com.m3u.features.live.components.DlnaDevicesBottomSheet
@ -55,8 +55,8 @@ internal fun LiveRoute(
val searching by viewModel.searching.collectAsStateWithLifecycle()
val maskState = rememberMaskState { visible ->
helper.statusBarsVisibility = visible.ub
helper.navigationBarsVisibility = false.ub
helper.statusBarsVisibility = visible.u
helper.navigationBarsVisibility = false.u
}
var isPipMode by remember { mutableStateOf(false) }
@ -80,8 +80,8 @@ internal fun LiveRoute(
}
}
darkMode = true
statusBarsVisibility = false.ub
navigationBarsVisibility = false.ub
statusBarsVisibility = false.u
navigationBarsVisibility = false.u
onPipModeChanged = OnPipModeChanged { info ->
isPipMode = info.isInPictureInPictureMode
if (!isPipMode) {

View File

@ -39,6 +39,8 @@ com-google-android-material = "1.9.0"
androidx-test-uiautomator = "2.2.0"
androidx-benchmark = "1.1.1"
lint = "31.1.2"
[libraries]
androidx-core-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" }
androidx-core-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidx-core-splashscreen" }
@ -107,6 +109,8 @@ androidx-test-espresso-espresso-core = { group = "androidx.test.espresso", name
androidx-test-uiautomator-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "androidx-test-uiautomator" }
androidx-benchmark-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "androidx-benchmark" }
com-android-tools-lint-lint-api = { group = "com.android.tools.lint", name = "lint-api", version.ref = "lint" }
[plugins]
com-android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
com-android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }

1
lint/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

9
lint/build.gradle.kts Normal file
View File

@ -0,0 +1,9 @@
plugins {
`java-library`
id("kotlin")
id("com.android.lint")
}
dependencies {
compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
compileOnly(libs.com.android.tools.lint.lint.api)
}

0
lint/consumer-rules.pro Normal file
View File

21
lint/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,106 @@
package com.m3u.lint.helper
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.LintFix
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.getContainingUMethod
class UseHelperInsteadOfWindowInsetsDetector : Detector(), SourceCodeScanner {
override fun getApplicableMethodNames(): List<String> = listOf("show", "hide")
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
val evaluator = context.evaluator
val marked =
node.getContainingUMethod()
?.uAnnotations
?.find { it.qualifiedName == ANNOTATION_NAME } != null
if (marked) return
if (evaluator.isMemberInClass(method, "androidx.core.view.WindowInsetsControllerCompat")) {
val isStatusBars = node.getArgumentForParameter(0)?.evaluate() == 1
val visible = node.methodName == "show"
reportUsage(
context = context,
node = node,
isStatusBars = isStatusBars,
visible = visible
)
}
}
private fun reportUsage(
context: JavaContext,
node: UCallExpression,
isStatusBars: Boolean,
visible: Boolean
) {
val property = if (isStatusBars) "status" else "navigation"
val value = if (visible) "true" else "false"
val expression = "helper.${property}BarsVisibility = ${value}.u"
val data = LintFix.LintFixGroup(
displayName = "display",
familyName = "family",
type = LintFix.GroupType.ALTERNATIVES,
fixes = listOf(
LintFix.create()
.name("Replace with $expression")
.replace()
.imports(
"com.m3u.core.unspecified.u",
"com.m3u.ui.model.Helper"
)
.with(expression)
.autoFix(robot = true, independent = true)
.build(),
LintFix.create()
.name("Marked with @$CHILD_ANNOTATION_NAME annotation")
.annotate("@$ANNOTATION_NAME")
.autoFix(robot = true, independent = false)
.build()
)
)
context.report(
issue = UseHelperIssue,
location = context.getLocation(node),
message = EXPLANATION,
quickfixData = data
)
}
companion object {
private const val CHILD_ANNOTATION_NAME = "WindowInsetsAllowed"
private const val ANNOTATION_NAME = "com.m3u.ui.model.Helper.${CHILD_ANNOTATION_NAME}"
private const val BRIEF_DESCRIPTION = "Helper instead of WindowInsetsControllerCompat"
val EXPLANATION = """
Usage of WindowInsetsControllerCompat to control the system bars is not allowed in this project.
```kotlin
// Get helper instance in composable scope
@Composable
fun Screen() {
val helper = LocalHelper.current
}
// then use helper to control system bars visibility
helper.statusBarsVisibility = true.u
```
""".trimIndent()
val UseHelperIssue = Issue.create(
id = "UseHelperIssue",
briefDescription = BRIEF_DESCRIPTION,
explanation = EXPLANATION,
category = Category.CORRECTNESS,
severity = Severity.FATAL,
implementation = Implementation(
UseHelperInsteadOfWindowInsetsDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)
}
}

View File

@ -0,0 +1,16 @@
package com.m3u.lint.helper
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.client.api.Vendor
import com.android.tools.lint.detector.api.Issue
import com.m3u.lint.helper.UseHelperInsteadOfWindowInsetsDetector.Companion.UseHelperIssue
class UseHelperInsteadOfWindowInsetsIssueRegistry : IssueRegistry() {
override val issues: List<Issue>
get() = listOf(UseHelperIssue)
override val vendor: Vendor = Vendor(
vendorName = "M3UAndroid Project",
feedbackUrl = "https://t.me/m3u_android_chat",
contact = "https://t.me/sortBy"
)
}

View File

@ -0,0 +1 @@
com.m3u.lint.helper.UseHelperInsteadOfWindowInsetsIssueRegistry

View File

@ -32,3 +32,4 @@ include(
":features:about"
)
include(":benchmark")
include(":lint")

View File

@ -20,6 +20,9 @@ typealias OnPipModeChanged = Consumer<PictureInPictureModeChangedInfo>
@Stable
interface Helper {
@Retention(AnnotationRetention.SOURCE)
annotation class WindowInsetsAllowed
var title: String
var actions: List<Action>
var fob: Fob?