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

Commit e9226f99 authored by Yan Yan's avatar Yan Yan Committed by Gerrit Code Review
Browse files

Merge changes from topics "PersistableBundleUtils", "bytearray-utils", "integer-utils"

* changes:
  Support converting integer to/from PersistableBundle
  Support converting byte array to/from PersistableBundle
  Include PersistableBundleUtils in framework-ike-shared-srcs
  Add utils for converting Maps and ParcelUuid to/from PersistableBundle
  Add utilities to persist lists of Persistable objects
parents e1bb600f cfb6847e
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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",
    ],
}
+341 −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.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 <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);
            };

    /**
     * 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, 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.
     *
     * <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;
    }

    /**
     * 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();
            }
        }
    }
}
+214 −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.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<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);
    }

    @Test
    public void testMapConversionLossless() throws Exception {
        final LinkedHashMap<TestKey, TestClass> 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<TestKey, TestClass> 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);
    }
}