diff --git a/Android.bp b/Android.bp index eb1718efbef65..570040f3ab93c 100644 --- a/Android.bp +++ b/Android.bp @@ -744,6 +744,7 @@ filegroup { "core/java/com/android/internal/util/IState.java", "core/java/com/android/internal/util/State.java", "core/java/com/android/internal/util/StateMachine.java", + "services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java", "telephony/java/android/telephony/Annotation.java", ], } diff --git a/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java b/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java new file mode 100644 index 0000000000000..5c1b5ffb22099 --- /dev/null +++ b/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2020 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 com.android.server.vcn.util; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.ParcelUuid; +import android.os.PersistableBundle; + +import com.android.internal.util.HexDump; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +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"; + + /** + * Functional interface to convert an object of the specified type to a PersistableBundle. + * + * @param the type of the source object + */ + public interface Serializer { + /** + * 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 the type of the resultant object + */ + public interface Deserializer { + /** + * 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_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_DESERIALIZER = + (bundle) -> { + Objects.requireNonNull(bundle, "PersistableBundle is null"); + return bundle.getInt(INTEGER_KEY); + }; + + /** + * Converts a ParcelUuid to a PersistableBundle. + * + *

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. + * + *

To avoid key collisions, NO additional key/value pairs should be added to the returned + * PersistableBundle object. + * + * @param 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 PersistableBundle fromList( + @NonNull List in, @NonNull Serializer 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 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 List toList( + @NonNull PersistableBundle in, @NonNull Deserializer deserializer) { + final int listLength = in.getInt(COLLECTION_SIZE_KEY); + final ArrayList 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. + * + *

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, HexDump.toHexString(array)); + + return result; + } + + /** + * 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 HexDump.hexStringToByteArray(hex); + } + + /** + * Converts from a Map of Persistable objects to a single PersistableBundle. + * + *

To avoid key collisions, NO additional key/value pairs should be added to the returned + * PersistableBundle object. + * + * @param the type of the map-key to convert to the PersistableBundle + * @param 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 PersistableBundle fromMap( + @NonNull Map in, + @NonNull Serializer keySerializer, + @NonNull Serializer valueSerializer) { + final PersistableBundle result = new PersistableBundle(); + + result.putInt(COLLECTION_SIZE_KEY, in.size()); + int i = 0; + for (Entry 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. + * + *

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 the type of the map-key to convert from a PersistableBundle + * @param 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 LinkedHashMap toMap( + @NonNull PersistableBundle in, + @NonNull Deserializer keyDeserializer, + @NonNull Deserializer valueDeserializer) { + final int mapSize = in.getInt(COLLECTION_SIZE_KEY); + final LinkedHashMap 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; + } + + /** + * Ensures safe reading and writing of {@link PersistableBundle}s to and from disk. + * + *

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(); + } + } + } +} diff --git a/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java b/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java new file mode 100644 index 0000000000000..a44a734a2dce6 --- /dev/null +++ b/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2020 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 com.android.server.vcn.util; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import android.os.PersistableBundle; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Objects; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class PersistableBundleUtilsTest { + private static final String TEST_KEY = "testKey"; + private static final String TEST_STRING_PREFIX = "testString"; + private static final int[] TEST_INT_ARRAY = new int[] {0, 1, 2, 3, 4}; + + private static final int NUM_COLLECTION_ENTRIES = 10; + + private static class TestKey { + private static final String TEST_INTEGER_KEY = + "mTestInteger"; // Purposely colliding with keys of test class to ensure namespacing + private final int mTestInteger; + + TestKey(int testInteger) { + mTestInteger = testInteger; + } + + TestKey(PersistableBundle in) { + mTestInteger = in.getInt(TEST_INTEGER_KEY); + } + + public PersistableBundle toPersistableBundle() { + final PersistableBundle result = new PersistableBundle(); + + result.putInt(TEST_INTEGER_KEY, mTestInteger); + + return result; + } + + @Override + public int hashCode() { + return Objects.hash(mTestInteger); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof TestKey)) { + return false; + } + + final TestKey other = (TestKey) o; + return mTestInteger == other.mTestInteger; + } + } + + private static class TestClass { + private static final String TEST_INTEGER_KEY = "mTestInteger"; + private final int mTestInteger; + + private static final String TEST_INT_ARRAY_KEY = "mTestIntArray"; + private final int[] mTestIntArray; + + private static final String TEST_STRING_KEY = "mTestString"; + private final String mTestString; + + private static final String TEST_PERSISTABLE_BUNDLE_KEY = "mTestPersistableBundle"; + private final PersistableBundle mTestPersistableBundle; + + TestClass( + int testInteger, + int[] testIntArray, + String testString, + PersistableBundle testPersistableBundle) { + mTestInteger = testInteger; + mTestIntArray = testIntArray; + mTestString = testString; + mTestPersistableBundle = testPersistableBundle; + } + + TestClass(PersistableBundle in) { + mTestInteger = in.getInt(TEST_INTEGER_KEY); + mTestIntArray = in.getIntArray(TEST_INT_ARRAY_KEY); + mTestString = in.getString(TEST_STRING_KEY); + mTestPersistableBundle = in.getPersistableBundle(TEST_PERSISTABLE_BUNDLE_KEY); + } + + public PersistableBundle toPersistableBundle() { + final PersistableBundle result = new PersistableBundle(); + + result.putInt(TEST_INTEGER_KEY, mTestInteger); + result.putIntArray(TEST_INT_ARRAY_KEY, mTestIntArray); + result.putString(TEST_STRING_KEY, mTestString); + result.putPersistableBundle(TEST_PERSISTABLE_BUNDLE_KEY, mTestPersistableBundle); + + return result; + } + + @Override + public int hashCode() { + return Objects.hash( + mTestInteger, + Arrays.hashCode(mTestIntArray), + mTestString, + mTestPersistableBundle); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof TestClass)) { + return false; + } + + final TestClass other = (TestClass) o; + + // TODO: Add a proper equals() to PersistableBundle. But in the meantime, force + // TODO: unparcelling in order to allow test comparison. + if (mTestPersistableBundle.size() != other.mTestPersistableBundle.size()) { + return false; + } + + return mTestInteger == other.mTestInteger + && Arrays.equals(mTestIntArray, other.mTestIntArray) + && mTestString.equals(other.mTestString) + && mTestPersistableBundle.kindofEquals(other.mTestPersistableBundle); + } + } + + @Test + public void testListConversionLossless() throws Exception { + final List sourceList = new ArrayList<>(); + for (int i = 0; i < NUM_COLLECTION_ENTRIES; i++) { + final PersistableBundle innerBundle = new PersistableBundle(); + innerBundle.putInt(TEST_KEY, i); + + sourceList.add(new TestClass(i, TEST_INT_ARRAY, TEST_STRING_PREFIX + i, innerBundle)); + } + + final PersistableBundle bundled = + PersistableBundleUtils.fromList(sourceList, TestClass::toPersistableBundle); + final List resultList = PersistableBundleUtils.toList(bundled, TestClass::new); + + assertEquals(sourceList, resultList); + } + + @Test + public void testMapConversionLossless() throws Exception { + final LinkedHashMap sourceMap = new LinkedHashMap<>(); + for (int i = 0; i < NUM_COLLECTION_ENTRIES; i++) { + final TestKey key = new TestKey(i * i); + + final PersistableBundle innerBundle = new PersistableBundle(); + innerBundle.putInt(TEST_KEY, i); + final TestClass value = + new TestClass(i, TEST_INT_ARRAY, TEST_STRING_PREFIX + i, innerBundle); + + sourceMap.put(key, value); + } + + final PersistableBundle bundled = + PersistableBundleUtils.fromMap( + sourceMap, TestKey::toPersistableBundle, TestClass::toPersistableBundle); + final LinkedHashMap resultList = + PersistableBundleUtils.toMap(bundled, TestKey::new, TestClass::new); + + assertEquals(sourceMap, resultList); + } + + @Test + public void testByteArrayConversionLossless() { + final byte[] byteArray = "testByteArrayConversionLossless".getBytes(); + + PersistableBundle bundle = PersistableBundleUtils.fromByteArray(byteArray); + byte[] result = PersistableBundleUtils.toByteArray(bundle); + + assertArrayEquals(byteArray, result); + } + + @Test + public void testIntegerConversionLossless() throws Exception { + final int testInt = 1; + final PersistableBundle integerBundle = + PersistableBundleUtils.INTEGER_SERIALIZER.toPersistableBundle(testInt); + final int result = + PersistableBundleUtils.INTEGER_DESERIALIZER.fromPersistableBundle(integerBundle); + + assertEquals(testInt, result); + } +}