blob: 578e83e21b79207e9d138f9b6078179a6854c3c6 [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.work;
import android.annotation.SuppressLint;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.room.TypeConverter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* A persistable set of key/value pairs which are used as inputs and outputs for
* {@link ListenableWorker}s. Keys are Strings, and values can be Strings, primitive types, or
* their array variants.
* <p>
* This is a lightweight container, and should not be considered your data store. As such, there is
* an enforced {@link #MAX_DATA_BYTES} limit on the serialized (byte array) size of the payloads.
* This class will throw {@link IllegalStateException}s if you try to serialize or deserialize past
* this limit.
*/
public final class Data {
private static final String TAG = Logger.tagWithPrefix("Data");
/**
* An empty Data object with no elements.
*/
public static final Data EMPTY = new Data.Builder().build();
/**
* The maximum number of bytes for Data when it is serialized (converted to a byte array).
* Please see the class-level Javadoc for more information.
*/
@SuppressLint("MinMaxConstant")
public static final int MAX_DATA_BYTES = 10 * 1024; // 10KB
@SuppressWarnings("WeakerAccess") /* synthetic access */
Map<String, Object> mValues;
Data() { // stub required for room
}
public Data(@NonNull Data other) {
mValues = new HashMap<>(other.mValues);
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public Data(@NonNull Map<String, ?> values) {
mValues = new HashMap<>(values);
}
/**
* Gets the boolean value for the given key.
*
* @param key The key for the argument
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
public boolean getBoolean(@NonNull String key, boolean defaultValue) {
Object value = mValues.get(key);
if (value instanceof Boolean) {
return (boolean) value;
} else {
return defaultValue;
}
}
/**
* Gets the boolean array value for the given key.
*
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
@Nullable
public boolean[] getBooleanArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Boolean[]) {
Boolean[] array = (Boolean[]) value;
return convertToPrimitiveArray(array);
} else {
return null;
}
}
/**
* Gets the byte value for the given key.
*
* @param key The key for the argument
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
public byte getByte(@NonNull String key, byte defaultValue) {
Object value = mValues.get(key);
if (value instanceof Byte) {
return (byte) value;
} else {
return defaultValue;
}
}
/**
* Gets the byte array value for the given key.
*
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
@Nullable
public byte[] getByteArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Byte[]) {
Byte[] array = (Byte[]) value;
return convertToPrimitiveArray(array);
} else {
return null;
}
}
/**
* Gets the integer value for the given key.
*
* @param key The key for the argument
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
public int getInt(@NonNull String key, int defaultValue) {
Object value = mValues.get(key);
if (value instanceof Integer) {
return (int) value;
} else {
return defaultValue;
}
}
/**
* Gets the integer array value for the given key.
*
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
@Nullable
public int[] getIntArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Integer[]) {
Integer[] array = (Integer[]) value;
return convertToPrimitiveArray(array);
} else {
return null;
}
}
/**
* Gets the long value for the given key.
*
* @param key The key for the argument
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
public long getLong(@NonNull String key, long defaultValue) {
Object value = mValues.get(key);
if (value instanceof Long) {
return (long) value;
} else {
return defaultValue;
}
}
/**
* Gets the long array value for the given key.
*
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
@Nullable
public long[] getLongArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Long[]) {
Long[] array = (Long[]) value;
return convertToPrimitiveArray(array);
} else {
return null;
}
}
/**
* Gets the float value for the given key.
*
* @param key The key for the argument
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
public float getFloat(@NonNull String key, float defaultValue) {
Object value = mValues.get(key);
if (value instanceof Float) {
return (float) value;
} else {
return defaultValue;
}
}
/**
* Gets the float array value for the given key.
*
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
@Nullable
public float[] getFloatArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Float[]) {
Float[] array = (Float[]) value;
return convertToPrimitiveArray(array);
} else {
return null;
}
}
/**
* Gets the double value for the given key.
*
* @param key The key for the argument
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
public double getDouble(@NonNull String key, double defaultValue) {
Object value = mValues.get(key);
if (value instanceof Double) {
return (double) value;
} else {
return defaultValue;
}
}
/**
* Gets the double array value for the given key.
*
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
@Nullable
public double[] getDoubleArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Double[]) {
Double[] array = (Double[]) value;
return convertToPrimitiveArray(array);
} else {
return null;
}
}
/**
* Gets the String value for the given key.
*
* @param key The key for the argument
* @return The value specified by the key if it exists; the default value otherwise
*/
@Nullable
public String getString(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof String) {
return (String) value;
} else {
return null;
}
}
/**
* Gets the String array value for the given key.
*
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
@Nullable
public String[] getStringArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof String[]) {
return (String[]) value;
} else {
return null;
}
}
/**
* Gets all the values in this Data object.
*
* @return A {@link Map} of key-value pairs for this object; this Map is unmodifiable and should
* be used for reads only.
*/
@NonNull
public Map<String, Object> getKeyValueMap() {
return Collections.unmodifiableMap(mValues);
}
/**
* Converts this Data to a byte array suitable for sending to other processes in your
* application. There are no versioning guarantees with this byte array, so you should not
* use this for IPCs between applications or persistence.
*
* @return The byte array representation of the input
* @throws IllegalStateException if the serialized payload is bigger than
* {@link #MAX_DATA_BYTES}
*/
@NonNull
public byte[] toByteArray() {
return Data.toByteArrayInternal(this);
}
/**
* Returns {@code true} if the instance of {@link Data} has a non-null value corresponding to
* the given {@link String} key with the expected type of {@code T}.
*
* @param key The {@link String} key
* @param klass The {@link Class} container for the expected type
* @param <T> The expected type
* @return {@code true} If the instance of {@link Data} has a value for the given
* {@link String} key with the expected type.
*/
public <T> boolean hasKeyWithValueOfType(@NonNull String key, @NonNull Class<T> klass) {
Object value = mValues.get(key);
return value != null && klass.isAssignableFrom(value.getClass());
}
/**
* @return The number of elements in this Data object.
* @hide
*/
@VisibleForTesting
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public int size() {
return mValues.size();
}
/**
* Converts {@link Data} to a byte array for persistent storage.
*
* @param data The {@link Data} object to convert
* @return The byte array representation of the input
* @throws IllegalStateException if the serialized payload is bigger than
* {@link #MAX_DATA_BYTES}
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@TypeConverter
@NonNull
public static byte[] toByteArrayInternal(@NonNull Data data) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = null;
try {
objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeInt(data.size());
for (Map.Entry<String, Object> entry : data.mValues.entrySet()) {
objectOutputStream.writeUTF(entry.getKey());
objectOutputStream.writeObject(entry.getValue());
}
} catch (IOException e) {
Log.e(TAG, "Error in Data#toByteArray: ", e);
return outputStream.toByteArray();
} finally {
if (objectOutputStream != null) {
try {
// NOTE: this writes something to the output stream for bookkeeping purposes.
// Don't get the byteArray before we do this!
objectOutputStream.close();
} catch (IOException e) {
Log.e(TAG, "Error in Data#toByteArray: ", e);
}
}
try {
outputStream.close();
} catch (IOException e) {
Log.e(TAG, "Error in Data#toByteArray: ", e);
}
}
if (outputStream.size() > MAX_DATA_BYTES) {
throw new IllegalStateException(
"Data cannot occupy more than " + MAX_DATA_BYTES
+ " bytes when serialized");
}
return outputStream.toByteArray();
}
/**
* Converts a byte array to {@link Data}.
*
* @param bytes The byte array representation to convert
* @return An {@link Data} object built from the input
* @throws IllegalStateException if bytes is bigger than {@link #MAX_DATA_BYTES}
*/
@TypeConverter
@NonNull
public static Data fromByteArray(@NonNull byte[] bytes) {
if (bytes.length > MAX_DATA_BYTES) {
throw new IllegalStateException(
"Data cannot occupy more than " + MAX_DATA_BYTES + " bytes when serialized");
}
Map<String, Object> map = new HashMap<>();
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = null;
try {
objectInputStream = new ObjectInputStream(inputStream);
for (int i = objectInputStream.readInt(); i > 0; i--) {
map.put(objectInputStream.readUTF(), objectInputStream.readObject());
}
} catch (IOException | ClassNotFoundException e) {
Log.e(TAG, "Error in Data#fromByteArray: ", e);
} finally {
if (objectInputStream != null) {
try {
objectInputStream.close();
} catch (IOException e) {
Log.e(TAG, "Error in Data#fromByteArray: ", e);
}
}
try {
inputStream.close();
} catch (IOException e) {
Log.e(TAG, "Error in Data#fromByteArray: ", e);
}
}
return new Data(map);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Data other = (Data) o;
Set<String> keys = mValues.keySet();
if (!keys.equals(other.mValues.keySet())) {
return false;
}
for (String key : keys) {
Object value = mValues.get(key);
Object otherValue = other.mValues.get(key);
boolean equal;
if (value == null || otherValue == null) {
equal = value == otherValue;
} else if (value instanceof Object[] && otherValue instanceof Object[]) {
equal = Arrays.deepEquals((Object[]) value, (Object[]) otherValue);
} else {
equal = value.equals(otherValue);
}
if (!equal) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
return 31 * mValues.hashCode();
}
@NonNull
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Data {");
if (!mValues.isEmpty()) {
for (String key : mValues.keySet()) {
sb.append(key).append(" : ");
Object value = mValues.get(key);
if (value instanceof Object[]) {
sb.append(Arrays.toString((Object[]) value));
} else {
sb.append(value);
}
sb.append(", ");
}
}
sb.append("}");
return sb.toString();
}
/**
* * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public static Boolean[] convertPrimitiveBooleanArray(@NonNull boolean[] value) {
Boolean[] returnValue = new Boolean[value.length];
for (int i = 0; i < value.length; ++i) {
returnValue[i] = value[i];
}
return returnValue;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public static Byte[] convertPrimitiveByteArray(@NonNull byte[] value) {
Byte[] returnValue = new Byte[value.length];
for (int i = 0; i < value.length; ++i) {
returnValue[i] = value[i];
}
return returnValue;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public static Integer[] convertPrimitiveIntArray(@NonNull int[] value) {
Integer[] returnValue = new Integer[value.length];
for (int i = 0; i < value.length; ++i) {
returnValue[i] = value[i];
}
return returnValue;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public static Long[] convertPrimitiveLongArray(@NonNull long[] value) {
Long[] returnValue = new Long[value.length];
for (int i = 0; i < value.length; ++i) {
returnValue[i] = value[i];
}
return returnValue;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public static Float[] convertPrimitiveFloatArray(@NonNull float[] value) {
Float[] returnValue = new Float[value.length];
for (int i = 0; i < value.length; ++i) {
returnValue[i] = value[i];
}
return returnValue;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public static Double[] convertPrimitiveDoubleArray(@NonNull double[] value) {
Double[] returnValue = new Double[value.length];
for (int i = 0; i < value.length; ++i) {
returnValue[i] = value[i];
}
return returnValue;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public static boolean[] convertToPrimitiveArray(@NonNull Boolean[] array) {
boolean[] returnArray = new boolean[array.length];
for (int i = 0; i < array.length; ++i) {
returnArray[i] = array[i];
}
return returnArray;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public static byte[] convertToPrimitiveArray(@NonNull Byte[] array) {
byte[] returnArray = new byte[array.length];
for (int i = 0; i < array.length; ++i) {
returnArray[i] = array[i];
}
return returnArray;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public static int[] convertToPrimitiveArray(@NonNull Integer[] array) {
int[] returnArray = new int[array.length];
for (int i = 0; i < array.length; ++i) {
returnArray[i] = array[i];
}
return returnArray;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public static long[] convertToPrimitiveArray(@NonNull Long[] array) {
long[] returnArray = new long[array.length];
for (int i = 0; i < array.length; ++i) {
returnArray[i] = array[i];
}
return returnArray;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public static float[] convertToPrimitiveArray(@NonNull Float[] array) {
float[] returnArray = new float[array.length];
for (int i = 0; i < array.length; ++i) {
returnArray[i] = array[i];
}
return returnArray;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public static double[] convertToPrimitiveArray(@NonNull Double[] array) {
double[] returnArray = new double[array.length];
for (int i = 0; i < array.length; ++i) {
returnArray[i] = array[i];
}
return returnArray;
}
/**
* A builder for {@link Data} objects.
*/
public static final class Builder {
private Map<String, Object> mValues = new HashMap<>();
/**
* Puts a boolean into the arguments.
*
* @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
@NonNull
public Builder putBoolean(@NonNull String key, boolean value) {
mValues.put(key, value);
return this;
}
/**
* Puts a boolean array into the arguments.
*
* @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
@NonNull
public Builder putBooleanArray(@NonNull String key, @NonNull boolean[] value) {
mValues.put(key, convertPrimitiveBooleanArray(value));
return this;
}
/**
* Puts an byte into the arguments.
*
* @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
@NonNull
public Builder putByte(@NonNull String key, byte value) {
mValues.put(key, value);
return this;
}
/**
* Puts an integer array into the arguments.
*
* @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
@NonNull
public Builder putByteArray(@NonNull String key, @NonNull byte[] value) {
mValues.put(key, convertPrimitiveByteArray(value));
return this;
}
/**
* Puts an integer into the arguments.
*
* @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
@NonNull
public Builder putInt(@NonNull String key, int value) {
mValues.put(key, value);
return this;
}
/**
* Puts an integer array into the arguments.
*
* @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
@NonNull
public Builder putIntArray(@NonNull String key, @NonNull int[] value) {
mValues.put(key, convertPrimitiveIntArray(value));
return this;
}
/**
* Puts a long into the arguments.
*
* @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
@NonNull
public Builder putLong(@NonNull String key, long value) {
mValues.put(key, value);
return this;
}
/**
* Puts a long array into the arguments.
*
* @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
@NonNull
public Builder putLongArray(@NonNull String key, @NonNull long[] value) {
mValues.put(key, convertPrimitiveLongArray(value));
return this;
}
/**
* Puts a float into the arguments.
*
* @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
@NonNull
public Builder putFloat(@NonNull String key, float value) {
mValues.put(key, value);
return this;
}
/**
* Puts a float array into the arguments.
*
* @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
@NonNull
public Builder putFloatArray(@NonNull String key, @NonNull float[] value) {
mValues.put(key, convertPrimitiveFloatArray(value));
return this;
}
/**
* Puts a double into the arguments.
*
* @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
@NonNull
public Builder putDouble(@NonNull String key, double value) {
mValues.put(key, value);
return this;
}
/**
* Puts a double array into the arguments.
*
* @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
@NonNull
public Builder putDoubleArray(@NonNull String key, @NonNull double[] value) {
mValues.put(key, convertPrimitiveDoubleArray(value));
return this;
}
/**
* Puts a String into the arguments.
*
* @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
@NonNull
public Builder putString(@NonNull String key, @Nullable String value) {
mValues.put(key, value);
return this;
}
/**
* Puts a String array into the arguments.
*
* @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
@NonNull
public Builder putStringArray(@NonNull String key, @NonNull String[] value) {
mValues.put(key, value);
return this;
}
/**
* Puts all input key-value pairs from a {@link Data} into the Builder.
* <p>
* Valid value types are: Boolean, Integer, Long, Float, Double, String, and their array
* versions. Invalid types will throw an {@link IllegalArgumentException}.
*
* @param data {@link Data} containing key-value pairs to add
* @return The {@link Builder}
*/
@NonNull
public Builder putAll(@NonNull Data data) {
putAll(data.mValues);
return this;
}
/**
* Puts all input key-value pairs from a {@link Map} into the Builder.
* <p>
* Valid value types are: Boolean, Integer, Long, Float, Double, String, and their array
* versions. Invalid types will throw an {@link IllegalArgumentException}.
*
* @param values A {@link Map} of key-value pairs to add
* @return The {@link Builder}
*/
@NonNull
public Builder putAll(@NonNull Map<String, Object> values) {
for (Map.Entry<String, Object> entry : values.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
put(key, value);
}
return this;
}
/**
* Puts an input key-value pair into the Builder. Valid types are: Boolean, Integer,
* Long, Float, Double, String, and array versions of each of those types.
* Invalid types throw an {@link IllegalArgumentException}.
*
* @param key A {@link String} key to add
* @param value A nullable {@link Object} value to add of the valid types
* @return The {@link Builder}
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public Builder put(@NonNull String key, @Nullable Object value) {
if (value == null) {
mValues.put(key, null);
} else {
Class<?> valueType = value.getClass();
if (valueType == Boolean.class
|| valueType == Byte.class
|| valueType == Integer.class
|| valueType == Long.class
|| valueType == Float.class
|| valueType == Double.class
|| valueType == String.class
|| valueType == Boolean[].class
|| valueType == Byte[].class
|| valueType == Integer[].class
|| valueType == Long[].class
|| valueType == Float[].class
|| valueType == Double[].class
|| valueType == String[].class) {
mValues.put(key, value);
} else if (valueType == boolean[].class) {
mValues.put(key, convertPrimitiveBooleanArray((boolean[]) value));
} else if (valueType == byte[].class) {
mValues.put(key, convertPrimitiveByteArray((byte[]) value));
} else if (valueType == int[].class) {
mValues.put(key, convertPrimitiveIntArray((int[]) value));
} else if (valueType == long[].class) {
mValues.put(key, convertPrimitiveLongArray((long[]) value));
} else if (valueType == float[].class) {
mValues.put(key, convertPrimitiveFloatArray((float[]) value));
} else if (valueType == double[].class) {
mValues.put(key, convertPrimitiveDoubleArray((double[]) value));
} else {
throw new IllegalArgumentException(
String.format("Key %s has invalid type %s", key, valueType));
}
}
return this;
}
/**
* Builds a {@link Data} object.
*
* @return The {@link Data} object containing all key-value pairs specified by this
* {@link Builder}.
*/
@NonNull
public Data build() {
Data data = new Data(mValues);
// Make sure we catch Data objects that are too large at build() instead of later. This
// method will throw an exception if data is too big.
Data.toByteArrayInternal(data);
return data;
}
}
}