diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/state/impl/StateRepositoryImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/state/impl/StateRepositoryImpl.kt index 9abe20a1..55fb7fbd 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/state/impl/StateRepositoryImpl.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/state/impl/StateRepositoryImpl.kt @@ -73,6 +73,27 @@ fun JsonObject.toMap(): Map = this element.extractedContent } +fun Any?.toSchema(): JsonElement { + return when (this) { + null -> JsonNull + is String -> JsonPrimitive("string") + is Number -> JsonPrimitive("number") + is Boolean -> JsonPrimitive("boolean") + is Map<*, *> -> { + val content = map { (k, v) -> k.toString() to v.toSchema() } + JsonObject(content.toMap()) + } + + is List<*> -> { + val content = map { it.toSchema() } + JsonArray(content) + } + + is JsonElement -> JsonPrimitive(this::class.qualifiedName) + else -> JsonPrimitive("unknown:" + this::class.qualifiedName) + } +} + fun Any?.toJson(): JsonElement = when (this) { null -> JsonNull is String -> JsonPrimitive(this) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/PutScreenStateImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/PutScreenStateImpl.kt index 18709a09..77403b57 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/PutScreenStateImpl.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/PutScreenStateImpl.kt @@ -2,9 +2,14 @@ package com.artemchep.keyguard.common.usecase.impl import com.artemchep.keyguard.common.io.IO import com.artemchep.keyguard.common.service.state.StateRepository +import com.artemchep.keyguard.common.service.state.impl.toSchema import com.artemchep.keyguard.common.usecase.PutScreenState +import com.artemchep.keyguard.feature.crashlytics.crashlyticsTap +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import org.kodein.di.DirectDI import org.kodein.di.instance +import java.io.IOException class PutScreenStateImpl( private val stateRepository: StateRepository, @@ -13,6 +18,21 @@ class PutScreenStateImpl( stateRepository = directDI.instance(), ) + private class FailedToSaveScreenStateException( + message: String, + e: Throwable, + ) : IOException(message, e) + override fun invoke(key: String, state: Map): IO = stateRepository .put(key, state) + // We have tried to save something that is not + // serializable. Notify the developer so he has + // a chance to fix it. + .crashlyticsTap { e -> + val schema = Json.encodeToString(state.toSchema()) + FailedToSaveScreenStateException( + message = schema, + e = e, + ) + } } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/DiskHandleImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/DiskHandleImpl.kt index 2448b918..0b4edb2a 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/DiskHandleImpl.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/DiskHandleImpl.kt @@ -1,12 +1,15 @@ package com.artemchep.keyguard.feature.navigation.state import arrow.core.Either +import arrow.core.getOrElse import com.artemchep.keyguard.common.io.attempt import com.artemchep.keyguard.common.io.bind import com.artemchep.keyguard.common.io.toIO +import com.artemchep.keyguard.common.service.state.impl.toSchema import com.artemchep.keyguard.common.usecase.GetScreenState import com.artemchep.keyguard.common.usecase.PutScreenState import com.artemchep.keyguard.common.util.flow.combineToList +import com.artemchep.keyguard.feature.crashlytics.crashlyticsAttempt import com.artemchep.keyguard.feature.crashlytics.crashlyticsTap import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.CoroutineScope @@ -18,6 +21,9 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.IOException class DiskHandleImpl private constructor( private val scope: CoroutineScope, @@ -50,6 +56,11 @@ class DiskHandleImpl private constructor( } } + private class FailedToSerializeScreenStateException( + message: String, + e: Throwable, + ) : IOException(message, e) + private val registrySink = MutableStateFlow(persistentMapOf>()) init { @@ -61,12 +72,32 @@ class DiskHandleImpl private constructor( // key of the variable. value .map { f -> key to f } + .crashlyticsAttempt { e -> + val schema = Json.encodeToString(state.toSchema()) + FailedToSerializeScreenStateException( + message = schema, + e = e, + ) + } } .combineToList() } .debounce(SAVE_DEBOUNCE_MS) // no need to save all of the events .onEach { entries -> - val state = entries.toMap() + // Start by using the restored state. This is needed because + // of this flow: + // 1. A user has all of options configured. + // 2. A user opens a screen for a super brief moment, that + // either loads a different set of options or loads only + // a set of all options. + // 3. Previous state gets overwritten. + val state = restoredState.toMutableMap() + entries.forEach { result -> + val entry = result.getOrElse { + return@forEach + } + state[entry.first] = entry.second + } val result = tryWrite(state) if (result is Either.Left) { val e = result.value