blob: 1e6178a39d223dc6bbeca3b4c8481ac923924fc7 [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.car.navigation.utils;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
/**
* Class responsible for serializing and deserializing data into a {@link Bundle}. It also
* provides a way to detect what items in the {@link Bundle} have been modified during
* marshalling.
* <p>
* A single {@link BundleMarshaller} can be re-used to serialize or deserialize data multiple times.
* Similarity, deserialization can be done in-place, updating existing {@link Bundlable}s. This
* reduces the number of instances being allocated.
* <p>
* When serializing, use {@link #resetBundle()} before marshalling and {@link #getBundle()} to
* obtain an snap-shot of the serialized content. Or use {@link #resetDelta()} and
* {@link #getDelta()} to obtain a {@link Bundle} representing the patch between the last and
* the new serialized data.
* <p>
* When deserializing, use {@link #setBundle(Bundle)} to deserialize a {@link Bundle} containing an
* snap-shot, or {@link #applyDelta(Bundle)} to process a patch from the last deserialized data.
* <p>
* Keys used in the "get" and "put" methods must be lower camel case alphanumerical identifiers
* (e.g.: "distanceUnit"). Symbols like "." and "_" are reserved by the system.
* <p>
* When deserializing {@link List} objects, this class assumes that they implement random access
* (e.g. {@link ArrayList}), or they are relatively small (see more details at
* {@link #trimList(List, int)})
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public class BundleMarshaller {
/**
* Separator used to concatenate identifiers when marshalling non-primitive types (e.g. lists
* or {@link Bundlable}s).
*/
private static final String KEY_SEPARATOR = ".";
/**
* Identifier used to record if a given non-primitive field is null or not. This allows
* serializing null objects without the need of using reflection or static methods.
*/
private static final String IS_NULL_KEY = "_isNull";
/**
* Identifier used to record the length of a collection. This allows serializing changes to the
* length of a collection without having to remove elements or having to iterate over every
* possible collection key.
*/
private static final String SIZE_KEY = "_size";
/**
* Special value for {@link #SIZE_KEY} to serialize a null collection.
*/
private static final int NULL_SIZE = -1;
private Bundle mBundle = new Bundle();
private String mKeyPrefix = "";
private final Bundle mBundleDelta = new Bundle();
/**
* Returns data serialized since the last time this instance was constructed, or
* {@link #resetBundle()} was called.
*/
public Bundle getBundle() {
return mBundle;
}
/**
* Resets this {@link BundleMarshaller} causing {@link #getBundle()} to return an empty
* {@link Bundle} until the next marshalling is executed. This can be used occasionally to
* remove unused keys in the {@link Bundle}.
*/
public void resetBundle() {
mBundle.clear();
}
/**
* Replaces the {@link Bundle} to serialize into or deserialize from.
*/
public void setBundle(Bundle bundle) {
mBundle = bundle;
}
/**
* Gets a {@link Bundle} containing only the entries of {@link #getBundle()} that were modified
* since this instance was constructed, or {@link #resetDelta()} was called.
*/
public Bundle getDelta() {
return mBundleDelta;
}
/**
* Merges the provided {@link Bundle} on top of the one stored in this {@link BundleMarshaller}.
*
* @param delta a {@link Bundle} containing entries to be updated on one stored in this
* {@link BundleMarshaller} instance. Such {@link Bundle} can be produced by
* using the {@link #resetDelta()} and {@link #getDelta()} methods during data
* serialization.
*/
public void applyDelta(Bundle delta) {
mBundle.putAll(delta);
}
/**
* Resets tracking of modified entries, causing {@link #getDelta()} to return an empty
* {@link Bundle} until the next marshalling is executed. This can be used between
* serializations make {@link #getDelta()} return only the differences.
*/
public void resetDelta() {
mBundleDelta.clear();
}
/**
* Inserts an int value, replacing any existing value for the given key.
*
* @param key lower camel case alphanumerical identifier
* @param value an int
*/
public void putInt(@NonNull String key, int value) {
String mangledKey = getMangledKey(key);
if (!mBundle.containsKey(mangledKey) || mBundle.getInt(mangledKey) != value) {
mBundleDelta.putInt(mangledKey, value);
mBundle.putInt(mangledKey, value);
}
}
/**
* Returns the value associated with the given key, or 0 if no mapping of the desired type
* exists for the given key.
*
* @param key lower camel case alphanumerical identifier
* @return an int
*/
public int getInt(@NonNull String key) {
return mBundle.getInt(getMangledKey(key));
}
/**
* Inserts a float value, replacing any existing value for the given key.
*
* @param key lower camel case alphanumerical identifier
* @param value a float
*/
public void putFloat(@NonNull String key, float value) {
String mangledKey = getMangledKey(key);
if (!mBundle.containsKey(mangledKey)
|| Float.compare(mBundle.getFloat(mangledKey), value) != 0) {
mBundleDelta.putFloat(mangledKey, value);
mBundle.putFloat(mangledKey, value);
}
}
/**
* Returns the value associated with the given key, or 0.0f if no mapping of the desired type
* exists for the given key.
*
* @param key lower camel case alphanumerical identifier
* @return a float
*/
public float getFloat(@NonNull String key) {
return mBundle.getFloat(getMangledKey(key));
}
/**
* Inserts a double value, replacing any existing value for the given key.
*
* @param key lower camel case alphanumerical identifier
* @param value a double
*/
public void putDouble(@NonNull String key, double value) {
String mangledKey = getMangledKey(key);
if (!mBundle.containsKey(mangledKey)
|| Double.compare(mBundle.getDouble(mangledKey), value) != 0) {
mBundleDelta.putDouble(mangledKey, value);
mBundle.putDouble(mangledKey, value);
}
}
/**
* Returns the value associated with the given key, or 0.0 if no mapping of the desired type
* exists for the given key.
*
* @param key lower camel case alphanumerical identifier
* @return a double
*/
public double getDouble(@NonNull String key) {
return mBundle.getDouble(getMangledKey(key));
}
/**
* Inserts a boolean value, replacing any existing value for the given key.
*
* @param key lower camel case alphanumerical identifier
* @param value a boolean
*/
public void putBoolean(@NonNull String key, boolean value) {
String mangledKey = getMangledKey(key);
if (!mBundle.containsKey(mangledKey) || mBundle.getBoolean(mangledKey) != value) {
mBundleDelta.putBoolean(mangledKey, value);
mBundle.putBoolean(mangledKey, value);
}
}
/**
* Returns the value associated with the given key, or false if no mapping of the desired type
* exists for the given key.
*
* @param key lower camel case alphanumerical identifier
* @return a boolean
*/
public boolean getBoolean(@NonNull String key) {
return mBundle.getBoolean(getMangledKey(key));
}
/**
* Inserts a string value, replacing any existing value for the given key.
*
* @param key lower camel case alphanumerical identifier
* @param value a string, or null
*/
public void putString(@NonNull String key, @Nullable String value) {
String mangledKey = getMangledKey(key);
if (!mBundle.containsKey(mangledKey)
|| !Objects.equals(mBundle.getString(mangledKey), value)) {
mBundleDelta.putString(mangledKey, value);
mBundle.putString(mangledKey, value);
}
}
/**
* Returns the value associated with the given key, or null if no mapping of the desired type
* exists for the given key.
*
* @param key lower camel case alphanumerical identifier
* @return a string, or null
*/
@Nullable
public String getString(@NonNull String key) {
return mBundle.getString(getMangledKey(key));
}
/**
* Returns the value associated with the given key, or the provided default value if no mapping
* of the desired type exists for the given key.
*
* @param key lower camel case alphanumerical identifier
* @param defaultValue value to return if key does not exist or if a null value is associated
* with the given key.
* @return a string
*/
@NonNull
public String getStringNonNull(@NonNull String key, @NonNull String defaultValue) {
return mBundle.getString(getMangledKey(key), defaultValue);
}
/**
* Inserts an enum value, replacing any existing value for the given key. The provided enum
* will be serialized as a string using {@link Enum#name()}.
*
* @param key lower camel case alphanumerical identifier
* @param value an enum, or null
*/
public <T extends Enum<T>> void putEnum(@NonNull String key, @Nullable T value) {
putString(key, value != null ? value.name() : null);
}
/**
* Returns the value associated with the given key, or null if no mapping of the desired type
* exists for the given key.
*
* @param key lower camel case alphanumerical identifier
* @param clazz {@link Enum} class to be used to deserialize the value.
* @param <T> {@link Enum} type to be returned.
* @return an enum, or null
*/
@Nullable
public <T extends Enum<T>> T getEnum(@NonNull String key, @NonNull Class<T> clazz) {
String name = getString(key);
try {
return name != null ? Enum.valueOf(clazz, name) : null;
} catch (IllegalArgumentException ex) {
return null;
}
}
/**
* Returns the value associated with the given key, or the provided default value if no mapping
* of the desired type exists for the given key.
*
* @param key lower camel case alphanumerical identifier
* @param clazz {@link Enum} class to be used to deserialize the value.
* @param defaultValue value to return if key does not exist or if a null value is associated
* with the given key.
* @param <T> {@link Enum} type to be returned.
* @return an enum
*/
@NonNull
public <T extends Enum<T>> T getEnumNonNull(@NonNull String key, @NonNull Class<T> clazz,
@NonNull T defaultValue) {
T result = getEnum(key, clazz);
return result != null ? result : defaultValue;
}
/**
* Inserts a {@link Bundlable} value, replacing any existing value for the given key.
*
* @param key lower camel case alphanumerical identifier
* @param value a {@link Bundlable}, or null
*/
public <T extends Bundlable> void putBundlable(@NonNull String key, @Nullable T value) {
withKeyPrefix(key, () -> {
putBoolean(IS_NULL_KEY, value == null);
if (value != null) {
value.toBundle(this);
}
});
}
/**
* Returns the value associated with the given key, or null if no mapping of the desired type
* exists for the given key. If a non-null "current" instance is provided, then the
* deserialization would be done in place. Otherwise, a new instance will be created using the
* provided factory.
*
* @param key lower camel case alphanumerical identifier
* @param current current value (if available) to perform in-place deserialization, or null
* @param factory a {@link Supplier} capable of providing an instance of a {@link Bundlable} of
* type T. The suggested implementation is to pass a reference to the default
* constructor of that class.
* @param <T> {@link Bundlable} type to be returned.
* @return an instance of type T, or null
*/
@Nullable
public <T extends Bundlable> T getBundlable(@NonNull String key, @Nullable T current,
@NonNull Supplier<T> factory) {
return withKeyPrefix(key, () -> {
if (getBoolean(IS_NULL_KEY)) {
return null;
}
T result = current != null ? current : factory.get();
result.fromBundle(this);
return result;
});
}
/**
* Returns the value associated with the given key, or a default value if no mapping of the
* desired type exists for the given key. If a non-null value is available, then such value
* will be deserialized in-place on the given "current" instance. Otherwise, a default value
* will be generated using the provided factory.
*
* @param key lower camel case alphanumerical identifier
* @param current current value to perform in-place deserialization
* @param factory a {@link Supplier} capable of providing an instance of a {@link Bundlable} of
* type T. The suggested implementation is to pass a reference to the default
* constructor of that class.
* @param <T> {@link Bundlable} type to be returned.
* @return an instance of type T
*/
@NonNull
public <T extends Bundlable> T getBundlableNonNull(@NonNull String key, @NonNull T current,
@NonNull Supplier<T> factory) {
T result = getBundlable(key, current, factory);
return result != null ? result : factory.get();
}
/**
* Inserts a {@link List} of {@link Bundlable} values, replacing any existing value for the
* given key.
*
* @param key lower camel case alphanumerical identifier
* @param values a {@link List} of {@link Bundlable} values, or null
*/
public <T extends Bundlable> void putBundlableList(@NonNull String key,
@Nullable List<T> values) {
withKeyPrefix(key, () -> {
putInt(SIZE_KEY, values != null ? values.size() : NULL_SIZE);
if (values != null) {
int pos = 0;
// Using for-each as the provided list might not implement random access (e.g. it
// might be a linked list).
for (T value : values) {
putBundlable(String.valueOf(pos), value);
pos++;
}
}
});
}
/**
* Returns the value associated with the given key, or null if no mapping of the desired type
* exists for the given key. If a non-null "current" list is provided, then the deserialization
* would be done in place. Otherwise, a new list will be created and items will be instantiated
* using the provided factory.
*
* @param key lower camel case alphanumerical identifier
* @param current current value (if available) to perform in-place deserialization, or null
* @param factory a {@link Supplier} capable of providing an instance of a {@link Bundlable} of
* type T. The suggested implementation is to pass a reference to the default
* constructor of that class.
* @param <T> {@link Bundlable} type to be returned.
* @return a list of instances of type T, or null. The resulting list might contain null
* elements.
*/
@Nullable
public <T extends Bundlable> List<T> getBundlableList(@NonNull String key,
@Nullable List<T> current, @NonNull Supplier<T> factory) {
return withKeyPrefix(key, () -> {
int listSize = getInt(SIZE_KEY);
if (listSize == NULL_SIZE) {
return null;
}
List<T> result = current != null ? current : new ArrayList<>(listSize);
if (result.size() > listSize) {
result.subList(listSize, result.size()).clear();
}
for (int pos = 0; pos < listSize; pos++) {
String subKey = String.valueOf(pos);
if (pos < result.size()) {
result.set(pos, getBundlable(subKey, result.get(pos), factory));
} else {
result.add(getBundlable(String.valueOf(pos),
null /* force the creation of a new instance */,
factory));
}
}
return result;
});
}
/**
* Returns the value associated with the given key, or an empty list if no mapping of the
* desired type exists for the given key. If a non-null "current" list is provided, then the
* deserialization would be done in place. Otherwise, a new list will be created and items will
* be instantiated using the provided factory.
*
* @param key lower camel case alphanumerical identifier
* @param current current value (if available) to perform in-place deserialization, or null
* @param factory a {@link Supplier} capable of providing an instance of a {@link Bundlable} of
* type T. The suggested implementation is to pass a reference to the default
* constructor of that class.
* @param <T> {@link Bundlable} type to be returned.
* @return a list of instances of type T, or an empty list. The resulting list might contain
* null elements.
*/
@NonNull
public <T extends Bundlable> List<T> getBundlableListNonNull(@NonNull String key,
@NonNull List<T> current, @NonNull Supplier<T> factory) {
List<T> result = getBundlableList(key, current, factory);
return result != null ? result : new ArrayList<>();
}
/**
* Executes the given {@link Runnable} in a context where {@link #getMangledKey(String)}
* includes the given key as part of the prefix. Calls to this method can be nested (the
* provided {@link Runnable} can call to this method if needed). This method should be used when
* serializing or deserializing nested objects.
* <p>
* For example: calling to {@link #withKeyPrefix(String, Runnable)} with "foo" as key and
* a {@link Runnable} that calls {@link #getMangledKey(String)} with "bar" as key, will
* cause such {@link #getMangledKey(String)} call to return "foo.bar".
*/
private void withKeyPrefix(@NonNull String key, @NonNull Runnable runnable) {
String originalKeyPrefix = mKeyPrefix;
mKeyPrefix = mKeyPrefix + key + KEY_SEPARATOR;
runnable.run();
mKeyPrefix = originalKeyPrefix;
}
/**
* Similar to {@link #withKeyPrefix(String, Runnable)} but allows returning a value.
*/
private <X> X withKeyPrefix(@NonNull String key, @NonNull Supplier<X> supplier) {
String originalKeyPrefix = mKeyPrefix;
mKeyPrefix = mKeyPrefix + key + KEY_SEPARATOR;
X res = supplier.get();
mKeyPrefix = originalKeyPrefix;
return res;
}
/**
* Returns a composed key based on the given one and the current serialization/deserialization
* key prefix (initially empty). This prefix can be temporarily changed with
* {@link #withKeyPrefix(String, Runnable)} or {@link #withKeyPrefix(String, Supplier)}.
*/
private String getMangledKey(@NonNull String key) {
return mKeyPrefix + key;
}
}