From 210a39510cc2bd59dda28866a2676bfd215772f4 Mon Sep 17 00:00:00 2001 From: oxy Date: Sun, 21 Jul 2024 23:04:56 +0800 Subject: [PATCH] Use ksp to generate Likable code. --- annotation/.gitignore | 1 + annotation/build.gradle.kts | 7 + .../com/m3u/annotation/ExcludeProperty.kt | 3 + .../com/m3u/annotation/LikableDataClass.kt | 3 + .../main/java/com/m3u/core/util/Likable.kt | 19 --- data/build.gradle.kts | 4 +- .../com/m3u/data/database/model/Channel.kt | 16 +- .../com/m3u/data/database/model/Playlist.kt | 16 +- gradle/libs.versions.toml | 11 ++ processor/.gitignore | 1 + processor/build.gradle.kts | 20 +++ processor/consumer-rules.pro | 0 processor/proguard-rules.pro | 21 +++ processor/src/main/AndroidManifest.xml | 4 + .../likable/LikableSymbolProcessor.kt | 160 ++++++++++++++++++ .../likable/LikableSymbolProcessorProvider.kt | 16 ++ settings.gradle.kts | 2 + 17 files changed, 271 insertions(+), 33 deletions(-) create mode 100644 annotation/.gitignore create mode 100644 annotation/build.gradle.kts create mode 100644 annotation/src/main/java/com/m3u/annotation/ExcludeProperty.kt create mode 100644 annotation/src/main/java/com/m3u/annotation/LikableDataClass.kt delete mode 100644 core/src/main/java/com/m3u/core/util/Likable.kt create mode 100644 processor/.gitignore create mode 100644 processor/build.gradle.kts create mode 100644 processor/consumer-rules.pro create mode 100644 processor/proguard-rules.pro create mode 100644 processor/src/main/AndroidManifest.xml create mode 100644 processor/src/main/java/com/m3u/processor/likable/LikableSymbolProcessor.kt create mode 100644 processor/src/main/java/com/m3u/processor/likable/LikableSymbolProcessorProvider.kt diff --git a/annotation/.gitignore b/annotation/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/annotation/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/annotation/build.gradle.kts b/annotation/build.gradle.kts new file mode 100644 index 000000000..5bd54b44a --- /dev/null +++ b/annotation/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + kotlin("jvm") +} + +dependencies { + implementation(kotlin("stdlib")) +} \ No newline at end of file diff --git a/annotation/src/main/java/com/m3u/annotation/ExcludeProperty.kt b/annotation/src/main/java/com/m3u/annotation/ExcludeProperty.kt new file mode 100644 index 000000000..303b54eae --- /dev/null +++ b/annotation/src/main/java/com/m3u/annotation/ExcludeProperty.kt @@ -0,0 +1,3 @@ +package com.m3u.annotation + +annotation class ExcludeProperty() diff --git a/annotation/src/main/java/com/m3u/annotation/LikableDataClass.kt b/annotation/src/main/java/com/m3u/annotation/LikableDataClass.kt new file mode 100644 index 000000000..cbb8fd6db --- /dev/null +++ b/annotation/src/main/java/com/m3u/annotation/LikableDataClass.kt @@ -0,0 +1,3 @@ +package com.m3u.annotation + +annotation class LikableDataClass diff --git a/core/src/main/java/com/m3u/core/util/Likable.kt b/core/src/main/java/com/m3u/core/util/Likable.kt deleted file mode 100644 index db52352d4..000000000 --- a/core/src/main/java/com/m3u/core/util/Likable.kt +++ /dev/null @@ -1,19 +0,0 @@ -@file:Suppress("unused") - -package com.m3u.core.util - -// TODO: use ksp to generate code. -interface Likable { - infix fun like(another: T): Boolean = this == another -} - -infix fun > T.unlike(another: T): Boolean = this.like(another).not() -infix fun > T.belong(collection: Collection): Boolean = - collection.any { it like this } - -infix fun > T.notbelong(collection: Collection): Boolean = - collection.all { it unlike this } - -infix fun > Collection.hold(element: T): Boolean = this.any { it like element } -infix fun > Collection.nothold(element: T): Boolean = - this.all { it unlike element } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 804fcbb0c..11e84e520 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -26,7 +26,8 @@ android { dependencies { implementation(project(":core")) - implementation(libs.androidx.media3.exoplayer.workmanager) + implementation(project(":annotation")) + ksp(project(":processor")) val richCodec = gradle .startParameter @@ -61,6 +62,7 @@ dependencies { implementation(libs.androidx.media3.exoplayer.hls) implementation(libs.androidx.media3.exoplayer.rtsp) implementation(libs.androidx.media3.exoplayer.smoothstreaming) + implementation(libs.androidx.media3.exoplayer.workmanager) implementation(libs.androidx.media3.session) implementation(libs.androidx.media3.container) implementation(libs.androidx.media3.datasource.rtmp) diff --git a/data/src/main/java/com/m3u/data/database/model/Channel.kt b/data/src/main/java/com/m3u/data/database/model/Channel.kt index 673b47d2e..e72e74ef5 100644 --- a/data/src/main/java/com/m3u/data/database/model/Channel.kt +++ b/data/src/main/java/com/m3u/data/database/model/Channel.kt @@ -4,7 +4,8 @@ import androidx.compose.runtime.Immutable import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import com.m3u.core.util.Likable +import com.m3u.annotation.ExcludeProperty +import com.m3u.annotation.LikableDataClass import com.m3u.data.parser.xtream.XtreamChannelInfo import io.ktor.http.URLBuilder import io.ktor.http.Url @@ -17,6 +18,7 @@ import kotlinx.serialization.Serializable ) @Immutable @Serializable +@LikableDataClass data class Channel( @ColumnInfo(name = "url") // playable url @@ -37,26 +39,26 @@ data class Channel( val licenseKey: String? = null, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") + @ExcludeProperty val id: Int = 0, // extra fields @ColumnInfo(name = "favourite", index = true) + @ExcludeProperty val favourite: Boolean = false, @ColumnInfo(name = "hidden", defaultValue = "0") + @ExcludeProperty val hidden: Boolean = false, @ColumnInfo(name = "seen", defaultValue = "0") + @ExcludeProperty val seen: Long = 0L, @ColumnInfo(name = "channel_id", defaultValue = "NULL") + @ExcludeProperty // if it is from m3u, it may be tvg-id // if it is xtream live, it may be epgChannelId // if it is xtream vod, it may be streamId // if it is xtream series, it may be seriesId val originalId: String? = null -) : Likable { - override infix fun like(another: Channel): Boolean = - this.url == another.url && this.playlistUrl == another.playlistUrl && this.cover == another.cover - && this.category == another.category && this.title == another.title && this.licenseType == another.licenseType - && this.licenseKey == another.licenseKey - +) { companion object { const val LICENSE_TYPE_WIDEVINE = "com.widevine.alpha" const val LICENSE_TYPE_CLEAR_KEY = "clearkey" diff --git a/data/src/main/java/com/m3u/data/database/model/Playlist.kt b/data/src/main/java/com/m3u/data/database/model/Playlist.kt index 857144252..c7b507662 100644 --- a/data/src/main/java/com/m3u/data/database/model/Playlist.kt +++ b/data/src/main/java/com/m3u/data/database/model/Playlist.kt @@ -7,7 +7,8 @@ import androidx.room.Embedded import androidx.room.Entity import androidx.room.PrimaryKey import androidx.room.Relation -import com.m3u.core.util.Likable +import com.m3u.annotation.ExcludeProperty +import com.m3u.annotation.LikableDataClass import com.m3u.core.util.basic.startsWithAny import com.m3u.data.parser.xtream.XtreamInput import com.m3u.data.parser.xtream.XtreamParser @@ -23,6 +24,7 @@ import kotlinx.serialization.encoding.Encoder @Entity(tableName = "playlists") @Immutable @Serializable +@LikableDataClass data class Playlist( @ColumnInfo(name = "title") val title: String, @@ -38,24 +40,25 @@ data class Playlist( val url: String, // extra fields @ColumnInfo(name = "pinned_groups", defaultValue = "[]") + @ExcludeProperty val pinnedCategories: List = emptyList(), @ColumnInfo(name = "hidden_groups", defaultValue = "[]") + @ExcludeProperty val hiddenCategories: List = emptyList(), @ColumnInfo(name = "source", defaultValue = "0") @Serializable(with = DataSourceSerializer::class) val source: DataSource = DataSource.M3U, @ColumnInfo(name = "user_agent", defaultValue = "NULL") + @ExcludeProperty val userAgent: String? = null, // epg playlist urls @ColumnInfo(name = "epg_urls", defaultValue = "[]") + @ExcludeProperty val epgUrls: List = emptyList(), @ColumnInfo(name = "auto_refresh_programmes", defaultValue = "0") + @ExcludeProperty val autoRefreshProgrammes: Boolean = false -) : Likable { - override fun like(another: Playlist): Boolean { - return title == another.title && url == another.url && source == another.source - } - +) { companion object { const val URL_IMPORTED = "imported" @@ -99,6 +102,7 @@ fun Playlist.epgUrlsOrXtreamXmlUrl(): List = when (source) { ) listOf(epgUrl) } + else -> emptyList() } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b5d955af4..805ea1c9f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,12 +17,15 @@ androidx-tvprovider = "1.0.0" androidx-startup = "1.1.1" androidx-paging = "3.3.0" +autoServiceAnnotations = "1.0" +autoServiceKsp = "1.0.0" glance = "1.1.0" google-accompanist = "0.35.1-alpha" google-dagger = "2.51.1" haze = "0.7.3" io-coil = "2.6.0" +kotlinpoet = "1.18.1" kotlinx-serialization-json = "1.6.3" kotlinx-serialization-converter-retrofit = "1.0.0" kotlinx-datetime = "0.6.0" @@ -50,6 +53,8 @@ androidx-graphics-shapes = "1.0.0-beta01" minabox = "1.7.1" ktor-server = "3.0.0-beta-1" mm2d-mmupnp = "3.1.6" +symbolProcessingApi = "2.0.0-1.0.22" +junit = "4.13.2" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" } @@ -115,6 +120,8 @@ androidx-room-compiler = { group = "androidx.room", name = "room-compiler", vers androidx-tvprovider = { group = "androidx.tvprovider", name = "tvprovider", version.ref = "androidx-tvprovider" } +auto-service-annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoServiceAnnotations" } +auto-service-ksp = { module = "dev.zacsweers.autoservice:auto-service-ksp", version.ref = "autoServiceKsp" } google-accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "google-accompanist" } google-dagger-hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "google-dagger" } @@ -122,6 +129,8 @@ google-dagger-hilt-compiler = { group = "com.google.dagger", name = "hilt-compil google-material = { group = "com.google.android.material", name = "material", version.ref = "com-google-android-material" } +kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } +kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet" } squareup-retrofit2 = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "squareup-retrofit2" } squareup-leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "squareup-leakcanary" } @@ -157,6 +166,8 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-api" } minabox = { group = "io.github.oleksandrbalan", name = "minabox", version.ref = "minabox" } net-mm2d-mmupnp-mmupnp = { group = "net.mm2d.mmupnp", name = "mmupnp", version.ref = "mm2d-mmupnp" } androidx-graphics-shapes-android = { group = "androidx.graphics", name = "graphics-shapes-android", version.ref = "androidx-graphics-shapes" } +symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "symbolProcessingApi" } +junit = { group = "junit", name = "junit", version.ref = "junit" } [plugins] com-android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } diff --git a/processor/.gitignore b/processor/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/processor/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/processor/build.gradle.kts b/processor/build.gradle.kts new file mode 100644 index 000000000..dc27070ee --- /dev/null +++ b/processor/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + kotlin("jvm") + alias(libs.plugins.com.google.devtools.ksp) +} + +ksp { + arg("autoserviceKsp.verify", "true") + arg("autoserviceKsp.verbose", "true") +} + +dependencies { + implementation(project(":annotation")) + + implementation(libs.symbol.processing.api) + implementation(libs.kotlinpoet) + implementation(libs.kotlinpoet.ksp) + implementation(libs.auto.service.annotations) + + ksp(libs.auto.service.ksp) +} \ No newline at end of file diff --git a/processor/consumer-rules.pro b/processor/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/processor/proguard-rules.pro b/processor/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/processor/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/processor/src/main/AndroidManifest.xml b/processor/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a5918e68a --- /dev/null +++ b/processor/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/processor/src/main/java/com/m3u/processor/likable/LikableSymbolProcessor.kt b/processor/src/main/java/com/m3u/processor/likable/LikableSymbolProcessor.kt new file mode 100644 index 000000000..65202f37b --- /dev/null +++ b/processor/src/main/java/com/m3u/processor/likable/LikableSymbolProcessor.kt @@ -0,0 +1,160 @@ +package com.m3u.processor.likable + +import com.google.devtools.ksp.getDeclaredProperties +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSVisitorVoid +import com.google.devtools.ksp.symbol.Modifier +import com.google.devtools.ksp.validate +import com.m3u.annotation.ExcludeProperty +import com.m3u.annotation.LikableDataClass +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.ksp.toTypeName +import com.squareup.kotlinpoet.ksp.writeTo +import com.squareup.kotlinpoet.withIndent + +class LikableSymbolProcessor( + private val logger: KSPLogger, + private val codeGenerator: CodeGenerator +) : SymbolProcessor { + override fun process(resolver: Resolver): List { + val symbols = resolver.getSymbolsWithAnnotation(LikableDataClass::class.qualifiedName!!) + val unableToProcess = symbols.filterNot { it.validate() } + symbols.forEach { symbol -> + symbol.accept(Visitor(), Unit) + } + return unableToProcess.toList() + } + + private inner class Visitor : KSVisitorVoid() { + override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { + val qualifiedName = classDeclaration.qualifiedName?.asString() ?: return + val isDataClass = Modifier.DATA in classDeclaration.modifiers + if (!isDataClass) { + logger.error("@Likeable can only target data class.", classDeclaration) + return + } + val packageName = classDeclaration.packageName.asString() + val fileName = classDeclaration.simpleName.asString() + "Likeable" + val properties = classDeclaration + .getDeclaredProperties() + .filterNot { it.annotations.any { it.shortName.getShortName() == ExcludeProperty::class.simpleName } } + .toList() + val typeName = classDeclaration.asType(emptyList()).toTypeName() + val fileSpec = FileSpec.builder(packageName, fileName) + .addFunction( + FunSpec.builder("like") + .receiver(typeName) + .addModifiers(KModifier.INFIX) + .addParameter( + ParameterSpec.builder("another", typeName).build() + ) + .returns(Boolean::class) + .addCode( + CodeBlock.builder().apply { + add("return ") + properties.forEachIndexed { index, property -> + add("this.${property} == another.${property}") + if (index != properties.lastIndex) { + add(" && \n") + } + } + } + .build() + ) + .build() + ) + .addFunction( + FunSpec.builder("unlike") + .addModifiers(KModifier.INFIX) + .receiver(typeName) + .addParameter("another", typeName) + .returns(Boolean::class) + .addCode("return this.like(another).not()") + .build() + ) + .addFunction( + FunSpec.builder("belong") + .addModifiers(KModifier.INFIX) + .receiver(typeName) + .addParameter( + "collection", + ClassName("kotlin.collections", "Collection").parameterizedBy(typeName) + ) + .returns(Boolean::class) + .addCode( + CodeBlock.builder() + .add("return collection.any {\n") + .withIndent { add("it like this\n") } + .add("}") + .build() + ) + .build() + ) + .addFunction( + FunSpec.builder("notbelong") + .addModifiers(KModifier.INFIX) + .receiver(typeName) + .addParameter( + "collection", + ClassName("kotlin.collections", "Collection").parameterizedBy(typeName) + ) + .returns(Boolean::class) + .addCode( + CodeBlock.builder() + .add("return collection.all {\n") + .withIndent { add("it unlike this\n") } + .add("}") + .build() + ) + .build() + ) + .addFunction( + FunSpec.builder("hold") + .addModifiers(KModifier.INFIX) + .receiver( + ClassName("kotlin.collections", "Collection").parameterizedBy(typeName) + ) + .addParameter("element", typeName) + .returns(Boolean::class) + .addCode( + CodeBlock.builder() + .add("return this.any {\n") + .withIndent { add("it like element\n") } + .add("}") + .build() + ) + .build() + ) + .addFunction( + FunSpec.builder("nothold") + .addModifiers(KModifier.INFIX) + .receiver( + ClassName("kotlin.collections", "Collection").parameterizedBy(typeName) + ) + .addParameter("element", typeName) + .returns(Boolean::class) + .addCode( + CodeBlock.builder() + .add("return this.all {\n") + .withIndent { add("it unlike element\n") } + .add("}") + .build() + ) + .build() + ) + .build() + fileSpec.writeTo(codeGenerator, false) + } + } +} \ No newline at end of file diff --git a/processor/src/main/java/com/m3u/processor/likable/LikableSymbolProcessorProvider.kt b/processor/src/main/java/com/m3u/processor/likable/LikableSymbolProcessorProvider.kt new file mode 100644 index 000000000..c2a4d6ee3 --- /dev/null +++ b/processor/src/main/java/com/m3u/processor/likable/LikableSymbolProcessorProvider.kt @@ -0,0 +1,16 @@ +package com.m3u.processor.likable + +import com.google.auto.service.AutoService +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +@AutoService(SymbolProcessorProvider::class) +class LikableSymbolProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return LikableSymbolProcessor( + logger = environment.logger, + codeGenerator = environment.codeGenerator + ) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 10bc558a6..5f60c1bf2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,3 +32,5 @@ include( include(":benchmark") include(":i18n") include(":codec:lite", ":codec:rich") +include(":processor") +include(":annotation")