diff --git a/app/src/main/java/io/github/ryunen344/suburi/navigation/NavType.kt b/app/src/main/java/io/github/ryunen344/suburi/navigation/NavType.kt new file mode 100644 index 0000000..bb15cdd --- /dev/null +++ b/app/src/main/java/io/github/ryunen344/suburi/navigation/NavType.kt @@ -0,0 +1,132 @@ +package io.github.ryunen344.suburi.navigation + +import android.os.Bundle +import androidx.core.os.BundleCompat +import androidx.navigation.NavType +import io.github.ryunen344.suburi.util.deserialize +import io.github.ryunen344.suburi.util.serialize +import timber.log.Timber +import java.util.Base64 +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +@Suppress("FunctionName") +inline fun <reified T : java.io.Serializable> SerializableNavTypeMap(): Map<KType, SerializableNavType<T>> = + mapOf(typeOf<T>() to SerializableNavType<T>()) + +@Suppress("FunctionName") +inline fun <reified T : java.io.Serializable> SerializableNavTypeMap( + noinline onParseValue: ((String) -> T?), +): Map<KType, SerializableNavType<T>> = mapOf(typeOf<T>() to SerializableNavType<T>(onParseValue)) + +inline fun <reified T : java.io.Serializable> SerializableNavType() = SerializableNavType(T::class.java) + +inline fun <reified T : java.io.Serializable> SerializableNavType(noinline onParseValue: ((String) -> T?)) = + SerializableNavType(T::class.java, onParseValue) + +class SerializableNavType<T : java.io.Serializable>( + val clazz: Class<T>, + private val onParseValue: ((String) -> T?)? = null, +) : NavType<T>(false) { + + override val name: String + get() = clazz.name + + override fun put(bundle: Bundle, key: String, value: T) { + bundle.putSerializable(key, value) + } + + override fun get(bundle: Bundle, key: String): T? { + return BundleCompat.getSerializable(bundle, key, clazz) + } + + override fun parseValue(value: String): T { + Timber.d("parseValue $value") + return onParseValue?.invoke(value) ?: Base64.getUrlDecoder().decode(value).deserialize() + } + + override fun serializeAsValue(value: T): String { + Timber.d("serializeAsValue $value") + return Base64.getUrlEncoder().encodeToString(value.serialize()) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SerializableNavType<*> + + if (clazz != other.clazz) return false + if (onParseValue != other.onParseValue) return false + + return true + } + + override fun hashCode(): Int { + var result = clazz.hashCode() + result = 31 * result + (onParseValue?.hashCode() ?: 0) + return result + } +} + +@Suppress("FunctionName") +inline fun <reified T : java.io.Serializable> NullableSerializableNavTypeMap(): Map<KType, NullableSerializableNavType<T>> = + mapOf(typeOf<T>() to NullableSerializableNavType<T>()) + +@Suppress("FunctionName") +inline fun <reified T : java.io.Serializable> NullableSerializableNavTypeMap( + noinline onParseValue: ((String) -> T?), +): Map<KType, NullableSerializableNavType<T>> = mapOf(typeOf<T>() to NullableSerializableNavType<T>(onParseValue)) + +inline fun <reified T : java.io.Serializable> NullableSerializableNavType() = NullableSerializableNavType(T::class.java) + +inline fun <reified T : java.io.Serializable> NullableSerializableNavType(noinline onParseValue: ((String) -> T?)) = + NullableSerializableNavType(T::class.java, onParseValue) + +class NullableSerializableNavType<T : java.io.Serializable>( + val clazz: Class<T>, + private val onParseValue: ((String) -> T?)? = null, +) : NavType<T?>(true) { + + override val name: String + get() = clazz.name + + override fun put(bundle: Bundle, key: String, value: T?) { + bundle.putSerializable(key, value) + } + + override fun get(bundle: Bundle, key: String): T? { + return BundleCompat.getSerializable(bundle, key, clazz) + } + + override fun parseValue(value: String): T? { + return if (value == "null") null else onParseValue?.invoke(value) ?: Base64.getUrlDecoder().decode(value).deserialize() + } + + override fun serializeAsValue(value: T?): String { + Timber.d("serializeAsValue $value") + return if (value == null) { + "null" + } else { + Base64.getUrlEncoder().encodeToString(value.serialize()) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NullableSerializableNavType<*> + + if (clazz != other.clazz) return false + if (onParseValue != other.onParseValue) return false + + return true + } + + override fun hashCode(): Int { + var result = clazz.hashCode() + result = 31 * result + (onParseValue?.hashCode() ?: 0) + return result + } +} diff --git a/app/src/main/java/io/github/ryunen344/suburi/ui/screen/Routes.kt b/app/src/main/java/io/github/ryunen344/suburi/ui/screen/Routes.kt index 2b1e305..b171f32 100644 --- a/app/src/main/java/io/github/ryunen344/suburi/ui/screen/Routes.kt +++ b/app/src/main/java/io/github/ryunen344/suburi/ui/screen/Routes.kt @@ -1,6 +1,5 @@ package io.github.ryunen344.suburi.ui.screen -import android.net.Uri import android.os.Bundle import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedContentTransitionScope @@ -8,7 +7,6 @@ import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.SizeTransform import androidx.compose.runtime.Composable -import androidx.core.os.BundleCompat import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDeepLink @@ -20,6 +18,7 @@ import androidx.navigation.get import androidx.navigation.internalToRoute import androidx.navigation.navDeepLink import androidx.navigation.serialization.decodeArguments +import io.github.ryunen344.suburi.navigation.SerializableNavTypeMap import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -29,13 +28,20 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.serializer import timber.log.Timber -import java.io.ByteArrayOutputStream -import java.io.ObjectInputStream -import java.io.ObjectOutputStream import java.util.UUID import kotlin.reflect.KClass import kotlin.reflect.KType -import kotlin.reflect.typeOf + +val onWrappedUuidParse: ((String) -> WrappedUuid?) = { value -> + runCatching { + UUID.fromString(value) + .let(::WrappedUuid) + .also { Timber.d("from Deeplink $it") } + }.getOrElse { + Timber.d("from Navigation") + null + } +} @Serializable sealed class Routes { @@ -52,8 +58,8 @@ sealed class Routes { inline val <reified T : Routes> KClass<T>.typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> get() = when (this) { Routes.Top::class -> emptyMap() - Routes.Uuid::class -> WrappedUuid.typeMap - Routes.Structures::class -> Structure.typeMap + Routes.Uuid::class -> SerializableNavTypeMap<WrappedUuid>(onParseValue = onWrappedUuidParse) + Routes.Structures::class -> SerializableNavTypeMap<Structure>() else -> error("unexpected type parameter") } @@ -61,7 +67,10 @@ val <T : Routes> KClass<T>.deepLinks: List<NavDeepLink> get() = when (this) { Routes.Top::class -> emptyList() Routes.Uuid::class -> listOf( - navDeepLink<Routes.Uuid>(basePath = "https://www.example.com/uuid", typeMap = WrappedUuid.typeMap), + navDeepLink<Routes.Uuid>( + basePath = "https://www.example.com/uuid", + typeMap = SerializableNavTypeMap<WrappedUuid>(onParseValue = onWrappedUuidParse), + ), ) Routes.Structures::class -> emptyList() @@ -69,78 +78,11 @@ val <T : Routes> KClass<T>.deepLinks: List<NavDeepLink> } @Serializable -data class WrappedUuid(@Serializable(with = UUIDSerializer::class) val value: UUID) : java.io.Serializable { - companion object { - val navType = object : NavType<WrappedUuid>(false) { - override fun get(bundle: Bundle, key: String): WrappedUuid? { - return BundleCompat.getSerializable(bundle, key, WrappedUuid::class.java) - } - - override fun parseValue(value: String): WrappedUuid { - Timber.d("parseValue $value") - return runCatching { - UUID.fromString(value) - .let(::WrappedUuid) - .also { Timber.d("from Deeplink $it") } - }.getOrElse { - Timber.d("from Navigation") - Uri.decode(value) - .hexToByteArray() - .inputStream() - .use(::ObjectInputStream) - .use(ObjectInputStream::readObject) as WrappedUuid - } - } - - override fun put(bundle: Bundle, key: String, value: WrappedUuid) { - bundle.putSerializable(key, value) - } - - override fun serializeAsValue(value: WrappedUuid): String { - Timber.d("serializeAsValue $value") - val hex = ByteArrayOutputStream().apply { - use(::ObjectOutputStream).use { it.writeObject(value) } - }.toByteArray().toHexString() - return Uri.encode(hex) - } - } - - val typeMap = mapOf(typeOf<WrappedUuid>() to navType) - } -} +data class WrappedUuid(@Serializable(with = UUIDSerializer::class) val value: UUID) : java.io.Serializable @Serializable data class Structure(val value1: String, val value2: Long, val value3: String) : java.io.Serializable { companion object { - val navType = object : NavType<Structure>(false) { - override fun get(bundle: Bundle, key: String): Structure? { - return BundleCompat.getSerializable(bundle, key, Structure::class.java) - } - - override fun parseValue(value: String): Structure { - Timber.d("parseValue $value") - return Uri.decode(value) - .hexToByteArray() - .inputStream() - .use(::ObjectInputStream) - .use(ObjectInputStream::readObject) as Structure - } - - override fun put(bundle: Bundle, key: String, value: Structure) { - bundle.putSerializable(key, value) - } - - override fun serializeAsValue(value: Structure): String { - Timber.d("serializeAsValue $value") - val hex = ByteArrayOutputStream().apply { - use(::ObjectOutputStream).use { it.writeObject(value) } - }.toByteArray().toHexString() - return Uri.encode(hex) - } - } - - val typeMap = mapOf(typeOf<Structure>() to navType) - fun random(): Structure { return Structure( value1 = UUID.randomUUID().toString(), diff --git a/app/src/main/java/io/github/ryunen344/suburi/util/Serializable.kt b/app/src/main/java/io/github/ryunen344/suburi/util/Serializable.kt new file mode 100644 index 0000000..012dbc5 --- /dev/null +++ b/app/src/main/java/io/github/ryunen344/suburi/util/Serializable.kt @@ -0,0 +1,19 @@ +package io.github.ryunen344.suburi.util + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.Serializable + +fun Serializable.serialize(): ByteArray { + return ByteArrayOutputStream(DEFAULT_BUFFER_SIZE).use { byteArrayOutputStream -> + ObjectOutputStream(byteArrayOutputStream).use { it.writeObject(this) } + byteArrayOutputStream.toByteArray() + } +} + +@Suppress("UNCHECKED_CAST") +fun <T : Serializable> ByteArray.deserialize(): T { + return ObjectInputStream(ByteArrayInputStream(this)).use(ObjectInputStream::readObject) as T +}