RouteDecoder.kt

/*
 * Copyright 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.navigation.serialization

import android.os.Bundle
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavType
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.AbstractDecoder
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule

/**
 * Decoder to deserialize a bundle of argument back into an object instance of type [T]
 *
 * This decoder iterates through every class field (argument) in [T], retrieves the value
 * for that argument from the bundle (or fallback to default value), then use the retrieved values
 * to re-create the object instance.
 */
@OptIn(ExperimentalSerializationApi::class)
internal class RouteDecoder : AbstractDecoder {

    // Bundle as argument source
    constructor(
        bundle: Bundle,
        typeMap: Map<String, NavType<*>>
    ) {
        val store = BundleArgStore(bundle, typeMap)
        decoder = Decoder(store)
    }

    // SavedStateHandle as argument source
    constructor(
        handle: SavedStateHandle
    ) {
        val store = SavedStateArgStore(handle)
        decoder = Decoder(store)
    }

    private val decoder: Decoder

    @Suppress("DEPRECATION") // deprecated in 1.6.3
    override val serializersModule: SerializersModule = EmptySerializersModule

    /**
     * Decodes the index of the next element to be decoded. Index represents a position of the
     * current element in the [descriptor] that can be found with [descriptor].getElementIndex.
     *
     * The returned index will trigger deserializer to call [decodeValue] on the argument
     * at that index.
     *
     * The decoder continually calls this method to process the next available argument until
     * this method returns [CompositeDecoder.DECODE_DONE], which indicates that there are
     * no more arguments to decode.
     *
     * This method should sequentially return the element index for every element that has its
     * value available within the [ArgStore]. For more details,
     * see [Decoder.computeNextElementIndex].
     */
    override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
        return decoder.computeNextElementIndex(descriptor)
    }

    /**
     * Returns argument value from the [ArgStore] for the argument at the index returned from
     * [decodeElementIndex]
     */
    override fun decodeValue(): Any = decoder.decodeValue()

    override fun decodeNull(): Nothing? = null

    // we want to know if it is not null, so its !isNull
    override fun decodeNotNullMark(): Boolean = !decoder.isCurrentElementNull()

    // value from decodeValue() rather than decodeInt, decodeBoolean etc.. needs to be casted
    @Suppress("UNCHECKED_CAST")
    override fun <T> decodeSerializableElement(
        descriptor: SerialDescriptor,
        index: Int,
        deserializer: DeserializationStrategy<T>,
        previousValue: T?
    ): T = decoder.decodeValue() as T
}

private class Decoder(private val store: ArgStore) {
    private var elementIndex: Int = -1
    private var elementName: String = ""

    /**
     * Computes the index of the next element to call [decodeValue] on.
     *
     * [decodeValue] should only be called for arguments with values stored within [store].
     * Otherwise, we should let the deserializer fall back to default value. This is done by
     * skipping (not returning) the indices whose argument is not present in the bundle. In doing
     * so, the deserializer considers the skipped element un-processed and will use the
     * default value (if present) instead.
     */
    @OptIn(ExperimentalSerializationApi::class)
    fun computeNextElementIndex(descriptor: SerialDescriptor): Int {
        var currentIndex = elementIndex
        while (true) {
            // proceed to next element
            currentIndex++
            // if we have reached the end, let decoder know there are not more arguments to decode
            if (currentIndex >= descriptor.elementsCount) return CompositeDecoder.DECODE_DONE
            val currentName = descriptor.getElementName(currentIndex)
            // Check if bundle has argument value. If so, we tell decoder to process
            // currentIndex. Otherwise, we skip this index and proceed to next index.
            if (store.contains(currentName)) {
                elementIndex = currentIndex
                elementName = currentName
                return elementIndex
            }
        }
    }

    /**
     * Retrieves argument value stored in the bundle
     */
    fun decodeValue(): Any {
        val arg = store.get(elementName)
        checkNotNull(arg) {
            "Unexpected null value for non-nullable argument $elementName"
        }
        return arg
    }

    fun isCurrentElementNull() = store.get(elementName) == null
}

// key-value map of argument values where the key is argument name
private abstract class ArgStore {
    // Retrieves argument value from store
    abstract fun get(key: String): Any?
    // Checks if store contains argument for key
    abstract fun contains(key: String): Boolean
}

private class SavedStateArgStore(private val handle: SavedStateHandle) : ArgStore() {
    override fun get(key: String): Any? = handle[key]
    override fun contains(key: String) = handle.contains(key)
}

private class BundleArgStore(
    private val bundle: Bundle,
    private val typeMap: Map<String, NavType<*>>
) : ArgStore() {
    override fun get(key: String): Any? {
        val navType = typeMap[key]
        return navType?.get(bundle, key)
    }
    override fun contains(key: String) = bundle.containsKey(key)
}