diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 4d04a357625..137d0b08b72 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -32,7 +32,7 @@ jobs: # run: ./gradlew library:checkKotlinAbi - name: Run Gradle - run: ./gradlew assemblePrereleaseDebug + run: ./gradlew assemblePrereleaseDebug lint check - name: Upload Artifact uses: actions/upload-artifact@v7 diff --git a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt b/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt index 466ef5a007c..3122169846a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt @@ -121,7 +121,7 @@ class CloudStreamApp : Application(), SingletonImageLoader.Factory { } fun setKeyClass(path: String, value: T) { - context?.setKey(path, value) + context?.setKey(path, value) } fun removeKeys(folder: String): Int? { @@ -132,6 +132,11 @@ class CloudStreamApp : Application(), SingletonImageLoader.Factory { context?.setKey(path, value) } + @JvmName("setKeyReified") + inline fun setKey(path: String, value: T) { + context?.setKey(path, value) + } + fun setKey(folder: String, path: String, value: T) { context?.setKey(folder, path, value) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index 02ee697911f..9510c31b7c5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -7,6 +7,7 @@ import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKeyClass import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKeyClass +import com.lagradost.cloudstream3.InternalAPI import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJsonLiteral @@ -170,22 +171,34 @@ object DataStore { } } - fun Context.setKey(path: String, value: T) { + @InternalAPI + fun Context.putStringKey(path: String, value: String?) { try { getSharedPrefs().edit { - putString(path, value?.toJsonLiteral()) + putString(path, value) } } catch (e: Exception) { logError(e) } } + /** Non-reified fallback for binary compat. Prefer the reified overload where possible. */ + fun Context.setKey(path: String, value: T) { + putStringKey(path, value?.toJsonLiteral()) + } + + /** Reified overload, prevents type erasure for generic types. */ + @JvmName("setKeyReified") + inline fun Context.setKey(path: String, value: T) { + putStringKey(path, value.toJsonLiteral()) + } + fun Context.getKey(path: String, valueType: Class): T? { - try { + return try { val json: String = getSharedPrefs().getString(path, null) ?: return null - return parseJson(json, valueType.kotlin) - } catch (e: Exception) { - return null + parseJson(json, valueType.kotlin) + } catch (_: Exception) { + null } } @@ -193,21 +206,37 @@ object DataStore { setKey(getFolderName(folder, path), value) } + @Deprecated( + message = "Use parseJson(this) directly instead.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith( + expression = "parseJson(this)", + imports = ["com.lagradost.cloudstream3.utils.AppUtils.parseJson"], + ), + ) inline fun String.toKotlinObject(): T { return parseJson(this) } + @Deprecated( + message = "Use parseJson(this) directly instead.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith( + expression = "parseJson(this)", + imports = ["com.lagradost.cloudstream3.utils.AppUtils.parseJson"], + ), + ) fun String.toKotlinObject(valueType: Class): T { return parseJson(this, valueType.kotlin) } // GET KEY GIVEN PATH AND DEFAULT VALUE, NULL IF ERROR inline fun Context.getKey(path: String, defVal: T?): T? { - try { + return try { val json: String = getSharedPrefs().getString(path, null) ?: return defVal - return json.toKotlinObject() - } catch (e: Exception) { - return null + parseJson(json) + } catch (_: Exception) { + null } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadQueueManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadQueueManager.kt index f3866408833..58aae29d348 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadQueueManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadQueueManager.kt @@ -53,7 +53,7 @@ object DownloadQueueManager { fun init(context: Context) { ioSafe { _queue.collect { queue -> - setKey(QUEUE_KEY, queue) + setKey>(QUEUE_KEY, queue) } } @@ -247,4 +247,4 @@ object DownloadQueueManager { localQueue.copyOf() } } -} \ No newline at end of file +} diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 59a36ce7fdc..64d96505e30 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -84,17 +84,6 @@ kotlin { androidMain { dependsOn(jvmCommonMain) } jvmMain { dependsOn(jvmCommonMain) } } - - @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class) - // https://kotlinlang.org/docs/gradle-binary-compatibility-validation.html - abiValidation { - filters { - exclude { - annotatedWith.add("com.lagradost.cloudstream3.Prerelease") - annotatedWith.add("com.lagradost.cloudstream3.InternalAPI") - } - } - } } tasks.withType { 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 1c635013fc2..26d5daa6ffc 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.module.kotlin.readValue import com.lagradost.cloudstream3.InternalAPI import com.lagradost.cloudstream3.json import com.lagradost.cloudstream3.mapper +import com.lagradost.cloudstream3.mvvm.debugPrint import com.lagradost.cloudstream3.mvvm.logError import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi @@ -21,60 +22,89 @@ object AppUtils { return toJsonLiteral() } - /** Sometimes we want to encode as JSON even if it is already a String. */ @InternalAPI - fun Any.toJsonLiteral(): String { - // @Serializable generates a serializer at compile time; contextual serializers are - // 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) { + fun Any.toJsonLiteralImpl(serializer: KSerializer?): String { + var fallbackTrace: String? = null + if (serializer != null) { try { - @Suppress("UNCHECKED_CAST") - json.encodeToString(serializer as KSerializer, this) + debugPrint { "AppUtils/toJsonLiteral: using kotlinx serialization for ${this::class.qualifiedName}" } + return json.encodeToString(serializer, this) } catch (e: SerializationException) { logError(e) - mapper.writeValueAsString(this) + fallbackTrace = e.stackTraceToString() + debugPrint { "AppUtils/toJsonLiteral: kotlinx failed, falling back to Jackson for ${this::class.qualifiedName}" } } } else { - mapper.writeValueAsString(this) + fallbackTrace = Exception().stackTraceToString() } + debugPrint { "AppUtils/toJsonLiteral: using Jackson for ${this::class.qualifiedName}\n$fallbackTrace" } + return mapper.writeValueAsString(this) + } + + /** Runtime lookup version, subject to type erasure for generic types. */ + @InternalAPI + fun Any.toJsonLiteral(): String { + val serializer = this::class.serializerOrNull() + ?: json.serializersModule.getContextual(this::class) + @Suppress("UNCHECKED_CAST") + return toJsonLiteralImpl(serializer as KSerializer?) + } + + /** Reified version, preserves full generic type info at call site. */ + @InternalAPI + @JvmName("toJsonLiteralReified") + inline fun T.toJsonLiteral(): String { + val serializer = runCatching { serializer() } + .recoverCatching { json.serializersModule.getContextual(T::class) } + .getOrNull() + @Suppress("UNCHECKED_CAST") + return toJsonLiteralImpl(serializer as KSerializer?) } @InternalAPI fun parseJson(value: String, kClass: KClass): T { val serializer = kClass.serializerOrNull() ?: json.serializersModule.getContextual(kClass) + var fallbackTrace: String? = null if (serializer != null) { try { + debugPrint { "AppUtils/parseJson(kClass): using kotlinx serialization for ${kClass.qualifiedName}" } return json.decodeFromString(serializer, value) } catch (e: SerializationException) { logError(e) + fallbackTrace = e.stackTraceToString() + debugPrint { "AppUtils/parseJson(kClass): kotlinx failed, falling back to Jackson for ${kClass.qualifiedName}" } } + } else { + fallbackTrace = Exception().stackTraceToString() } - + debugPrint { "AppUtils/parseJson(kClass): using Jackson for ${kClass.qualifiedName}\n$fallbackTrace" } return mapper.readValue(value, kClass.java) } // This is inlined code and can easily cause breakage in extensions! // Watch out when editing this to make sure stable also supports all inlined code! 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() } .recoverCatching { json.serializersModule.getContextual(T::class) } .getOrNull() - // Prefer Kotlin Serialization over Jackson + var fallbackTrace: String? = null if (serializer != null) { try { + debugPrint { "AppUtils/parseJson: using kotlinx serialization for ${T::class.qualifiedName}" } return json.decodeFromString(serializer, value) } catch (e: SerializationException) { logError(e) - } catch (_: Throwable) { - // Pass, the above code will trigger a NoSuchMethodError on stable due to our previously undefined json variable + fallbackTrace = e.stackTraceToString() + debugPrint { "AppUtils/parseJson: kotlinx failed, falling back to Jackson for ${T::class.qualifiedName}" } + } catch (e: Throwable) { + fallbackTrace = e.stackTraceToString() + debugPrint { "AppUtils/parseJson: unexpected error for ${T::class.qualifiedName}, falling back to Jackson" } } + } else { + fallbackTrace = Exception().stackTraceToString() } - + debugPrint { "AppUtils/parseJson: using Jackson for ${T::class.qualifiedName}\n$fallbackTrace" } return mapper.readValue(value) } @@ -85,7 +115,6 @@ object AppUtils { replaceWith = ReplaceWith("parseJson(reader.readText())") ) inline fun parseJson(reader: java.io.Reader, valueType: Class): T { - // Reader-based parsing has no kotlinx equivalent, fall back to Jackson return mapper.readValue(reader, valueType) }