NavType.kt

/*
 * Copyright 2018 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

import android.os.Bundle
import android.os.Parcelable
import androidx.annotation.AnyRes
import androidx.annotation.RestrictTo
import java.io.Serializable

/**
 * NavType denotes the type that can be used in a [NavArgument].
 *
 * There are built-in NavTypes for primitive types, such as int, long, boolean, float, and strings,
 * parcelable, and serializable classes (including Enums), as well as arrays of each supported type.
 *
 * You should only use one of the static NavType instances and subclasses defined in this class.
 *
 * @param T the type of the data that is supported by this NavType
 */
public abstract class NavType<T> internal constructor(
    /**
     * Check if an argument with this type can hold a null value.
     * @return Returns true if this type allows null values, false otherwise.
     */
    public open val isNullableAllowed: Boolean
) {

    /**
     * Put a value of this type in the `bundle`
     *
     * @param bundle bundle to put value in
     * @param key    bundle key
     * @param value  value of this type
     */
    public abstract fun put(bundle: Bundle, key: String, value: T)

    /**
     * Get a value of this type from the `bundle`
     *
     * @param bundle bundle to get value from
     * @param key    bundle key
     * @return value of this type
     */
    public abstract operator fun get(bundle: Bundle, key: String): T?

    /**
     * Parse a value of this type from a String.
     *
     * @param value string representation of a value of this type
     * @return parsed value of the type represented by this NavType
     * @throws IllegalArgumentException if value cannot be parsed into this type
     */
    public abstract fun parseValue(value: String): T

    /**
     * Parse a value of this type from a String and put it in a `bundle`
     *
     * @param bundle bundle to put value in
     * @param key    bundle key under which to put the value
     * @param value  parsed value
     * @return parsed value of the type represented by this NavType
     * @suppress
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public fun parseAndPut(bundle: Bundle, key: String, value: String): T {
        val parsedValue = parseValue(value)
        put(bundle, key, parsedValue)
        return parsedValue
    }

    /**
     * The name of this type.
     *
     * This is the same value that is used in Navigation XML `argType` attribute.
     *
     * @return name of this type
     */
    public abstract val name: String

    override fun toString(): String {
        return name
    }

    public companion object {
        /**
         * Parse an argType string into a NavType.
         *
         * @param type        argType string, usually parsed from the Navigation XML file
         * @param packageName package name of the R file,
         * used for parsing relative class names starting with a dot.
         * @return a NavType representing the type indicated by the argType string.
         * Defaults to StringType for null.
         * @throws IllegalArgumentException if there is no valid argType
         * @throws RuntimeException if the type class name cannot be found
         */
        @Suppress("NON_FINAL_MEMBER_IN_OBJECT", "UNCHECKED_CAST") // this needs to be open to
        // maintain api compatibility and type cast are unchecked
        @JvmStatic
        public open fun fromArgType(type: String?, packageName: String?): NavType<*> {
            when {
                IntType.name == type -> return IntType
                IntArrayType.name == type -> return IntArrayType
                LongType.name == type -> return LongType
                LongArrayType.name == type -> return LongArrayType
                BoolType.name == type -> return BoolType
                BoolArrayType.name == type -> return BoolArrayType
                StringType.name == type -> return StringType
                StringArrayType.name == type -> return StringArrayType
                FloatType.name == type -> return FloatType
                FloatArrayType.name == type -> return FloatArrayType
                ReferenceType.name == type -> return ReferenceType
                !type.isNullOrEmpty() -> {
                    try {
                        var className: String
                        className = if (type.startsWith(".") && packageName != null) {
                            packageName + type
                        } else {
                            type
                        }
                        if (type.endsWith("[]")) {
                            className = className.substring(0, className.length - 2)
                            val clazz = Class.forName(className)
                            when {
                                Parcelable::class.java.isAssignableFrom(clazz) -> {
                                    return ParcelableArrayType(clazz as Class<Parcelable>)
                                }
                                Serializable::class.java.isAssignableFrom(clazz) -> {
                                    return SerializableArrayType(clazz as Class<Serializable>)
                                }
                            }
                        } else {
                            val clazz = Class.forName(className)
                            when {
                                Parcelable::class.java.isAssignableFrom(clazz) -> {
                                    return ParcelableType(clazz as Class<Any?>)
                                }
                                Enum::class.java.isAssignableFrom(clazz) -> {
                                    return EnumType(clazz as Class<Enum<*>>)
                                }
                                Serializable::class.java.isAssignableFrom(clazz) -> {
                                    return SerializableType(clazz as Class<Serializable>)
                                }
                            }
                        }
                        throw IllegalArgumentException(
                            "$className is not Serializable or Parcelable."
                        )
                    } catch (e: ClassNotFoundException) {
                        throw RuntimeException(e)
                    }
                }
            }
            return StringType
        }

        /** @suppress */
        @Suppress("UNCHECKED_CAST") // needed for cast to NavType<Any>
        @JvmStatic
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        public fun inferFromValue(value: String): NavType<Any> {
            // because we allow Long literals without the L suffix at runtime,
            // the order of IntType and LongType parsing has to be reversed compared to Safe Args
            try {
                IntType.parseValue(value)
                return IntType as NavType<Any>
            } catch (e: IllegalArgumentException) {
                // ignored, proceed to check next type
            }
            try {
                LongType.parseValue(value)
                return LongType as NavType<Any>
            } catch (e: IllegalArgumentException) {
                // ignored, proceed to check next type
            }
            try {
                FloatType.parseValue(value)
                return FloatType as NavType<Any>
            } catch (e: IllegalArgumentException) {
                // ignored, proceed to check next type
            }
            try {
                BoolType.parseValue(value)
                return BoolType as NavType<Any>
            } catch (e: IllegalArgumentException) {
                // ignored, proceed to check next type
            }
            return StringType as NavType<Any>
        }

        /**
         * @param value nothing
         * @throws IllegalArgumentException not real
         * @suppress
         */
        @Suppress("UNCHECKED_CAST") // needed for cast to NavType<Any>
        @JvmStatic
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        public fun inferFromValueType(value: Any?): NavType<Any> {
            return when {
                value is Int -> IntType as NavType<Any>
                value is IntArray -> IntArrayType as NavType<Any>
                value is Long -> LongType as NavType<Any>
                value is LongArray -> LongArrayType as NavType<Any>
                value is Float -> FloatType as NavType<Any>
                value is FloatArray -> FloatArrayType as NavType<Any>
                value is Boolean -> BoolType as NavType<Any>
                value is BooleanArray -> BoolArrayType as NavType<Any>
                value is String || value == null -> StringType as NavType<Any>
                value is Array<*> && value.isArrayOf<String>() -> StringArrayType as NavType<Any>
                value.javaClass.isArray &&
                    Parcelable::class.java.isAssignableFrom(value.javaClass.componentType!!) -> {
                    ParcelableArrayType(
                        value.javaClass.componentType as Class<Parcelable>
                    ) as NavType<Any>
                }
                value.javaClass.isArray &&
                    Serializable::class.java.isAssignableFrom(value.javaClass.componentType!!) -> {
                    SerializableArrayType(
                        value.javaClass.componentType as Class<Serializable>
                    ) as NavType<Any>
                }
                value is Parcelable -> ParcelableType(value.javaClass) as NavType<Any>
                value is Enum<*> -> EnumType(value.javaClass) as NavType<Any>
                value is Serializable -> SerializableType(value.javaClass) as NavType<Any>
                else -> {
                    throw IllegalArgumentException(
                        "Object of type ${value.javaClass.name} is not supported for navigation " +
                            "arguments."
                    )
                }
            }
        }

        /**
         * NavType for storing integer values,
         * corresponding with the "integer" type in a Navigation XML file.
         *
         * Null values are not supported.
         */
        @JvmField
        public val IntType: NavType<Int> = object : NavType<Int>(false) {
            override val name: String
                get() = "integer"

            override fun put(bundle: Bundle, key: String, value: Int) {
                bundle.putInt(key, value)
            }

            override fun get(bundle: Bundle, key: String): Int {
                return bundle[key] as Int
            }

            override fun parseValue(value: String): Int {
                return if (value.startsWith("0x")) {
                    value.substring(2).toInt(16)
                } else {
                    value.toInt()
                }
            }
        }

        /**
         * NavType for storing integer values representing resource ids,
         * corresponding with the "reference" type in a Navigation XML file.
         *
         * Null values are not supported.
         */
        @JvmField
        public val ReferenceType: NavType<Int> = object : NavType<Int>(false) {
            override val name: String
                get() = "reference"

            override fun put(bundle: Bundle, key: String, @AnyRes value: Int) {
                bundle.putInt(key, value)
            }

            @AnyRes
            override fun get(bundle: Bundle, key: String): Int {
                return bundle[key] as Int
            }

            override fun parseValue(value: String): Int {
                return if (value.startsWith("0x")) {
                    value.substring(2).toInt(16)
                } else {
                    value.toInt()
                }
            }
        }

        /**
         * NavType for storing integer arrays,
         * corresponding with the "integer[]" type in a Navigation XML file.
         *
         * Null values are supported.
         * Default values in Navigation XML files are not supported.
         */
        @JvmField
        public val IntArrayType: NavType<IntArray?> = object : NavType<IntArray?>(true) {
            override val name: String
                get() = "integer[]"

            override fun put(bundle: Bundle, key: String, value: IntArray?) {
                bundle.putIntArray(key, value)
            }

            override fun get(bundle: Bundle, key: String): IntArray? {
                return bundle[key] as IntArray?
            }

            override fun parseValue(value: String): IntArray {
                throw UnsupportedOperationException("Arrays don't support default values.")
            }
        }

        /**
         * NavType for storing long values,
         * corresponding with the "long" type in a Navigation XML file.
         *
         * Null values are not supported.
         * Default values for this type in Navigation XML files must always end with an 'L' suffix, e.g.
         * `app:defaultValue="123L"`.
         */
        @JvmField
        public val LongType: NavType<Long> = object : NavType<Long>(false) {
            override val name: String
                get() = "long"

            override fun put(bundle: Bundle, key: String, value: Long) {
                bundle.putLong(key, value)
            }

            override fun get(bundle: Bundle, key: String): Long {
                return bundle[key] as Long
            }

            override fun parseValue(value: String): Long {
                // At runtime the L suffix is optional, contrary to the Safe Args plugin.
                // This is in order to be able to parse long numbers passed as deep link URL
                // parameters
                var localValue = value
                if (value.endsWith("L")) {
                    localValue = localValue.substring(0, value.length - 1)
                }
                return if (value.startsWith("0x")) {
                    localValue.substring(2).toLong(16)
                } else {
                    localValue.toLong()
                }
            }
        }

        /**
         * NavType for storing long arrays,
         * corresponding with the "long[]" type in a Navigation XML file.
         *
         * Null values are supported.
         * Default values in Navigation XML files are not supported.
         */
        @JvmField
        public val LongArrayType: NavType<LongArray?> = object : NavType<LongArray?>(true) {
            override val name: String
                get() = "long[]"

            override fun put(bundle: Bundle, key: String, value: LongArray?) {
                bundle.putLongArray(key, value)
            }

            override fun get(bundle: Bundle, key: String): LongArray? {
                return bundle[key] as LongArray?
            }

            override fun parseValue(value: String): LongArray {
                throw UnsupportedOperationException("Arrays don't support default values.")
            }
        }

        /**
         * NavType for storing float values,
         * corresponding with the "float" type in a Navigation XML file.
         *
         * Null values are not supported.
         */
        @JvmField
        public val FloatType: NavType<Float> = object : NavType<Float>(false) {
            override val name: String
                get() = "float"

            override fun put(bundle: Bundle, key: String, value: Float) {
                bundle.putFloat(key, value)
            }

            override fun get(bundle: Bundle, key: String): Float {
                return bundle[key] as Float
            }

            override fun parseValue(value: String): Float {
                return value.toFloat()
            }
        }

        /**
         * NavType for storing float arrays,
         * corresponding with the "float[]" type in a Navigation XML file.
         *
         * Null values are supported.
         * Default values in Navigation XML files are not supported.
         */
        @JvmField
        public val FloatArrayType: NavType<FloatArray?> = object : NavType<FloatArray?>(true) {
            override val name: String
                get() = "float[]"

            override fun put(bundle: Bundle, key: String, value: FloatArray?) {
                bundle.putFloatArray(key, value)
            }

            override fun get(bundle: Bundle, key: String): FloatArray? {
                return bundle[key] as FloatArray?
            }

            override fun parseValue(value: String): FloatArray {
                throw UnsupportedOperationException("Arrays don't support default values.")
            }
        }

        /**
         * NavType for storing boolean values,
         * corresponding with the "boolean" type in a Navigation XML file.
         *
         * Null values are not supported.
         */
        @JvmField
        public val BoolType: NavType<Boolean> = object : NavType<Boolean>(false) {
            override val name: String
                get() = "boolean"

            override fun put(bundle: Bundle, key: String, value: Boolean) {
                bundle.putBoolean(key, value)
            }

            override fun get(bundle: Bundle, key: String): Boolean? {
                return bundle[key] as Boolean?
            }

            override fun parseValue(value: String): Boolean {
                return when (value) {
                    "true" -> true
                    "false" -> false
                    else -> {
                        throw IllegalArgumentException(
                            "A boolean NavType only accepts \"true\" or \"false\" values."
                        )
                    }
                }
            }
        }

        /**
         * NavType for storing boolean arrays,
         * corresponding with the "boolean[]" type in a Navigation XML file.
         *
         * Null values are supported.
         * Default values in Navigation XML files are not supported.
         */
        @JvmField
        public val BoolArrayType: NavType<BooleanArray?> = object : NavType<BooleanArray?>(true) {
            override val name: String
                get() = "boolean[]"

            override fun put(bundle: Bundle, key: String, value: BooleanArray?) {
                bundle.putBooleanArray(key, value)
            }

            override fun get(bundle: Bundle, key: String): BooleanArray? {
                return bundle[key] as BooleanArray?
            }

            override fun parseValue(value: String): BooleanArray {
                throw UnsupportedOperationException("Arrays don't support default values.")
            }
        }

        /**
         * NavType for storing String values,
         * corresponding with the "string" type in a Navigation XML file.
         *
         * Null values are supported.
         */
        @JvmField
        public val StringType: NavType<String?> = object : NavType<String?>(true) {
            override val name: String
                get() = "string"

            override fun put(bundle: Bundle, key: String, value: String?) {
                bundle.putString(key, value)
            }

            override fun get(bundle: Bundle, key: String): String? {
                return bundle[key] as String?
            }

            override fun parseValue(value: String): String {
                return value
            }
        }

        /**
         * NavType for storing String arrays,
         * corresponding with the "string[]" type in a Navigation XML file.
         *
         * Null values are supported.
         * Default values in Navigation XML files are not supported.
         */
        @JvmField
        public val StringArrayType: NavType<Array<String>?> = object : NavType<Array<String>?>(
            true
        ) {
            override val name: String
                get() = "string[]"

            override fun put(bundle: Bundle, key: String, value: Array<String>?) {
                bundle.putStringArray(key, value)
            }

            @Suppress("UNCHECKED_CAST")
            override fun get(bundle: Bundle, key: String): Array<String>? {
                return bundle[key] as Array<String>?
            }

            override fun parseValue(value: String): Array<String>? {
                throw UnsupportedOperationException("Arrays don't support default values.")
            }
        }
    }

    /**
     * ParcelableType is used for passing Parcelables in [NavArgument]s.
     *
     * Null values are supported.
     * Default values in Navigation XML files are not supported.
     *
     * @param type the Parcelable class that is supported by this NavType
     */
    public class ParcelableType<D>(type: Class<D>) : NavType<D>(true) {
        private val type: Class<D>

        public override val name: String
            get() = type.name

        public override fun put(bundle: Bundle, key: String, value: D) {
            type.cast(value)
            if (value == null || value is Parcelable) {
                bundle.putParcelable(key, value as Parcelable?)
            } else if (value is Serializable) {
                bundle.putSerializable(key, value as Serializable?)
            }
        }

        @Suppress("UNCHECKED_CAST")
        public override fun get(bundle: Bundle, key: String): D? {
            return bundle[key] as D?
        }

        /**
         * @throws UnsupportedOperationException since Parcelables do not support default values
         */
        public override fun parseValue(value: String): D {
            throw UnsupportedOperationException("Parcelables don't support default values.")
        }

        public override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (other == null || javaClass != other.javaClass) return false
            val that = other as ParcelableType<*>
            return type == that.type
        }

        public override fun hashCode(): Int {
            return type.hashCode()
        }

        /**
         * Constructs a NavType that supports a given Parcelable type.
         */
        init {
            require(
                Parcelable::class.java.isAssignableFrom(type) ||
                    Serializable::class.java.isAssignableFrom(type)
            ) { "$type does not implement Parcelable or Serializable." }
            this.type = type
        }
    }

    /**
     * ParcelableArrayType is used for [NavArgument]s which hold arrays of Parcelables.
     *
     * Null values are supported.
     * Default values in Navigation XML files are not supported.
     *
     * @param type the type of Parcelable component class of the array
     */
    public class ParcelableArrayType<D : Parcelable>(type: Class<D>) : NavType<Array<D>?>(true) {
        private val arrayType: Class<Array<D>>

        public override val name: String
            get() = arrayType.name

        public override fun put(bundle: Bundle, key: String, value: Array<D>?) {
            arrayType.cast(value)
            bundle.putParcelableArray(key, value)
        }

        @Suppress("UNCHECKED_CAST")
        public override fun get(bundle: Bundle, key: String): Array<D>? {
            return bundle[key] as Array<D>?
        }

        /**
         * @throws UnsupportedOperationException since Arrays do not support default values
         */
        public override fun parseValue(value: String): Array<D> {
            throw UnsupportedOperationException("Arrays don't support default values.")
        }

        @Suppress("UNCHECKED_CAST")
        public override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (other == null || javaClass != other.javaClass) return false
            val that = other as ParcelableArrayType<Parcelable>
            return arrayType == that.arrayType
        }

        public override fun hashCode(): Int {
            return arrayType.hashCode()
        }

        /**
         * Constructs a NavType that supports arrays of a given Parcelable type.
         */
        init {
            require(Parcelable::class.java.isAssignableFrom(type)) {
                "$type does not implement Parcelable."
            }
            val arrayType: Class<Array<D>> = try {
                @Suppress("UNCHECKED_CAST")
                Class.forName("[L${type.name};") as Class<Array<D>>
            } catch (e: ClassNotFoundException) {
                throw RuntimeException(e) // should never happen
            }
            this.arrayType = arrayType
        }
    }

    /**
     * SerializableType is used for Serializable [NavArgument]s.
     * For handling Enums you must use [EnumType] instead.
     *
     * Null values are supported.
     * Default values in Navigation XML files are not supported.
     *
     * @see EnumType
     */
    public open class SerializableType<D : Serializable> : NavType<D> {
        private val type: Class<D>

        public override val name: String
            get() = type.name

        /**
         * Constructs a NavType that supports a given Serializable type.
         * @param type class that is a subtype of Serializable
         */
        public constructor(type: Class<D>) : super(true) {
            require(
                Serializable::class.java.isAssignableFrom(type)
            ) { "$type does not implement Serializable." }
            require(!type.isEnum) { "$type is an Enum. You should use EnumType instead." }
            this.type = type
        }

        internal constructor(nullableAllowed: Boolean, type: Class<D>) : super(nullableAllowed) {
            require(
                Serializable::class.java.isAssignableFrom(type)
            ) { "$type does not implement Serializable." }
            this.type = type
        }

        public override fun put(bundle: Bundle, key: String, value: D) {
            type.cast(value)
            bundle.putSerializable(key, value)
        }

        @Suppress("UNCHECKED_CAST")
        public override fun get(bundle: Bundle, key: String): D? {
            return bundle[key] as D?
        }

        /**
         * @throws UnsupportedOperationException since Serializables do not support default values
         */
        public override fun parseValue(value: String): D {
            throw UnsupportedOperationException("Serializables don't support default values.")
        }

        public override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (other !is SerializableType<*>) return false
            return type == other.type
        }

        public override fun hashCode(): Int {
            return type.hashCode()
        }
    }

    /**
     * EnumType is used for [NavArgument]s holding enum values.
     *
     * Null values are not supported.
     * To specify a default value in a Navigation XML file, simply use the enum constant
     * without the class name, e.g. `app:defaultValue="MONDAY"`.
     *
     * @param type the Enum class that is supported by this NavType
     */
    public class EnumType<D : Enum<*>>(
        type: Class<D>
    ) : SerializableType<D>(false, type) {
        private val type: Class<D>

        public override val name: String
            get() = type.name

        /**
         * Parse a value of this type from a String.
         *
         * @param value string representation of a value of this type
         * @return parsed value of the type represented by this NavType
         * @throws IllegalArgumentException if value cannot be parsed into this type
         */
        @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
        public override fun parseValue(value: String): D {
            return type.enumConstants.firstOrNull { constant ->
                constant.name.equals(value, ignoreCase = true)
            } ?: throw IllegalArgumentException(
                "Enum value $value not found for type ${type.name}."
            )
        }

        /**
         * Constructs a NavType that supports a given Enum type.
         */
        init {
            require(type.isEnum) { "$type is not an Enum type." }
            this.type = type
        }
    }

    /**
     * SerializableArrayType is used for [NavArgument]s that hold arrays of Serializables.
     * This type also supports arrays of Enums.
     *
     * Null values are supported.
     * Default values in Navigation XML files are not supported.
     *
     * @param type the Serializable component class of the array
     */
    public class SerializableArrayType<D : Serializable>(type: Class<D>) :
        NavType<Array<D>?>(true) {
        private val arrayType: Class<Array<D>>

        public override val name: String
            get() = arrayType.name

        public override fun put(bundle: Bundle, key: String, value: Array<D>?) {
            arrayType.cast(value)
            bundle.putSerializable(key, value)
        }

        @Suppress("UNCHECKED_CAST")
        public override fun get(bundle: Bundle, key: String): Array<D>? {
            return bundle[key] as Array<D>?
        }

        /**
         * @throws UnsupportedOperationException since Arrays do not support default values
         */
        public override fun parseValue(value: String): Array<D> {
            throw UnsupportedOperationException("Arrays don't support default values.")
        }

        public override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (other == null || javaClass != other.javaClass) return false
            val that = other as SerializableArrayType<*>
            return arrayType == that.arrayType
        }

        public override fun hashCode(): Int {
            return arrayType.hashCode()
        }

        /**
         * Constructs a NavType that supports arrays of a given Serializable type.
         */
        init {
            require(
                Serializable::class.java.isAssignableFrom(type)
            ) { "$type does not implement Serializable." }
            val arrayType: Class<Array<D>> = try {
                @Suppress("UNCHECKED_CAST")
                Class.forName("[L${type.name};") as Class<Array<D>>
            } catch (e: ClassNotFoundException) {
                throw RuntimeException(e) // should never happen
            }
            this.arrayType = arrayType
        }
    }
}