From c33ab31eade7f71a6909b683a73b2ecd57b44e2c Mon Sep 17 00:00:00 2001 From: oxy Date: Sat, 7 Oct 2023 01:57:30 +0800 Subject: [PATCH] lint: UseHelperInsteadOfWindowInsets. --- .idea/gradle.xml | 1 + .../java/com/m3u/androidApp/MainActivity.kt | 4 +- core/build.gradle.kts | 2 + .../java/com/m3u/core/unspecified/UBoolean.kt | 2 +- .../java/com/m3u/features/live/LiveScreen.kt | 10 +- gradle/libs.versions.toml | 4 + lint/.gitignore | 1 + lint/build.gradle.kts | 9 ++ lint/consumer-rules.pro | 0 lint/proguard-rules.pro | 21 ++++ .../UseHelperInsteadOfWindowInsetsDetector.kt | 106 ++++++++++++++++++ ...elperInsteadOfWindowInsetsIssueRegistry.kt | 16 +++ ...ndroid.tools.lint.client.api.IssueRegistry | 1 + settings.gradle.kts | 1 + ui/src/main/java/com/m3u/ui/model/Helper.kt | 3 + 15 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 lint/.gitignore create mode 100644 lint/build.gradle.kts create mode 100644 lint/consumer-rules.pro create mode 100644 lint/proguard-rules.pro create mode 100644 lint/src/main/java/com/m3u/lint/helper/UseHelperInsteadOfWindowInsetsDetector.kt create mode 100644 lint/src/main/java/com/m3u/lint/helper/UseHelperInsteadOfWindowInsetsIssueRegistry.kt create mode 100644 lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 1655d646..2c65b219 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -24,6 +24,7 @@ diff --git a/androidApp/src/main/java/com/m3u/androidApp/MainActivity.kt b/androidApp/src/main/java/com/m3u/androidApp/MainActivity.kt index 471637e8..b5a24a8c 100644 --- a/androidApp/src/main/java/com/m3u/androidApp/MainActivity.kt +++ b/androidApp/src/main/java/com/m3u/androidApp/MainActivity.kt @@ -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() -> { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 583d9e18..805e0e0b 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -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) diff --git a/core/src/main/java/com/m3u/core/unspecified/UBoolean.kt b/core/src/main/java/com/m3u/core/unspecified/UBoolean.kt index 3274d4cf..8a53485c 100644 --- a/core/src/main/java/com/m3u/core/unspecified/UBoolean.kt +++ b/core/src/main/java/com/m3u/core/unspecified/UBoolean.kt @@ -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) { diff --git a/features/live/src/main/java/com/m3u/features/live/LiveScreen.kt b/features/live/src/main/java/com/m3u/features/live/LiveScreen.kt index 1deb7e8a..bad987da 100644 --- a/features/live/src/main/java/com/m3u/features/live/LiveScreen.kt +++ b/features/live/src/main/java/com/m3u/features/live/LiveScreen.kt @@ -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) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f42c91ea..1cb2aff9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/lint/.gitignore b/lint/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/lint/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/lint/build.gradle.kts b/lint/build.gradle.kts new file mode 100644 index 00000000..d6cdbc7c --- /dev/null +++ b/lint/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/lint/consumer-rules.pro b/lint/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/lint/proguard-rules.pro b/lint/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/lint/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/lint/src/main/java/com/m3u/lint/helper/UseHelperInsteadOfWindowInsetsDetector.kt b/lint/src/main/java/com/m3u/lint/helper/UseHelperInsteadOfWindowInsetsDetector.kt new file mode 100644 index 00000000..24778d6f --- /dev/null +++ b/lint/src/main/java/com/m3u/lint/helper/UseHelperInsteadOfWindowInsetsDetector.kt @@ -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 = 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 + ) + ) + } +} diff --git a/lint/src/main/java/com/m3u/lint/helper/UseHelperInsteadOfWindowInsetsIssueRegistry.kt b/lint/src/main/java/com/m3u/lint/helper/UseHelperInsteadOfWindowInsetsIssueRegistry.kt new file mode 100644 index 00000000..181a0c76 --- /dev/null +++ b/lint/src/main/java/com/m3u/lint/helper/UseHelperInsteadOfWindowInsetsIssueRegistry.kt @@ -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 + get() = listOf(UseHelperIssue) + override val vendor: Vendor = Vendor( + vendorName = "M3UAndroid Project", + feedbackUrl = "https://t.me/m3u_android_chat", + contact = "https://t.me/sortBy" + ) +} diff --git a/lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry new file mode 100644 index 00000000..61d0075d --- /dev/null +++ b/lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry @@ -0,0 +1 @@ +com.m3u.lint.helper.UseHelperInsteadOfWindowInsetsIssueRegistry \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 53f3ed15..26b69535 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,3 +32,4 @@ include( ":features:about" ) include(":benchmark") +include(":lint") diff --git a/ui/src/main/java/com/m3u/ui/model/Helper.kt b/ui/src/main/java/com/m3u/ui/model/Helper.kt index 287176c8..8bcbce83 100644 --- a/ui/src/main/java/com/m3u/ui/model/Helper.kt +++ b/ui/src/main/java/com/m3u/ui/model/Helper.kt @@ -20,6 +20,9 @@ typealias OnPipModeChanged = Consumer @Stable interface Helper { + @Retention(AnnotationRetention.SOURCE) + annotation class WindowInsetsAllowed + var title: String var actions: List var fob: Fob?