/*
* 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.NonNull;
import androidx.annotation.Nullable;
import java.io.Serializable;
import java.text.ParseException;
/**
* NavType denotes the type that can be used in a {@link NavArgument}.
* <p>
* 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.
* <p>
* 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> {
private final boolean mNullableAllowed;
NavType(boolean nullableAllowed) {
this.mNullableAllowed = nullableAllowed;
}
/**
* Check if an argument with this type can hold a null value.
* @return Returns true if this type allows null values, false otherwise.
*/
public boolean isNullableAllowed() {
return mNullableAllowed;
}
/**
* Put a value of this type in he {@code bundle}
*
* @param bundle bundle to put value in
* @param key bundle key
* @param value value of this type
*/
public abstract void put(@NonNull Bundle bundle, @NonNull String key, @Nullable T value);
/**
* Get a value of this type from the {@code bundle}
*
* @param bundle bundle to get value from
* @param key bundle key
* @return value of this type
*/
@Nullable
public abstract T get(@NonNull Bundle bundle, @NonNull String key);
/**
* 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
*/
@NonNull
public abstract T parseValue(@NonNull String value);
/**
* Parse a value of this type from a String and put it in a {@code 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
* @throws ParseException if value cannot be parsed into this type
*/
@NonNull
T parseAndPut(@NonNull Bundle bundle, @NonNull String key, @NonNull String value) {
T parsedValue = parseValue(value);
put(bundle, key, parsedValue);
return parsedValue;
}
/**
* Returns the name of this type.
* <p>
* This is the same value that is used in Navigation XML <code>argType</code> attribute.
*
* @return name of this type
*/
@NonNull
public abstract String getName();
@Override
@NonNull
public String toString() {
return getName();
}
/**
* 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.
*/
@SuppressWarnings("unchecked")
@NonNull
public static NavType<?> fromArgType(@Nullable String type, @Nullable String packageName) {
if (IntType.getName().equals(type)) {
return IntType;
} else if (IntArrayType.getName().equals(type)) {
return IntArrayType;
} else if (LongType.getName().equals(type)) {
return LongType;
} else if (LongArrayType.getName().equals(type)) {
return LongArrayType;
} else if (BoolType.getName().equals(type)) {
return BoolType;
} else if (BoolArrayType.getName().equals(type)) {
return BoolArrayType;
} else if (StringType.getName().equals(type)) {
return StringType;
} else if (StringArrayType.getName().equals(type)) {
return StringArrayType;
} else if (FloatType.getName().equals(type)) {
return FloatType;
} else if (FloatArrayType.getName().equals(type)) {
return FloatArrayType;
} else if (ReferenceType.getName().equals(type)) {
return ReferenceType;
} else if (type != null && !type.isEmpty()) {
try {
String className;
if (type.startsWith(".") && packageName != null) {
className = packageName + type;
} else {
className = type;
}
if (type.endsWith("[]")) {
className = className.substring(0, className.length() - 2);
Class<?> clazz = Class.forName(className);
if (Parcelable.class.isAssignableFrom(clazz)) {
return new ParcelableArrayType(clazz);
} else if (Serializable.class.isAssignableFrom(clazz)) {
return new SerializableArrayType(clazz);
}
} else {
Class<?> clazz = Class.forName(className);
if (Parcelable.class.isAssignableFrom(clazz)) {
return new ParcelableType(clazz);
} else if (Enum.class.isAssignableFrom(clazz)) {
return new EnumType(clazz);
} else if (Serializable.class.isAssignableFrom(clazz)) {
return new SerializableType(clazz);
}
}
throw new IllegalArgumentException(className + " is not Serializable or "
+ "Parcelable.");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
return StringType;
}
@NonNull
static NavType inferFromValue(@NonNull String value) {
//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;
} catch (IllegalArgumentException e) {
//ignored, proceed to check next type
}
try {
LongType.parseValue(value);
return LongType;
} catch (IllegalArgumentException e) {
//ignored, proceed to check next type
}
try {
FloatType.parseValue(value);
return FloatType;
} catch (IllegalArgumentException e) {
//ignored, proceed to check next type
}
try {
BoolType.parseValue(value);
return BoolType;
} catch (IllegalArgumentException e) {
//ignored, proceed to check next type
}
return StringType;
}
@SuppressWarnings("unchecked")
@NonNull
static NavType inferFromValueType(@Nullable Object value) {
if (value instanceof Integer) {
return IntType;
} else if (value instanceof int[]) {
return IntArrayType;
} else if (value instanceof Long) {
return LongType;
} else if (value instanceof long[]) {
return LongArrayType;
} else if (value instanceof Float) {
return FloatType;
} else if (value instanceof float[]) {
return FloatArrayType;
} else if (value instanceof Boolean) {
return BoolType;
} else if (value instanceof boolean[]) {
return BoolArrayType;
} else if (value instanceof String || value == null) {
return StringType;
} else if (value instanceof String[]) {
return StringArrayType;
} else if (value.getClass().isArray() && (
Parcelable.class.isAssignableFrom(value.getClass().getComponentType()))) {
return new ParcelableArrayType(value.getClass().getComponentType());
} else if (value.getClass().isArray() && (
Serializable.class.isAssignableFrom(value.getClass().getComponentType()))) {
return new SerializableArrayType(value.getClass().getComponentType());
} else if (value instanceof Parcelable) {
return new ParcelableType(value.getClass());
} else if (value instanceof Enum) {
return new EnumType(value.getClass());
} else if (value instanceof Serializable) {
return new SerializableType(value.getClass());
} else {
throw new IllegalArgumentException("Object of type " + value.getClass().getName()
+ " is not supported for navigation arguments.");
}
}
/**
* NavType for storing integer values,
* corresponding with the "integer" type in a Navigation XML file.
* <p>
* Null values are not supported.
*/
@NonNull
public static final NavType<Integer> IntType = new NavType<Integer>(false) {
@Override
public void put(@NonNull Bundle bundle, @NonNull String key, @NonNull Integer value) {
bundle.putInt(key, value);
}
@Override
public Integer get(@NonNull Bundle bundle, @NonNull String key) {
return (Integer) bundle.get(key);
}
@NonNull
@Override
public Integer parseValue(@NonNull String value) {
if (value.startsWith("0x")) {
return Integer.parseInt(value.substring(2), 16);
} else {
return Integer.parseInt(value);
}
}
@NonNull
@Override
public String getName() {
return "integer";
}
};
/**
* NavType for storing integer values representing resource ids,
* corresponding with the "reference" type in a Navigation XML file.
* <p>
* Null values are not supported.
*/
@NonNull
public static final NavType<Integer> ReferenceType = new NavType<Integer>(false) {
@Override
public void put(@NonNull Bundle bundle, @NonNull String key,
@NonNull @AnyRes Integer value) {
bundle.putInt(key, value);
}
@AnyRes
@Override
public Integer get(@NonNull Bundle bundle, @NonNull String key) {
return (Integer) bundle.get(key);
}
@NonNull
@Override
public Integer parseValue(@NonNull String value) {
throw new UnsupportedOperationException(
"References don't support parsing string values.");
}
@NonNull
@Override
public String getName() {
return "reference";
}
};
/**
* NavType for storing integer arrays,
* corresponding with the "integer[]" type in a Navigation XML file.
* <p>
* Null values are supported.
* Default values in Navigation XML files are not supported.
*/
@NonNull
public static final NavType<int[]> IntArrayType = new NavType<int[]>(true) {
@Override
public void put(@NonNull Bundle bundle, @NonNull String key, @Nullable int[] value) {
bundle.putIntArray(key, value);
}
@Override
public int[] get(@NonNull Bundle bundle, @NonNull String key) {
return (int[]) bundle.get(key);
}
@NonNull
@Override
public int[] parseValue(@NonNull String value) {
throw new UnsupportedOperationException("Arrays don't support default values.");
}
@NonNull
@Override
public String getName() {
return "integer[]";
}
};
/**
* NavType for storing long values,
* corresponding with the "long" type in a Navigation XML file.
* <p>
* 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"`.
*/
@NonNull
public static final NavType<Long> LongType = new NavType<Long>(false) {
@Override
public void put(@NonNull Bundle bundle, @NonNull String key, @NonNull Long value) {
bundle.putLong(key, value);
}
@Override
public Long get(@NonNull Bundle bundle, @NonNull String key) {
return (Long) bundle.get(key);
}
@NonNull
public Long parseValue(@NonNull String value) {
//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
if (value.endsWith("L")) {
value = value.substring(0, value.length() - 1);
}
if (value.startsWith("0x")) {
return Long.parseLong(value.substring(2), 16);
} else {
return Long.parseLong(value);
}
}
@NonNull
@Override
public String getName() {
return "long";
}
};
/**
* NavType for storing long arrays,
* corresponding with the "long[]" type in a Navigation XML file.
* <p>
* Null values are supported.
* Default values in Navigation XML files are not supported.
*/
@NonNull
public static final NavType<long[]> LongArrayType = new NavType<long[]>(true) {
@Override
public void put(@NonNull Bundle bundle, @NonNull String key, @Nullable long[] value) {
bundle.putLongArray(key, value);
}
@Override
public long[] get(@NonNull Bundle bundle, @NonNull String key) {
return (long[]) bundle.get(key);
}
@NonNull
@Override
public long[] parseValue(@NonNull String value) {
throw new UnsupportedOperationException("Arrays don't support default values.");
}
@NonNull
@Override
public String getName() {
return "long[]";
}
};
/**
* NavType for storing float values,
* corresponding with the "float" type in a Navigation XML file.
* <p>
* Null values are not supported.
*/
@NonNull
public static final NavType<Float> FloatType = new NavType<Float>(false) {
@Override
public void put(@NonNull Bundle bundle, @NonNull String key, @NonNull Float value) {
bundle.putFloat(key, value);
}
@Override
public Float get(@NonNull Bundle bundle, @NonNull String key) {
return (Float) bundle.get(key);
}
@NonNull
@Override
public Float parseValue(@NonNull String value) {
return Float.parseFloat(value);
}
@NonNull
@Override
public String getName() {
return "float";
}
};
/**
* NavType for storing float arrays,
* corresponding with the "float[]" type in a Navigation XML file.
* <p>
* Null values are supported.
* Default values in Navigation XML files are not supported.
*/
@NonNull
public static final NavType<float[]> FloatArrayType = new NavType<float[]>(true) {
@Override
public void put(@NonNull Bundle bundle, @NonNull String key, @Nullable float[] value) {
bundle.putFloatArray(key, value);
}
@Override
public float[] get(@NonNull Bundle bundle, @NonNull String key) {
return (float[]) bundle.get(key);
}
@NonNull
@Override
public float[] parseValue(@NonNull String value) {
throw new UnsupportedOperationException("Arrays don't support default values.");
}
@NonNull
@Override
public String getName() {
return "float[]";
}
};
/**
* NavType for storing boolean values,
* corresponding with the "boolean" type in a Navigation XML file.
* <p>
* Null values are not supported.
*/
@NonNull
public static final NavType<Boolean> BoolType = new NavType<Boolean>(false) {
@Override
public void put(@NonNull Bundle bundle, @NonNull String key, @NonNull Boolean value) {
bundle.putBoolean(key, value);
}
@Override
public Boolean get(@NonNull Bundle bundle, @NonNull String key) {
return (Boolean) bundle.get(key);
}
@NonNull
@Override
public Boolean parseValue(@NonNull String value) {
if ("true".equals(value)) {
return true;
} else if ("false".equals(value)) {
return false;
} else {
throw new IllegalArgumentException(
"A boolean NavType only accepts \"true\" or \"false\" values.");
}
}
@NonNull
@Override
public String getName() {
return "boolean";
}
};
/**
* NavType for storing boolean arrays,
* corresponding with the "boolean[]" type in a Navigation XML file.
* <p>
* Null values are supported.
* Default values in Navigation XML files are not supported.
*/
@NonNull
public static final NavType<boolean[]> BoolArrayType = new NavType<boolean[]>(true) {
@Override
public void put(@NonNull Bundle bundle, @NonNull String key, @Nullable boolean[] value) {
bundle.putBooleanArray(key, value);
}
@Override
public boolean[] get(@NonNull Bundle bundle, @NonNull String key) {
return (boolean[]) bundle.get(key);
}
@NonNull
@Override
public boolean[] parseValue(@NonNull String value) {
throw new UnsupportedOperationException("Arrays don't support default values.");
}
@NonNull
@Override
public String getName() {
return "boolean[]";
}
};
/**
* NavType for storing String values,
* corresponding with the "string" type in a Navigation XML file.
* <p>
* Null values are supported.
*/
@NonNull
public static final NavType<String> StringType = new NavType<String>(true) {
@Override
public void put(@NonNull Bundle bundle, @NonNull String key, @Nullable String value) {
bundle.putString(key, value);
}
@Override
public String get(@NonNull Bundle bundle, @NonNull String key) {
return (String) bundle.get(key);
}
@NonNull
@Override
public String parseValue(@NonNull String value) {
return value;
}
@NonNull
@Override
public String getName() {
return "string";
}
};
/**
* NavType for storing String arrays,
* corresponding with the "string[]" type in a Navigation XML file.
* <p>
* Null values are supported.
* Default values in Navigation XML files are not supported.
*/
@NonNull
public static final NavType<String[]> StringArrayType = new NavType<String[]>(true) {
@Override
public void put(@NonNull Bundle bundle, @NonNull String key, @Nullable String[] value) {
bundle.putStringArray(key, value);
}
@Override
public String[] get(@NonNull Bundle bundle, @NonNull String key) {
return (String[]) bundle.get(key);
}
@NonNull
@Override
public String[] parseValue(@NonNull String value) {
throw new UnsupportedOperationException("Arrays don't support default values.");
}
@NonNull
@Override
public String getName() {
return "string[]";
}
};
/**
* ParcelableType is used for passing Parcelables in {@link NavArgument}s.
* <p>
* Null values are supported.
* Default values in Navigation XML files are not supported.
*
* @param <D> the Parcelable class that is supported by this NavType
*/
public static final class ParcelableType<D> extends NavType<D> {
@NonNull
private final Class<D> mType;
/**
* Constructs a NavType that supports a given Parcelable type.
* @param type class that is a subtype of Parcelable
*/
public ParcelableType(@NonNull Class<D> type) {
super(true);
if (!Parcelable.class.isAssignableFrom(type)
&& !Serializable.class.isAssignableFrom(type)) {
throw new IllegalArgumentException(
type + " does not implement Parcelable or Serializable.");
}
this.mType = type;
}
@Override
public void put(@NonNull Bundle bundle, @NonNull String key, @Nullable D value) {
mType.cast(value);
if (value == null || value instanceof Parcelable) {
bundle.putParcelable(key, (Parcelable) value);
} else if (value instanceof Serializable) {
bundle.putSerializable(key, (Serializable) value);
}
}
@SuppressWarnings("unchecked")
@Override
@Nullable
public D get(@NonNull Bundle bundle, @NonNull String key) {
return (D) bundle.get(key);
}
@NonNull
@Override
public D parseValue(@NonNull String value) {
throw new UnsupportedOperationException("Parcelables don't support default values.");
}
@Override
@NonNull
public String getName() {
return mType.getName();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ParcelableType<?> that = (ParcelableType<?>) o;
return mType.equals(that.mType);
}
@Override
public int hashCode() {
return mType.hashCode();
}
}
/**
* ParcelableArrayType is used for {@link NavArgument}s which hold arrays of Parcelables.
* <p>
* Null values are supported.
* Default values in Navigation XML files are not supported.
*
* @param <D> the type of Parcelable component class of the array
*/
public static final class ParcelableArrayType<D extends Parcelable> extends NavType<D[]> {
@NonNull
private final Class<D[]> mArrayType;
/**
* Constructs a NavType that supports arrays of a given Parcelable type.
* @param type class that is a subtype of Parcelable
*/
@SuppressWarnings("unchecked")
public ParcelableArrayType(@NonNull Class<D> type) {
super(true);
if (!Parcelable.class.isAssignableFrom(type)) {
throw new IllegalArgumentException(
type + " does not implement Parcelable.");
}
Class<D[]> arrayType;
try {
arrayType = (Class<D[]>) Class.forName("[L" + type.getName() + ";");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e); //should never happen
}
this.mArrayType = arrayType;
}
@Override
public void put(@NonNull Bundle bundle, @NonNull String key, @Nullable D[] value) {
mArrayType.cast(value);
bundle.putParcelableArray(key, value);
}
@SuppressWarnings("unchecked")
@Override
@Nullable
public D[] get(@NonNull Bundle bundle, @NonNull String key) {
return (D[]) bundle.get(key);
}
@NonNull
@Override
public D[] parseValue(@NonNull String value) {
throw new UnsupportedOperationException("Arrays don't support default values.");
}
@Override
@NonNull
public String getName() {
return mArrayType.getName();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ParcelableArrayType<?> that = (ParcelableArrayType<?>) o;
return mArrayType.equals(that.mArrayType);
}
@Override
public int hashCode() {
return mArrayType.hashCode();
}
}
/**
* SerializableType is used for Serializable {@link NavArgument}s.
* For handling Enums you must use {@link EnumType} instead.
* <p>
* Null values are supported.
* Default values in Navigation XML files are not supported.
*
* @param <D> the Serializable class that is supported by this NavType
* @see EnumType
*/
public static class SerializableType<D extends Serializable> extends NavType<D> {
@NonNull
private final Class<D> mType;
/**
* Constructs a NavType that supports a given Serializable type.
* @param type class that is a subtype of Serializable
*/
public SerializableType(@NonNull Class<D> type) {
super(true);
if (!Serializable.class.isAssignableFrom(type)) {
throw new IllegalArgumentException(
type + " does not implement Serializable.");
}
if (type.isEnum()) {
throw new IllegalArgumentException(
type + " is an Enum. You should use EnumType instead.");
}
this.mType = type;
}
SerializableType(boolean nullableAllowed, @NonNull Class<D> type) {
super(nullableAllowed);
if (!Serializable.class.isAssignableFrom(type)) {
throw new IllegalArgumentException(
type + " does not implement Serializable.");
}
this.mType = type;
}
@Override
public void put(@NonNull Bundle bundle, @NonNull String key, @Nullable D value) {
mType.cast(value);
bundle.putSerializable(key, value);
}
@SuppressWarnings("unchecked")
@Override
@Nullable
public D get(@NonNull Bundle bundle, @NonNull String key) {
return (D) bundle.get(key);
}
@NonNull
@Override
public D parseValue(@NonNull String value) {
throw new UnsupportedOperationException("Serializables don't support default values.");
}
@Override
@NonNull
public String getName() {
return mType.getName();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SerializableType<?> that = (SerializableType<?>) o;
return mType.equals(that.mType);
}
@Override
public int hashCode() {
return mType.hashCode();
}
}
/**
* EnumType is used for {@link NavArgument}s holding enum values.
* <p>
* 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 <D> the Enum class that is supported by this NavType
*/
public static final class EnumType<D extends Enum> extends SerializableType<D> {
@NonNull
private final Class<D> mType;
/**
* Constructs a NavType that supports a given Enum type.
* @param type class that is an Enum
*/
public EnumType(@NonNull Class<D> type) {
super(false, type);
if (!type.isEnum()) {
throw new IllegalArgumentException(
type + " is not an Enum type.");
}
mType = type;
}
@SuppressWarnings("unchecked")
@NonNull
@Override
public D parseValue(@NonNull String value) {
for (Object constant : mType.getEnumConstants()) {
if (((Enum) constant).name().equals(value)) {
return (D) constant;
}
}
throw new IllegalArgumentException("Enum value " + value + " not found for type "
+ mType.getName() + ".");
}
@Override
@NonNull
public String getName() {
return mType.getName();
}
}
/**
* SerializableArrayType is used for {@link NavArgument}s that hold arrays of Serializables.
* This type also supports arrays of Enums.
* <p>
* Null values are supported.
* Default values in Navigation XML files are not supported.
*
* @param <D> the Serializable component class of the array
*/
public static final class SerializableArrayType<D extends Serializable> extends NavType<D[]> {
@NonNull
private final Class<D[]> mArrayType;
/**
* Constructs a NavType that supports arrays of a given Serializable type.
* @param type class that is a subtype of Serializable
*/
@SuppressWarnings("unchecked")
public SerializableArrayType(@NonNull Class<D> type) {
super(true);
if (!Serializable.class.isAssignableFrom(type)) {
throw new IllegalArgumentException(
type + " does not implement Serializable.");
}
Class<D[]> arrayType;
try {
arrayType = (Class<D[]>) Class.forName("[L" + type.getName() + ";");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e); //should never happen
}
this.mArrayType = arrayType;
}
@Override
public void put(@NonNull Bundle bundle, @NonNull String key, @Nullable D[] value) {
mArrayType.cast(value);
bundle.putSerializable(key, value);
}
@SuppressWarnings("unchecked")
@Override
@Nullable
public D[] get(@NonNull Bundle bundle, @NonNull String key) {
return (D[]) bundle.get(key);
}
@NonNull
@Override
public D[] parseValue(@NonNull String value) {
throw new UnsupportedOperationException("Arrays don't support default values.");
}
@Override
@NonNull
public String getName() {
return mArrayType.getName();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SerializableArrayType<?> that = (SerializableArrayType<?>) o;
return mArrayType.equals(that.mArrayType);
}
@Override
public int hashCode() {
return mArrayType.hashCode();
}
}
}