From b7dc03dfc047567b6e8a32a2d486a05e7ff38aa6 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sat, 6 Jun 2026 20:33:44 +0000 Subject: [PATCH] AppUtils: handle generic collection types for toJsonLiteral --- .../cloudstream3/SerializationClassTester.kt | 26 ++++++--- .../lagradost/cloudstream3/utils/AppUtils.kt | 56 ++++++++++++++++--- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt index 0b19535cbd7..d1a11e00365 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt @@ -3,7 +3,7 @@ package com.lagradost.cloudstream3 import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.lagradost.cloudstream3.utils.AppUtils.toJson -import io.github.classgraph.ClassGraph +import dalvik.system.DexFile import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer @@ -100,19 +100,27 @@ class SerializationClassTester { } } + // DEX files are the best solution to read all our classes dynamically. + // ClassGraph() can be used instead, but it only gives results on the JVM, not Android. + @Suppress("DEPRECATION") private fun findSerializableClasses(packageName: String): List> { val context = InstrumentationRegistry .getInstrumentation() .targetContext - return ClassGraph() - .enableClassInfo() - .enableAnnotationInfo() - .overrideClassLoaders(context.classLoader) - .acceptPackages(packageName) - .scan() - .getClassesWithAnnotation(Serializable::class.java.name) - .mapNotNull { runCatching { Class.forName(it.name, false, context.classLoader).kotlin }.getOrNull() } + val dexFile = DexFile(context.packageCodePath) + + return dexFile.entries() + .toList() + .filter { it.startsWith(packageName) } + .mapNotNull { + runCatching { Class.forName(it).kotlin }.getOrNull() + }.filter { kClass -> + // Not possible to use .hasAnnotation() on newer Android versions. + kClass.java.annotations.any { + it is Serializable + } + } } @OptIn(InternalSerializationApi::class) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt index 3d9cc8944f5..a002deb3155 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -9,6 +9,10 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.SetSerializer +import kotlinx.serialization.builtins.serializer import kotlinx.serialization.serializer import kotlinx.serialization.serializerOrNull import kotlin.reflect.KClass @@ -28,19 +32,56 @@ object AppUtils { // registered manually in serializersModule, we need both to support all cases val serializer = this::class.serializerOrNull() ?: json.serializersModule.getContextual(this::class) - return if (serializer != null) { + if (serializer != null) { try { @Suppress("UNCHECKED_CAST") - json.encodeToString(serializer as KSerializer, this) + return json.encodeToString(serializer as KSerializer, this) } catch (e: SerializationException) { logError(e) - mapper.writeValueAsString(this) + return mapper.writeValueAsString(this) + } + } + // Handle generic collection/map types where type params are erased at runtime + // and no serializer can be found via reflection alone + return try { + @Suppress("UNCHECKED_CAST") + when (this) { + is Array<*> -> json.encodeToString(ListSerializer(elementSerializer()), this.toList()) + is Set<*> -> json.encodeToString(SetSerializer(elementSerializer()), this) + is List<*> -> json.encodeToString(ListSerializer(elementSerializer()), this) + is Collection<*> -> json.encodeToString(ListSerializer(elementSerializer()), this.toList()) + is Map<*, *> -> json.encodeToString(MapSerializer(String.serializer(), valueSerializer()), this as Map) + else -> mapper.writeValueAsString(this) } - } else { + } catch (e: SerializationException) { + logError(e) mapper.writeValueAsString(this) } } + private fun elementSerializerForClass(kClass: KClass<*>): KSerializer { + val serializer = kClass.serializerOrNull() + ?: json.serializersModule.getContextual(kClass) + ?: throw SerializationException("No serializer found for element type ${kClass.simpleName}") + @Suppress("UNCHECKED_CAST") + return serializer as KSerializer + } + + private fun Collection<*>.elementSerializer(): KSerializer { + val elementClass = this.firstOrNull()?.let { it::class } ?: String::class + return elementSerializerForClass(elementClass) + } + + private fun Array<*>.elementSerializer(): KSerializer { + val elementClass = this.firstOrNull()?.let { it::class } ?: String::class + return elementSerializerForClass(elementClass) + } + + private fun Map<*, *>.valueSerializer(): KSerializer { + val elementClass = this.values.firstOrNull()?.let { it::class } ?: String::class + return elementSerializerForClass(elementClass) + } + @InternalAPI fun parseJson(value: String, kClass: KClass): T { val serializer = kClass.serializerOrNull() ?: json.serializersModule.getContextual(kClass) @@ -60,9 +101,8 @@ object AppUtils { inline fun parseJson(value: String): T { // @Serializable generates a serializer at compile time; contextual serializers are // registered manually in serializersModule, we need both to support all cases - val serializer = runCatching { serializer() }.runCatching { - json.serializersModule.getContextual(T::class) - }.getOrNull() + val serializer = runCatching { serializer() }.getOrNull() + ?: json.serializersModule.getContextual(T::class) // Prefer Kotlin Serialization over Jackson if (serializer != null) { @@ -94,4 +134,4 @@ object AppUtils { null } } -} \ No newline at end of file +}