diff --git a/app/src/main/java/io/github/ryunen344/suburi/navigation/ParcelableNavType.kt b/app/src/main/java/io/github/ryunen344/suburi/navigation/ParcelableNavType.kt new file mode 100644 index 0000000..2331e55 --- /dev/null +++ b/app/src/main/java/io/github/ryunen344/suburi/navigation/ParcelableNavType.kt @@ -0,0 +1,136 @@ +package io.github.ryunen344.suburi.navigation + +import android.os.Bundle +import android.os.Parcelable +import androidx.core.os.BundleCompat +import androidx.navigation.NavType +import io.github.ryunen344.suburi.util.parcel +import io.github.ryunen344.suburi.util.unparcel +import timber.log.Timber +import java.util.Base64 +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +@Suppress("FunctionName") +inline fun ParcelableNavTypeMap(): Map> = + mapOf(typeOf() to ParcelableNavType()) + +@Suppress("FunctionName") +inline fun ParcelableNavTypeMap( + noinline onParseValue: ((String) -> T?), +): Map> = mapOf(typeOf() to ParcelableNavType(onParseValue)) + +inline fun ParcelableNavType() = ParcelableNavType(T::class.java) + +inline fun ParcelableNavType(noinline onParseValue: ((String) -> T?)) = + ParcelableNavType(T::class.java, onParseValue) + +class ParcelableNavType( + val clazz: Class, + private val onParseValue: ((String) -> T?)? = null, +) : NavType(false) { + + override val name: String = clazz.name + + override fun put(bundle: Bundle, key: String, value: T) { + bundle.putParcelable(key, value) + } + + override fun get(bundle: Bundle, key: String): T? { + return BundleCompat.getParcelable(bundle, key, clazz) + } + + override fun parseValue(value: String): T { + Timber.d("parseValue $value") + return onParseValue?.invoke(value) ?: Base64.getUrlDecoder().decode(value).unparcel(clazz) + } + + override fun serializeAsValue(value: T): String { + Timber.d("serializeAsValue $value") + return Base64.getUrlEncoder().encodeToString(value.parcel()) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ParcelableNavType<*> + + if (clazz != other.clazz) return false + if (onParseValue != other.onParseValue) return false + if (name != other.name) return false + + return true + } + + override fun hashCode(): Int { + var result = clazz.hashCode() + result = 31 * result + (onParseValue?.hashCode() ?: 0) + result = 31 * result + name.hashCode() + return result + } +} + +@Suppress("FunctionName") +inline fun NullableParcelableNavTypeMap(): Map> = + mapOf(typeOf() to NullableParcelableNavType()) + +@Suppress("FunctionName") +inline fun NullableParcelableNavTypeMap( + noinline onParseValue: ((String) -> T?), +): Map> = mapOf(typeOf() to NullableParcelableNavType(onParseValue)) + +inline fun NullableParcelableNavType() = NullableParcelableNavType(T::class.java) + +inline fun NullableParcelableNavType(noinline onParseValue: ((String) -> T?)) = + NullableParcelableNavType(T::class.java, onParseValue) + +class NullableParcelableNavType( + val clazz: Class, + private val onParseValue: ((String) -> T?)? = null, +) : NavType(true) { + + override val name: String = clazz.name + + override fun put(bundle: Bundle, key: String, value: T?) { + bundle.putParcelable(key, value) + } + + override fun get(bundle: Bundle, key: String): T? { + return BundleCompat.getParcelable(bundle, key, clazz) + } + + override fun parseValue(value: String): T? { + Timber.d("parseValue $value") + return if (value == "null") null else onParseValue?.invoke(value) ?: Base64.getUrlDecoder().decode(value).unparcel(clazz) + } + + override fun serializeAsValue(value: T?): String { + Timber.d("serializeAsValue $value") + return if (value == null) { + "null" + } else { + Base64.getUrlEncoder().encodeToString(value.parcel()) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NullableParcelableNavType<*> + + if (clazz != other.clazz) return false + if (onParseValue != other.onParseValue) return false + if (name != other.name) return false + + return true + } + + override fun hashCode(): Int { + var result = clazz.hashCode() + result = 31 * result + (onParseValue?.hashCode() ?: 0) + result = 31 * result + name.hashCode() + return result + } +} diff --git a/app/src/main/java/io/github/ryunen344/suburi/navigation/NavType.kt b/app/src/main/java/io/github/ryunen344/suburi/navigation/SerializableNavType.kt similarity index 93% rename from app/src/main/java/io/github/ryunen344/suburi/navigation/NavType.kt rename to app/src/main/java/io/github/ryunen344/suburi/navigation/SerializableNavType.kt index bb15cdd..f155d60 100644 --- a/app/src/main/java/io/github/ryunen344/suburi/navigation/NavType.kt +++ b/app/src/main/java/io/github/ryunen344/suburi/navigation/SerializableNavType.kt @@ -29,8 +29,7 @@ class SerializableNavType( private val onParseValue: ((String) -> T?)? = null, ) : NavType(false) { - override val name: String - get() = clazz.name + override val name: String = clazz.name override fun put(bundle: Bundle, key: String, value: T) { bundle.putSerializable(key, value) @@ -58,6 +57,7 @@ class SerializableNavType( if (clazz != other.clazz) return false if (onParseValue != other.onParseValue) return false + if (name != other.name) return false return true } @@ -65,6 +65,7 @@ class SerializableNavType( override fun hashCode(): Int { var result = clazz.hashCode() result = 31 * result + (onParseValue?.hashCode() ?: 0) + result = 31 * result + name.hashCode() return result } } @@ -88,8 +89,7 @@ class NullableSerializableNavType( private val onParseValue: ((String) -> T?)? = null, ) : NavType(true) { - override val name: String - get() = clazz.name + override val name: String = clazz.name override fun put(bundle: Bundle, key: String, value: T?) { bundle.putSerializable(key, value) @@ -100,6 +100,7 @@ class NullableSerializableNavType( } override fun parseValue(value: String): T? { + Timber.d("parseValue $value") return if (value == "null") null else onParseValue?.invoke(value) ?: Base64.getUrlDecoder().decode(value).deserialize() } @@ -120,6 +121,7 @@ class NullableSerializableNavType( if (clazz != other.clazz) return false if (onParseValue != other.onParseValue) return false + if (name != other.name) return false return true } @@ -127,6 +129,7 @@ class NullableSerializableNavType( override fun hashCode(): Int { var result = clazz.hashCode() result = 31 * result + (onParseValue?.hashCode() ?: 0) + result = 31 * result + name.hashCode() 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 b171f32..e918b12 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,7 @@ package io.github.ryunen344.suburi.ui.screen import android.os.Bundle +import android.os.Parcelable import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.EnterTransition @@ -18,7 +19,8 @@ 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 io.github.ryunen344.suburi.navigation.ParcelableNavTypeMap +import kotlinx.parcelize.Parcelize import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -58,8 +60,8 @@ sealed class Routes { inline val KClass.typeMap: Map> get() = when (this) { Routes.Top::class -> emptyMap() - Routes.Uuid::class -> SerializableNavTypeMap(onParseValue = onWrappedUuidParse) - Routes.Structures::class -> SerializableNavTypeMap() + Routes.Uuid::class -> ParcelableNavTypeMap(onParseValue = onWrappedUuidParse) + Routes.Structures::class -> ParcelableNavTypeMap() else -> error("unexpected type parameter") } @@ -69,7 +71,7 @@ val KClass.deepLinks: List Routes.Uuid::class -> listOf( navDeepLink( basePath = "https://www.example.com/uuid", - typeMap = SerializableNavTypeMap(onParseValue = onWrappedUuidParse), + typeMap = ParcelableNavTypeMap(onParseValue = onWrappedUuidParse), ), ) @@ -77,11 +79,13 @@ val KClass.deepLinks: List else -> error("unexpected type parameter") } +@Parcelize @Serializable -data class WrappedUuid(@Serializable(with = UUIDSerializer::class) val value: UUID) : java.io.Serializable +class WrappedUuid(@Serializable(with = UUIDSerializer::class) val value: UUID) : java.io.Serializable, Parcelable +@Parcelize @Serializable -data class Structure(val value1: String, val value2: Long, val value3: String) : java.io.Serializable { +class Structure(val value1: String, val value2: Long, val value3: String) : java.io.Serializable, Parcelable { companion object { fun random(): Structure { return Structure( diff --git a/app/src/main/java/io/github/ryunen344/suburi/util/Parcelable.kt b/app/src/main/java/io/github/ryunen344/suburi/util/Parcelable.kt new file mode 100644 index 0000000..8fb7dbf --- /dev/null +++ b/app/src/main/java/io/github/ryunen344/suburi/util/Parcelable.kt @@ -0,0 +1,52 @@ +package io.github.ryunen344.suburi.util + +import android.os.Parcel +import android.os.Parcelable +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +@Suppress("TooGenericExceptionCaught") +inline fun T.use(block: (T) -> R): R { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + var exception: Throwable? = null + try { + return block(this) + } catch (e: Throwable) { + exception = e + throw e + } finally { + this.recycleFinally(exception) + } +} + +@Suppress("TooGenericExceptionCaught") +fun Parcel?.recycleFinally(cause: Throwable?): Unit = when { + this == null -> {} + cause == null -> recycle() + else -> + try { + recycle() + } catch (closeException: Throwable) { + cause.addSuppressed(closeException) + } +} + +fun Parcelable.parcel(): ByteArray { + return Parcel.obtain().use { parcel -> + writeToParcel(parcel, Parcelable.PARCELABLE_WRITE_RETURN_VALUE) + parcel.marshall() + } +} + +@Suppress("UNCHECKED_CAST") +fun ByteArray.unparcel(clazz: Class): T { + val creator = clazz.getDeclaredField("CREATOR").get(null) as? Parcelable.Creator + ?: throw IllegalArgumentException("Could not access CREATOR field in class ${clazz.simpleName}") + return Parcel.obtain().use { parcel -> + parcel.unmarshall(this, 0, this.size) + parcel.setDataPosition(0) + creator.createFromParcel(parcel) + } +}