Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nav): impl Base64 NavArg #17

Merged
merged 2 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions app/src/main/java/io/github/ryunen344/suburi/navigation/NavType.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
96 changes: 19 additions & 77 deletions app/src/main/java/io/github/ryunen344/suburi/ui/screen/Routes.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
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
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
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -52,95 +58,31 @@ 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")
}

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()
else -> error("unexpected type parameter")
}

@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(),
Expand Down
19 changes: 19 additions & 0 deletions app/src/main/java/io/github/ryunen344/suburi/util/Serializable.kt
Original file line number Diff line number Diff line change
@@ -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
}