blob: 48618f9d816057a885286f3bb45a2e9b258bfcd3 [file] [log] [blame]
/*
* 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
}
}
}