| /* |
| * Copyright (C) 2022 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 android.uwb.util; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.os.ParcelUuid; |
| import android.os.PersistableBundle; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Objects; |
| import java.util.TreeSet; |
| import java.util.concurrent.locks.ReadWriteLock; |
| import java.util.concurrent.locks.ReentrantReadWriteLock; |
| |
| /** @hide */ |
| public class PersistableBundleUtils { |
| // private static final String LIST_KEY_FORMAT = "LIST_ITEM_%d"; |
| // private static final String COLLECTION_SIZE_KEY = "COLLECTION_LENGTH"; |
| // private static final String MAP_KEY_FORMAT = "MAP_KEY_%d"; |
| // private static final String MAP_VALUE_FORMAT = "MAP_VALUE_%d"; |
| // |
| // private static final String PARCEL_UUID_KEY = "PARCEL_UUID"; |
| // private static final String BYTE_ARRAY_KEY = "BYTE_ARRAY_KEY"; |
| // private static final String INTEGER_KEY = "INTEGER_KEY"; |
| // private static final String STRING_KEY = "STRING_KEY"; |
| // |
| // private final static char[] HEX_DIGITS = |
| // {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; |
| // private final static char[] HEX_LOWER_CASE_DIGITS = |
| // {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; |
| |
| |
| // /** |
| // * Functional interface to convert an object of the specified type to a PersistableBundle. |
| // * |
| // * @param <T> the type of the source object |
| // */ |
| // public interface Serializer<T> { |
| // /** |
| // * Converts this object to a PersistableBundle. |
| // * |
| // * @return the PersistableBundle representation of this object |
| // */ |
| // PersistableBundle toPersistableBundle(T obj); |
| // } |
| // |
| // /** |
| // * Functional interface used to create an object of the specified type from a PersistableBundle. |
| // * |
| // * @param <T> the type of the resultant object |
| // */ |
| // public interface Deserializer<T> { |
| // /** |
| // * Creates an instance of specified type from a PersistableBundle representation. |
| // * |
| // * @param in the PersistableBundle representation |
| // * @return an instance of the specified type |
| // */ |
| // T fromPersistableBundle(PersistableBundle in); |
| // } |
| // |
| // /** Serializer to convert an integer to a PersistableBundle. */ |
| // public static final Serializer<Integer> INTEGER_SERIALIZER = |
| // (i) -> { |
| // final PersistableBundle result = new PersistableBundle(); |
| // result.putInt(INTEGER_KEY, i); |
| // return result; |
| // }; |
| // |
| // /** Deserializer to convert a PersistableBundle to an integer. */ |
| // public static final Deserializer<Integer> INTEGER_DESERIALIZER = |
| // (bundle) -> { |
| // Objects.requireNonNull(bundle, "PersistableBundle is null"); |
| // return bundle.getInt(INTEGER_KEY); |
| // }; |
| // |
| // /** Serializer to convert s String to a PersistableBundle. */ |
| // public static final Serializer<String> STRING_SERIALIZER = |
| // (i) -> { |
| // final PersistableBundle result = new PersistableBundle(); |
| // result.putString(STRING_KEY, i); |
| // return result; |
| // }; |
| // |
| // /** Deserializer to convert a PersistableBundle to a String. */ |
| // public static final Deserializer<String> STRING_DESERIALIZER = |
| // (bundle) -> { |
| // Objects.requireNonNull(bundle, "PersistableBundle is null"); |
| // return bundle.getString(STRING_KEY); |
| // }; |
| // |
| // /** |
| // * Converts a ParcelUuid to a PersistableBundle. |
| // * |
| // * <p>To avoid key collisions, NO additional key/value pairs should be added to the returned |
| // * PersistableBundle object. |
| // * |
| // * @param uuid a ParcelUuid instance to persist |
| // * @return the PersistableBundle instance |
| // */ |
| // public static PersistableBundle fromParcelUuid(ParcelUuid uuid) { |
| // final PersistableBundle result = new PersistableBundle(); |
| // |
| // result.putString(PARCEL_UUID_KEY, uuid.toString()); |
| // |
| // return result; |
| // } |
| // |
| // /** |
| // * Converts from a PersistableBundle to a ParcelUuid. |
| // * |
| // * @param bundle the PersistableBundle containing the ParcelUuid |
| // * @return the ParcelUuid instance |
| // */ |
| // public static ParcelUuid toParcelUuid(PersistableBundle bundle) { |
| // return ParcelUuid.fromString(bundle.getString(PARCEL_UUID_KEY)); |
| // } |
| // |
| // /** |
| // * Converts from a list of Persistable objects to a single PersistableBundle. |
| // * |
| // * <p>To avoid key collisions, NO additional key/value pairs should be added to the returned |
| // * PersistableBundle object. |
| // * |
| // * @param <T> the type of the objects to convert to the PersistableBundle |
| // * @param in the list of objects to be serialized into a PersistableBundle |
| // * @param serializer an implementation of the {@link Serializer} functional interface that |
| // * converts an object of type T to a PersistableBundle |
| // */ |
| // @NonNull |
| // public static <T> PersistableBundle fromList( |
| // @NonNull List<T> in, @NonNull Serializer<T> serializer) { |
| // final PersistableBundle result = new PersistableBundle(); |
| // |
| // result.putInt(COLLECTION_SIZE_KEY, in.size()); |
| // for (int i = 0; i < in.size(); i++) { |
| // final String key = String.format(LIST_KEY_FORMAT, i); |
| // result.putPersistableBundle(key, serializer.toPersistableBundle(in.get(i))); |
| // } |
| // return result; |
| // } |
| // |
| // /** |
| // * Converts from a PersistableBundle to a list of objects. |
| // * |
| // * @param <T> the type of the objects to convert from a PersistableBundle |
| // * @param in the PersistableBundle containing the persisted list |
| // * @param deserializer an implementation of the {@link Deserializer} functional interface that |
| // * builds the relevant type of objects. |
| // */ |
| // @NonNull |
| // public static <T> List<T> toList( |
| // @NonNull PersistableBundle in, @NonNull Deserializer<T> deserializer) { |
| // final int listLength = in.getInt(COLLECTION_SIZE_KEY); |
| // final ArrayList<T> result = new ArrayList<>(listLength); |
| // |
| // for (int i = 0; i < listLength; i++) { |
| // final String key = String.format(LIST_KEY_FORMAT, i); |
| // final PersistableBundle item = in.getPersistableBundle(key); |
| // |
| // result.add(deserializer.fromPersistableBundle(item)); |
| // } |
| // return result; |
| // } |
| // |
| // // TODO: b/170513329 Delete #fromByteArray and #toByteArray once BaseBundle#putByteArray and |
| // // BaseBundle#getByteArray are exposed. |
| // |
| // /** |
| // * Converts a byte array to a PersistableBundle. |
| // * |
| // * <p>To avoid key collisions, NO additional key/value pairs should be added to the returned |
| // * PersistableBundle object. |
| // * |
| // * @param array a byte array instance to persist |
| // * @return the PersistableBundle instance |
| // */ |
| // public static PersistableBundle fromByteArray(byte[] array) { |
| // final PersistableBundle result = new PersistableBundle(); |
| // |
| // result.putString(BYTE_ARRAY_KEY, toHexString(array)); |
| // |
| // return result; |
| // } |
| // |
| // // Copied from com.android.internal.util.HexDump |
| // @UnsupportedAppUsage |
| // public static String toHexString(byte[] array, int offset, int length) { |
| // return toHexString(array, offset, length, true); |
| // } |
| // |
| // public static String toHexString(byte[] array, int offset, int length, boolean upperCase) { |
| // char[] digits = upperCase ? HEX_DIGITS : HEX_LOWER_CASE_DIGITS; |
| // char[] buf = new char[length * 2]; |
| // |
| // int bufIndex = 0; |
| // for (int i = offset; i < offset + length; i++) { |
| // byte b = array[i]; |
| // buf[bufIndex++] = digits[(b >>> 4) & 0x0F]; |
| // buf[bufIndex++] = digits[b & 0x0F]; |
| // } |
| // |
| // return new String(buf); |
| // } |
| // |
| // @UnsupportedAppUsage |
| // public static String toHexString(byte[] array) { |
| // return toHexString(array, 0, array.length, true); |
| // } |
| // |
| // @UnsupportedAppUsage |
| // public static String toHexString(int i) { |
| // return toHexString(toByteArray(i)); |
| // } |
| // |
| // public static byte[] toByteArray(int i) { |
| // byte[] array = new byte[4]; |
| // |
| // array[3] = (byte) (i & 0xFF); |
| // array[2] = (byte) ((i >> 8) & 0xFF); |
| // array[1] = (byte) ((i >> 16) & 0xFF); |
| // array[0] = (byte) ((i >> 24) & 0xFF); |
| // |
| // return array; |
| // } |
| // |
| // @UnsupportedAppUsage |
| // public static byte[] hexStringToByteArray(String hexString) { |
| // int length = hexString.length(); |
| // byte[] buffer = new byte[length / 2]; |
| // |
| // for (int i = 0; i < length; i += 2) { |
| // buffer[i / 2] = (byte) ((toByte(hexString.charAt(i)) << 4) | toByte( |
| // hexString.charAt(i + 1))); |
| // } |
| // |
| // return buffer; |
| // } |
| // |
| // private static int toByte(char c) { |
| // if (c >= '0' && c <= '9') return (c - '0'); |
| // if (c >= 'A' && c <= 'F') return (c - 'A' + 10); |
| // if (c >= 'a' && c <= 'f') return (c - 'a' + 10); |
| // |
| // throw new RuntimeException("Invalid hex char '" + c + "'"); |
| // } |
| // |
| // /** |
| // * Converts from a PersistableBundle to a byte array. |
| // * |
| // * @param bundle the PersistableBundle containing the byte array |
| // * @return the byte array instance |
| // */ |
| // public static byte[] toByteArray(PersistableBundle bundle) { |
| // Objects.requireNonNull(bundle, "PersistableBundle is null"); |
| // |
| // String hex = bundle.getString(BYTE_ARRAY_KEY); |
| // if (hex == null || hex.length() % 2 != 0) { |
| // throw new IllegalArgumentException("PersistableBundle contains invalid byte array"); |
| // } |
| // |
| // return hexStringToByteArray(hex); |
| // } |
| // |
| // /** |
| // * Converts from a Map of Persistable objects to a single PersistableBundle. |
| // * |
| // * <p>To avoid key collisions, NO additional key/value pairs should be added to the returned |
| // * PersistableBundle object. |
| // * |
| // * @param <K> the type of the map-key to convert to the PersistableBundle |
| // * @param <V> the type of the map-value to convert to the PersistableBundle |
| // * @param in the Map of objects implementing the {@link Persistable} interface |
| // * @param keySerializer an implementation of the {@link Serializer} functional interface that |
| // * converts a map-key of type T to a PersistableBundle |
| // * @param valueSerializer an implementation of the {@link Serializer} functional interface that |
| // * converts a map-value of type E to a PersistableBundle |
| // */ |
| // @NonNull |
| // public static <K, V> PersistableBundle fromMap( |
| // @NonNull Map<K, V> in, |
| // @NonNull Serializer<K> keySerializer, |
| // @NonNull Serializer<V> valueSerializer) { |
| // final PersistableBundle result = new PersistableBundle(); |
| // |
| // result.putInt(COLLECTION_SIZE_KEY, in.size()); |
| // int i = 0; |
| // for (Entry<K, V> entry : in.entrySet()) { |
| // final String keyKey = String.format(MAP_KEY_FORMAT, i); |
| // final String valueKey = String.format(MAP_VALUE_FORMAT, i); |
| // result.putPersistableBundle(keyKey, keySerializer.toPersistableBundle(entry.getKey())); |
| // result.putPersistableBundle( |
| // valueKey, valueSerializer.toPersistableBundle(entry.getValue())); |
| // |
| // i++; |
| // } |
| // |
| // return result; |
| // } |
| // |
| // /** |
| // * Converts from a PersistableBundle to a Map of objects. |
| // * |
| // * <p>In an attempt to preserve ordering, the returned map will be a LinkedHashMap. However, the |
| // * guarantees on the ordering can only ever be as strong as the map that was serialized in |
| // * {@link fromMap()}. If the initial map that was serialized had no ordering guarantees, the |
| // * deserialized map similarly may be of a non-deterministic order. |
| // * |
| // * @param <K> the type of the map-key to convert from a PersistableBundle |
| // * @param <V> the type of the map-value to convert from a PersistableBundle |
| // * @param in the PersistableBundle containing the persisted Map |
| // * @param keyDeserializer an implementation of the {@link Deserializer} functional interface |
| // * that builds the relevant type of map-key. |
| // * @param valueDeserializer an implementation of the {@link Deserializer} functional interface |
| // * that builds the relevant type of map-value. |
| // * @return An instance of the parsed map as a LinkedHashMap (in an attempt to preserve |
| // * ordering). |
| // */ |
| // @NonNull |
| // public static <K, V> LinkedHashMap<K, V> toMap( |
| // @NonNull PersistableBundle in, |
| // @NonNull Deserializer<K> keyDeserializer, |
| // @NonNull Deserializer<V> valueDeserializer) { |
| // final int mapSize = in.getInt(COLLECTION_SIZE_KEY); |
| // final LinkedHashMap<K, V> result = new LinkedHashMap<>(mapSize); |
| // |
| // for (int i = 0; i < mapSize; i++) { |
| // final String keyKey = String.format(MAP_KEY_FORMAT, i); |
| // final String valueKey = String.format(MAP_VALUE_FORMAT, i); |
| // final PersistableBundle keyBundle = in.getPersistableBundle(keyKey); |
| // final PersistableBundle valueBundle = in.getPersistableBundle(valueKey); |
| // |
| // final K key = keyDeserializer.fromPersistableBundle(keyBundle); |
| // final V value = valueDeserializer.fromPersistableBundle(valueBundle); |
| // result.put(key, value); |
| // } |
| // return result; |
| // } |
| // |
| // /** |
| // * Converts a PersistableBundle into a disk-stable byte array format |
| // * |
| // * @param bundle the PersistableBundle to be converted to a disk-stable format |
| // * @return the byte array representation of the PersistableBundle |
| // */ |
| // @Nullable |
| // public static byte[] toDiskStableBytes(@NonNull PersistableBundle bundle) throws IOException { |
| // final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); |
| // bundle.writeToStream(outputStream); |
| // return outputStream.toByteArray(); |
| // } |
| // |
| // /** |
| // * Converts from a disk-stable byte array format to a PersistableBundle |
| // * |
| // * @param bytes the disk-stable byte array |
| // * @return the PersistableBundle parsed from this byte array. |
| // */ |
| // public static PersistableBundle fromDiskStableBytes(@NonNull byte[] bytes) throws IOException { |
| // final ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); |
| // return PersistableBundle.readFromStream(inputStream); |
| // } |
| // |
| // /** |
| // * Ensures safe reading and writing of {@link PersistableBundle}s to and from disk. |
| // * |
| // * <p>This class will enforce exclusion between reads and writes using the standard semantics of |
| // * a ReadWriteLock. Specifically, concurrent readers ARE allowed, but reads/writes from/to the |
| // * file are mutually exclusive. In other words, for an unbounded number n, the acceptable states |
| // * are n readers, OR 1 writer (but not both). |
| // */ |
| // public static class LockingReadWriteHelper { |
| // private final ReadWriteLock mDiskLock = new ReentrantReadWriteLock(); |
| // private final String mPath; |
| // |
| // public LockingReadWriteHelper(@NonNull String path) { |
| // mPath = Objects.requireNonNull(path, "fileName was null"); |
| // } |
| // |
| // /** |
| // * Reads the {@link PersistableBundle} from the disk. |
| // * |
| // * @return the PersistableBundle, if the file existed, or null otherwise |
| // */ |
| // @Nullable |
| // public PersistableBundle readFromDisk() throws IOException { |
| // try { |
| // mDiskLock.readLock().lock(); |
| // final File file = new File(mPath); |
| // if (!file.exists()) { |
| // return null; |
| // } |
| // |
| // try (FileInputStream fis = new FileInputStream(file)) { |
| // return PersistableBundle.readFromStream(fis); |
| // } |
| // } finally { |
| // mDiskLock.readLock().unlock(); |
| // } |
| // } |
| // |
| // /** |
| // * Writes a {@link PersistableBundle} to disk. |
| // * |
| // * @param bundle the {@link PersistableBundle} to write to disk |
| // */ |
| // public void writeToDisk(@NonNull PersistableBundle bundle) throws IOException { |
| // Objects.requireNonNull(bundle, "bundle was null"); |
| // |
| // try { |
| // mDiskLock.writeLock().lock(); |
| // final File file = new File(mPath); |
| // if (!file.exists()) { |
| // file.getParentFile().mkdirs(); |
| // } |
| // |
| // try (FileOutputStream fos = new FileOutputStream(file)) { |
| // bundle.writeToStream(fos); |
| // } |
| // } finally { |
| // mDiskLock.writeLock().unlock(); |
| // } |
| // } |
| // } |
| // |
| // /** |
| // * Returns a copy of the persistable bundle with only the specified keys |
| // * |
| // * <p>This allows for holding minimized copies for memory-saving purposes. |
| // */ |
| // @NonNull |
| // public static PersistableBundle minimizeBundle( |
| // @NonNull PersistableBundle bundle, String... keys) { |
| // final PersistableBundle minimized = new PersistableBundle(); |
| // |
| // if (bundle == null) { |
| // return minimized; |
| // } |
| // |
| // for (String key : keys) { |
| // if (bundle.containsKey(key)) { |
| // final Object value = bundle.get(key); |
| // if (value == null) { |
| // continue; |
| // } |
| // |
| // if (value instanceof Boolean) { |
| // minimized.putBoolean(key, (Boolean) value); |
| // } else if (value instanceof boolean[]) { |
| // minimized.putBooleanArray(key, (boolean[]) value); |
| // } else if (value instanceof Double) { |
| // minimized.putDouble(key, (Double) value); |
| // } else if (value instanceof double[]) { |
| // minimized.putDoubleArray(key, (double[]) value); |
| // } else if (value instanceof Integer) { |
| // minimized.putInt(key, (Integer) value); |
| // } else if (value instanceof int[]) { |
| // minimized.putIntArray(key, (int[]) value); |
| // } else if (value instanceof Long) { |
| // minimized.putLong(key, (Long) value); |
| // } else if (value instanceof long[]) { |
| // minimized.putLongArray(key, (long[]) value); |
| // } else if (value instanceof String) { |
| // minimized.putString(key, (String) value); |
| // } else if (value instanceof String[]) { |
| // minimized.putStringArray(key, (String[]) value); |
| // } else if (value instanceof PersistableBundle) { |
| // minimized.putPersistableBundle(key, (PersistableBundle) value); |
| // } else { |
| // continue; |
| // } |
| // } |
| // } |
| // |
| // return minimized; |
| // } |
| |
| /** Builds a stable hashcode */ |
| public static int getHashCode(@Nullable PersistableBundle bundle) { |
| if (bundle == null) { |
| return -1; |
| } |
| |
| int iterativeHashcode = 0; |
| TreeSet<String> treeSet = new TreeSet<>(bundle.keySet()); |
| for (String key : treeSet) { |
| Object val = bundle.get(key); |
| if (val instanceof PersistableBundle) { |
| iterativeHashcode = |
| Objects.hash(iterativeHashcode, key, getHashCode((PersistableBundle) val)); |
| } else { |
| iterativeHashcode = Objects.hash(iterativeHashcode, key, val); |
| } |
| } |
| |
| return iterativeHashcode; |
| } |
| |
| /** Checks for persistable bundle equality */ |
| public static boolean isEqual( |
| @Nullable PersistableBundle left, @Nullable PersistableBundle right) { |
| // Check for pointer equality & null equality |
| if (Objects.equals(left, right)) { |
| return true; |
| } |
| |
| // If only one of the two is null, but not the other, not equal by definition. |
| if (Objects.isNull(left) != Objects.isNull(right)) { |
| return false; |
| } |
| |
| if (!left.keySet().equals(right.keySet())) { |
| return false; |
| } |
| |
| for (String key : left.keySet()) { |
| Object leftVal = left.get(key); |
| Object rightVal = right.get(key); |
| |
| // Check for equality |
| if (Objects.equals(leftVal, rightVal)) { |
| continue; |
| } else if (Objects.isNull(leftVal) != Objects.isNull(rightVal)) { |
| // If only one of the two is null, but not the other, not equal by definition. |
| return false; |
| } else if (!Objects.equals(leftVal.getClass(), rightVal.getClass())) { |
| // If classes are different, not equal by definition. |
| return false; |
| } |
| if (leftVal instanceof PersistableBundle) { |
| if (!isEqual((PersistableBundle) leftVal, (PersistableBundle) rightVal)) { |
| return false; |
| } |
| } else if (leftVal.getClass().isArray()) { |
| if (leftVal instanceof boolean[]) { |
| if (!Arrays.equals((boolean[]) leftVal, (boolean[]) rightVal)) { |
| return false; |
| } |
| } else if (leftVal instanceof double[]) { |
| if (!Arrays.equals((double[]) leftVal, (double[]) rightVal)) { |
| return false; |
| } |
| } else if (leftVal instanceof int[]) { |
| if (!Arrays.equals((int[]) leftVal, (int[]) rightVal)) { |
| return false; |
| } |
| } else if (leftVal instanceof long[]) { |
| if (!Arrays.equals((long[]) leftVal, (long[]) rightVal)) { |
| return false; |
| } |
| } else if (!Arrays.equals((Object[]) leftVal, (Object[]) rightVal)) { |
| return false; |
| } |
| } else { |
| if (!Objects.equals(leftVal, rightVal)) { |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| // /** |
| // * Wrapper class around PersistableBundles to allow equality comparisons |
| // * |
| // * <p>This class exposes the minimal getters to retrieve values. |
| // */ |
| // public static class PersistableBundleWrapper { |
| // @NonNull |
| // private final PersistableBundle mBundle; |
| // |
| // public PersistableBundleWrapper(@NonNull PersistableBundle bundle) { |
| // mBundle = Objects.requireNonNull(bundle, "Bundle was null"); |
| // } |
| // |
| // /** |
| // * Retrieves the integer associated with the provided key. |
| // * |
| // * @param key the string key to query |
| // * @param defaultValue the value to return if key does not exist |
| // * @return the int value, or the default |
| // */ |
| // public int getInt(String key, int defaultValue) { |
| // return mBundle.getInt(key, defaultValue); |
| // } |
| // |
| // @Override |
| // public int hashCode() { |
| // return getHashCode(mBundle); |
| // } |
| // |
| // @Override |
| // public boolean equals(Object obj) { |
| // if (!(obj instanceof PersistableBundleWrapper)) { |
| // return false; |
| // } |
| // |
| // final PersistableBundleWrapper other = (PersistableBundleWrapper) obj; |
| // |
| // return isEqual(mBundle, other.mBundle); |
| // } |
| // } |
| } |