Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b92cbc78 authored by Yan Yan's avatar Yan Yan
Browse files

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
parent 26b017aa
Loading
Loading
Loading
Loading
+105 −0
Original line number Diff line number Diff line
/*
 * 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 <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);
    }

    /**
     * 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(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 <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(LIST_LENGTH_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;
    }
}
+131 −0
Original line number Diff line number Diff line
/*
 * 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<TestClass> 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<TestClass> resultList = PersistableBundleUtils.toList(bundled, TestClass::new);

        assertEquals(sourceList, resultList);
    }
}