From b92cbc787e0420b12619540083fee850775ce3c7 Mon Sep 17 00:00:00 2001 From: Yan Yan Date: Fri, 6 Nov 2020 14:53:28 -0800 Subject: [PATCH] Add utilities to persist lists of Persistable objects This change adds a utility class to enable persistance of Lists of objects. The PersistableBundle class does not currently support lists or arrays of PersistableBundles, presumably due to the potential for key conflicts. The utility classes added here avoid that concern by nesting all lists as separate persistable bundles. Bug: 163594033 Test: New PersistableBundleUtilsTest added, passing Change-Id: I89478cf0d05d41a4b0d769de4859421061a1f1d9 --- .../vcn/util/PersistableBundleUtils.java | 105 ++++++++++++++ .../vcn/util/PersistableBundleUtilsTest.java | 131 ++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java create mode 100644 tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.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..73054ce046239 --- /dev/null +++ b/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java @@ -0,0 +1,105 @@ +/* + * 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.os.PersistableBundle; + +import java.util.ArrayList; +import java.util.List; + +/** @hide */ +public class PersistableBundleUtils { + private static final String LIST_KEY_FORMAT = "LIST_ITEM_%d"; + private static final String LIST_LENGTH_KEY = "LIST_LENGTH"; + + /** + * 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); + } + + /** + * 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(LIST_LENGTH_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(LIST_LENGTH_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; + } +} 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..f78eeb69b5eb6 --- /dev/null +++ b/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java @@ -0,0 +1,131 @@ +/* + * 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.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.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 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 testConversionLossless() 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); + } +}