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?