1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.vcn.util;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.os.ParcelUuid;
22 import android.os.PersistableBundle;
23 
24 import com.android.internal.util.HexDump;
25 
26 import java.io.ByteArrayInputStream;
27 import java.io.ByteArrayOutputStream;
28 import java.io.File;
29 import java.io.FileInputStream;
30 import java.io.FileOutputStream;
31 import java.io.IOException;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.LinkedHashMap;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Map.Entry;
38 import java.util.Objects;
39 import java.util.TreeSet;
40 import java.util.concurrent.locks.ReadWriteLock;
41 import java.util.concurrent.locks.ReentrantReadWriteLock;
42 
43 /** @hide */
44 public class PersistableBundleUtils {
45     private static final String LIST_KEY_FORMAT = "LIST_ITEM_%d";
46     private static final String COLLECTION_SIZE_KEY = "COLLECTION_LENGTH";
47     private static final String MAP_KEY_FORMAT = "MAP_KEY_%d";
48     private static final String MAP_VALUE_FORMAT = "MAP_VALUE_%d";
49 
50     private static final String PARCEL_UUID_KEY = "PARCEL_UUID";
51     private static final String BYTE_ARRAY_KEY = "BYTE_ARRAY_KEY";
52     private static final String INTEGER_KEY = "INTEGER_KEY";
53     private static final String STRING_KEY = "STRING_KEY";
54 
55     /**
56      * Functional interface to convert an object of the specified type to a PersistableBundle.
57      *
58      * @param <T> the type of the source object
59      */
60     public interface Serializer<T> {
61         /**
62          * Converts this object to a PersistableBundle.
63          *
64          * @return the PersistableBundle representation of this object
65          */
toPersistableBundle(T obj)66         PersistableBundle toPersistableBundle(T obj);
67     }
68 
69     /**
70      * Functional interface used to create an object of the specified type from a PersistableBundle.
71      *
72      * @param <T> the type of the resultant object
73      */
74     public interface Deserializer<T> {
75         /**
76          * Creates an instance of specified type from a PersistableBundle representation.
77          *
78          * @param in the PersistableBundle representation
79          * @return an instance of the specified type
80          */
fromPersistableBundle(PersistableBundle in)81         T fromPersistableBundle(PersistableBundle in);
82     }
83 
84     /** Serializer to convert an integer to a PersistableBundle. */
85     public static final Serializer<Integer> INTEGER_SERIALIZER =
86             (i) -> {
87                 final PersistableBundle result = new PersistableBundle();
88                 result.putInt(INTEGER_KEY, i);
89                 return result;
90             };
91 
92     /** Deserializer to convert a PersistableBundle to an integer. */
93     public static final Deserializer<Integer> INTEGER_DESERIALIZER =
94             (bundle) -> {
95                 Objects.requireNonNull(bundle, "PersistableBundle is null");
96                 return bundle.getInt(INTEGER_KEY);
97             };
98 
99     /** Serializer to convert s String to a PersistableBundle. */
100     public static final Serializer<String> STRING_SERIALIZER =
101             (i) -> {
102                 final PersistableBundle result = new PersistableBundle();
103                 result.putString(STRING_KEY, i);
104                 return result;
105             };
106 
107     /** Deserializer to convert a PersistableBundle to a String. */
108     public static final Deserializer<String> STRING_DESERIALIZER =
109             (bundle) -> {
110                 Objects.requireNonNull(bundle, "PersistableBundle is null");
111                 return bundle.getString(STRING_KEY);
112             };
113 
114     /**
115      * Converts a ParcelUuid to a PersistableBundle.
116      *
117      * <p>To avoid key collisions, NO additional key/value pairs should be added to the returned
118      * PersistableBundle object.
119      *
120      * @param uuid a ParcelUuid instance to persist
121      * @return the PersistableBundle instance
122      */
fromParcelUuid(ParcelUuid uuid)123     public static PersistableBundle fromParcelUuid(ParcelUuid uuid) {
124         final PersistableBundle result = new PersistableBundle();
125 
126         result.putString(PARCEL_UUID_KEY, uuid.toString());
127 
128         return result;
129     }
130 
131     /**
132      * Converts from a PersistableBundle to a ParcelUuid.
133      *
134      * @param bundle the PersistableBundle containing the ParcelUuid
135      * @return the ParcelUuid instance
136      */
toParcelUuid(PersistableBundle bundle)137     public static ParcelUuid toParcelUuid(PersistableBundle bundle) {
138         return ParcelUuid.fromString(bundle.getString(PARCEL_UUID_KEY));
139     }
140 
141     /**
142      * Converts from a list of Persistable objects to a single PersistableBundle.
143      *
144      * <p>To avoid key collisions, NO additional key/value pairs should be added to the returned
145      * PersistableBundle object.
146      *
147      * @param <T> the type of the objects to convert to the PersistableBundle
148      * @param in the list of objects to be serialized into a PersistableBundle
149      * @param serializer an implementation of the {@link Serializer} functional interface that
150      *     converts an object of type T to a PersistableBundle
151      */
152     @NonNull
fromList( @onNull List<T> in, @NonNull Serializer<T> serializer)153     public static <T> PersistableBundle fromList(
154             @NonNull List<T> in, @NonNull Serializer<T> serializer) {
155         final PersistableBundle result = new PersistableBundle();
156 
157         result.putInt(COLLECTION_SIZE_KEY, in.size());
158         for (int i = 0; i < in.size(); i++) {
159             final String key = String.format(LIST_KEY_FORMAT, i);
160             result.putPersistableBundle(key, serializer.toPersistableBundle(in.get(i)));
161         }
162         return result;
163     }
164 
165     /**
166      * Converts from a PersistableBundle to a list of objects.
167      *
168      * @param <T> the type of the objects to convert from a PersistableBundle
169      * @param in the PersistableBundle containing the persisted list
170      * @param deserializer an implementation of the {@link Deserializer} functional interface that
171      *     builds the relevant type of objects.
172      */
173     @NonNull
toList( @onNull PersistableBundle in, @NonNull Deserializer<T> deserializer)174     public static <T> List<T> toList(
175             @NonNull PersistableBundle in, @NonNull Deserializer<T> deserializer) {
176         final int listLength = in.getInt(COLLECTION_SIZE_KEY);
177         final ArrayList<T> result = new ArrayList<>(listLength);
178 
179         for (int i = 0; i < listLength; i++) {
180             final String key = String.format(LIST_KEY_FORMAT, i);
181             final PersistableBundle item = in.getPersistableBundle(key);
182 
183             result.add(deserializer.fromPersistableBundle(item));
184         }
185         return result;
186     }
187 
188     // TODO: b/170513329 Delete #fromByteArray and #toByteArray once BaseBundle#putByteArray and
189     // BaseBundle#getByteArray are exposed.
190 
191     /**
192      * Converts a byte array to a PersistableBundle.
193      *
194      * <p>To avoid key collisions, NO additional key/value pairs should be added to the returned
195      * PersistableBundle object.
196      *
197      * @param array a byte array instance to persist
198      * @return the PersistableBundle instance
199      */
fromByteArray(byte[] array)200     public static PersistableBundle fromByteArray(byte[] array) {
201         final PersistableBundle result = new PersistableBundle();
202 
203         result.putString(BYTE_ARRAY_KEY, HexDump.toHexString(array));
204 
205         return result;
206     }
207 
208     /**
209      * Converts from a PersistableBundle to a byte array.
210      *
211      * @param bundle the PersistableBundle containing the byte array
212      * @return the byte array instance
213      */
toByteArray(PersistableBundle bundle)214     public static byte[] toByteArray(PersistableBundle bundle) {
215         Objects.requireNonNull(bundle, "PersistableBundle is null");
216 
217         String hex = bundle.getString(BYTE_ARRAY_KEY);
218         if (hex == null || hex.length() % 2 != 0) {
219             throw new IllegalArgumentException("PersistableBundle contains invalid byte array");
220         }
221 
222         return HexDump.hexStringToByteArray(hex);
223     }
224 
225     /**
226      * Converts from a Map of Persistable objects to a single PersistableBundle.
227      *
228      * <p>To avoid key collisions, NO additional key/value pairs should be added to the returned
229      * PersistableBundle object.
230      *
231      * @param <K> the type of the map-key to convert to the PersistableBundle
232      * @param <V> the type of the map-value to convert to the PersistableBundle
233      * @param in the Map of objects implementing the {@link Persistable} interface
234      * @param keySerializer an implementation of the {@link Serializer} functional interface that
235      *     converts a map-key of type T to a PersistableBundle
236      * @param valueSerializer an implementation of the {@link Serializer} functional interface that
237      *     converts a map-value of type E to a PersistableBundle
238      */
239     @NonNull
fromMap( @onNull Map<K, V> in, @NonNull Serializer<K> keySerializer, @NonNull Serializer<V> valueSerializer)240     public static <K, V> PersistableBundle fromMap(
241             @NonNull Map<K, V> in,
242             @NonNull Serializer<K> keySerializer,
243             @NonNull Serializer<V> valueSerializer) {
244         final PersistableBundle result = new PersistableBundle();
245 
246         result.putInt(COLLECTION_SIZE_KEY, in.size());
247         int i = 0;
248         for (Entry<K, V> entry : in.entrySet()) {
249             final String keyKey = String.format(MAP_KEY_FORMAT, i);
250             final String valueKey = String.format(MAP_VALUE_FORMAT, i);
251             result.putPersistableBundle(keyKey, keySerializer.toPersistableBundle(entry.getKey()));
252             result.putPersistableBundle(
253                     valueKey, valueSerializer.toPersistableBundle(entry.getValue()));
254 
255             i++;
256         }
257 
258         return result;
259     }
260 
261     /**
262      * Converts from a PersistableBundle to a Map of objects.
263      *
264      * <p>In an attempt to preserve ordering, the returned map will be a LinkedHashMap. However, the
265      * guarantees on the ordering can only ever be as strong as the map that was serialized in
266      * {@link fromMap()}. If the initial map that was serialized had no ordering guarantees, the
267      * deserialized map similarly may be of a non-deterministic order.
268      *
269      * @param <K> the type of the map-key to convert from a PersistableBundle
270      * @param <V> the type of the map-value to convert from a PersistableBundle
271      * @param in the PersistableBundle containing the persisted Map
272      * @param keyDeserializer an implementation of the {@link Deserializer} functional interface
273      *     that builds the relevant type of map-key.
274      * @param valueDeserializer an implementation of the {@link Deserializer} functional interface
275      *     that builds the relevant type of map-value.
276      * @return An instance of the parsed map as a LinkedHashMap (in an attempt to preserve
277      *     ordering).
278      */
279     @NonNull
toMap( @onNull PersistableBundle in, @NonNull Deserializer<K> keyDeserializer, @NonNull Deserializer<V> valueDeserializer)280     public static <K, V> LinkedHashMap<K, V> toMap(
281             @NonNull PersistableBundle in,
282             @NonNull Deserializer<K> keyDeserializer,
283             @NonNull Deserializer<V> valueDeserializer) {
284         final int mapSize = in.getInt(COLLECTION_SIZE_KEY);
285         final LinkedHashMap<K, V> result = new LinkedHashMap<>(mapSize);
286 
287         for (int i = 0; i < mapSize; i++) {
288             final String keyKey = String.format(MAP_KEY_FORMAT, i);
289             final String valueKey = String.format(MAP_VALUE_FORMAT, i);
290             final PersistableBundle keyBundle = in.getPersistableBundle(keyKey);
291             final PersistableBundle valueBundle = in.getPersistableBundle(valueKey);
292 
293             final K key = keyDeserializer.fromPersistableBundle(keyBundle);
294             final V value = valueDeserializer.fromPersistableBundle(valueBundle);
295             result.put(key, value);
296         }
297         return result;
298     }
299 
300     /**
301      * Converts a PersistableBundle into a disk-stable byte array format
302      *
303      * @param bundle the PersistableBundle to be converted to a disk-stable format
304      * @return the byte array representation of the PersistableBundle
305      */
306     @Nullable
toDiskStableBytes(@onNull PersistableBundle bundle)307     public static byte[] toDiskStableBytes(@NonNull PersistableBundle bundle) throws IOException {
308         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
309         bundle.writeToStream(outputStream);
310         return outputStream.toByteArray();
311     }
312 
313     /**
314      * Converts from a disk-stable byte array format to a PersistableBundle
315      *
316      * @param bytes the disk-stable byte array
317      * @return the PersistableBundle parsed from this byte array.
318      */
fromDiskStableBytes(@onNull byte[] bytes)319     public static PersistableBundle fromDiskStableBytes(@NonNull byte[] bytes) throws IOException {
320         final ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
321         return PersistableBundle.readFromStream(inputStream);
322     }
323 
324     /**
325      * Ensures safe reading and writing of {@link PersistableBundle}s to and from disk.
326      *
327      * <p>This class will enforce exclusion between reads and writes using the standard semantics of
328      * a ReadWriteLock. Specifically, concurrent readers ARE allowed, but reads/writes from/to the
329      * file are mutually exclusive. In other words, for an unbounded number n, the acceptable states
330      * are n readers, OR 1 writer (but not both).
331      */
332     public static class LockingReadWriteHelper {
333         private final ReadWriteLock mDiskLock = new ReentrantReadWriteLock();
334         private final String mPath;
335 
LockingReadWriteHelper(@onNull String path)336         public LockingReadWriteHelper(@NonNull String path) {
337             mPath = Objects.requireNonNull(path, "fileName was null");
338         }
339 
340         /**
341          * Reads the {@link PersistableBundle} from the disk.
342          *
343          * @return the PersistableBundle, if the file existed, or null otherwise
344          */
345         @Nullable
readFromDisk()346         public PersistableBundle readFromDisk() throws IOException {
347             try {
348                 mDiskLock.readLock().lock();
349                 final File file = new File(mPath);
350                 if (!file.exists()) {
351                     return null;
352                 }
353 
354                 try (FileInputStream fis = new FileInputStream(file)) {
355                     return PersistableBundle.readFromStream(fis);
356                 }
357             } finally {
358                 mDiskLock.readLock().unlock();
359             }
360         }
361 
362         /**
363          * Writes a {@link PersistableBundle} to disk.
364          *
365          * @param bundle the {@link PersistableBundle} to write to disk
366          */
writeToDisk(@onNull PersistableBundle bundle)367         public void writeToDisk(@NonNull PersistableBundle bundle) throws IOException {
368             Objects.requireNonNull(bundle, "bundle was null");
369 
370             try {
371                 mDiskLock.writeLock().lock();
372                 final File file = new File(mPath);
373                 if (!file.exists()) {
374                     file.getParentFile().mkdirs();
375                 }
376 
377                 try (FileOutputStream fos = new FileOutputStream(file)) {
378                     bundle.writeToStream(fos);
379                 }
380             } finally {
381                 mDiskLock.writeLock().unlock();
382             }
383         }
384     }
385 
386     /**
387      * Returns a copy of the persistable bundle with only the specified keys
388      *
389      * <p>This allows for holding minimized copies for memory-saving purposes.
390      */
391     @NonNull
minimizeBundle( @onNull PersistableBundle bundle, String... keys)392     public static PersistableBundle minimizeBundle(
393             @NonNull PersistableBundle bundle, String... keys) {
394         final PersistableBundle minimized = new PersistableBundle();
395 
396         if (bundle == null) {
397             return minimized;
398         }
399 
400         for (String key : keys) {
401             if (bundle.containsKey(key)) {
402                 final Object value = bundle.get(key);
403                 if (value == null) {
404                     continue;
405                 }
406 
407                 if (value instanceof Boolean) {
408                     minimized.putBoolean(key, (Boolean) value);
409                 } else if (value instanceof boolean[]) {
410                     minimized.putBooleanArray(key, (boolean[]) value);
411                 } else if (value instanceof Double) {
412                     minimized.putDouble(key, (Double) value);
413                 } else if (value instanceof double[]) {
414                     minimized.putDoubleArray(key, (double[]) value);
415                 } else if (value instanceof Integer) {
416                     minimized.putInt(key, (Integer) value);
417                 } else if (value instanceof int[]) {
418                     minimized.putIntArray(key, (int[]) value);
419                 } else if (value instanceof Long) {
420                     minimized.putLong(key, (Long) value);
421                 } else if (value instanceof long[]) {
422                     minimized.putLongArray(key, (long[]) value);
423                 } else if (value instanceof String) {
424                     minimized.putString(key, (String) value);
425                 } else if (value instanceof String[]) {
426                     minimized.putStringArray(key, (String[]) value);
427                 } else if (value instanceof PersistableBundle) {
428                     minimized.putPersistableBundle(key, (PersistableBundle) value);
429                 } else {
430                     continue;
431                 }
432             }
433         }
434 
435         return minimized;
436     }
437 
438     /** Builds a stable hashcode */
getHashCode(@ullable PersistableBundle bundle)439     public static int getHashCode(@Nullable PersistableBundle bundle) {
440         if (bundle == null) {
441             return -1;
442         }
443 
444         int iterativeHashcode = 0;
445         TreeSet<String> treeSet = new TreeSet<>(bundle.keySet());
446         for (String key : treeSet) {
447             Object val = bundle.get(key);
448             if (val instanceof PersistableBundle) {
449                 iterativeHashcode =
450                         Objects.hash(iterativeHashcode, key, getHashCode((PersistableBundle) val));
451             } else {
452                 iterativeHashcode = Objects.hash(iterativeHashcode, key, val);
453             }
454         }
455 
456         return iterativeHashcode;
457     }
458 
459     /** Checks for persistable bundle equality */
isEqual( @ullable PersistableBundle left, @Nullable PersistableBundle right)460     public static boolean isEqual(
461             @Nullable PersistableBundle left, @Nullable PersistableBundle right) {
462         // Check for pointer equality & null equality
463         if (Objects.equals(left, right)) {
464             return true;
465         }
466 
467         // If only one of the two is null, but not the other, not equal by definition.
468         if (Objects.isNull(left) != Objects.isNull(right)) {
469             return false;
470         }
471 
472         if (!left.keySet().equals(right.keySet())) {
473             return false;
474         }
475 
476         for (String key : left.keySet()) {
477             Object leftVal = left.get(key);
478             Object rightVal = right.get(key);
479 
480             // Check for equality
481             if (Objects.equals(leftVal, rightVal)) {
482                 continue;
483             } else if (Objects.isNull(leftVal) != Objects.isNull(rightVal)) {
484                 // If only one of the two is null, but not the other, not equal by definition.
485                 return false;
486             } else if (!Objects.equals(leftVal.getClass(), rightVal.getClass())) {
487                 // If classes are different, not equal by definition.
488                 return false;
489             }
490             if (leftVal instanceof PersistableBundle) {
491                 if (!isEqual((PersistableBundle) leftVal, (PersistableBundle) rightVal)) {
492                     return false;
493                 }
494             } else if (leftVal.getClass().isArray()) {
495                 if (leftVal instanceof boolean[]) {
496                     if (!Arrays.equals((boolean[]) leftVal, (boolean[]) rightVal)) {
497                         return false;
498                     }
499                 } else if (leftVal instanceof double[]) {
500                     if (!Arrays.equals((double[]) leftVal, (double[]) rightVal)) {
501                         return false;
502                     }
503                 } else if (leftVal instanceof int[]) {
504                     if (!Arrays.equals((int[]) leftVal, (int[]) rightVal)) {
505                         return false;
506                     }
507                 } else if (leftVal instanceof long[]) {
508                     if (!Arrays.equals((long[]) leftVal, (long[]) rightVal)) {
509                         return false;
510                     }
511                 } else if (!Arrays.equals((Object[]) leftVal, (Object[]) rightVal)) {
512                     return false;
513                 }
514             } else {
515                 if (!Objects.equals(leftVal, rightVal)) {
516                     return false;
517                 }
518             }
519         }
520 
521         return true;
522     }
523 
524     /**
525      * Wrapper class around PersistableBundles to allow equality comparisons
526      *
527      * <p>This class exposes the minimal getters to retrieve values.
528      */
529     public static class PersistableBundleWrapper {
530         @NonNull private final PersistableBundle mBundle;
531 
PersistableBundleWrapper(@onNull PersistableBundle bundle)532         public PersistableBundleWrapper(@NonNull PersistableBundle bundle) {
533             mBundle = Objects.requireNonNull(bundle, "Bundle was null");
534         }
535 
536         /**
537          * Retrieves the integer associated with the provided key.
538          *
539          * @param key the string key to query
540          * @param defaultValue the value to return if key does not exist
541          * @return the int value, or the default
542          */
getInt(String key, int defaultValue)543         public int getInt(String key, int defaultValue) {
544             return mBundle.getInt(key, defaultValue);
545         }
546 
547         /**
548          * Returns the value associated with the given key, or null if no mapping of the desired
549          * type exists for the given key or a null value is explicitly associated with the key.
550          *
551          * @param key a String, or null
552          * @param defaultValue the value to return if key does not exist
553          * @return an int[] value, or null
554          */
555         @Nullable
getIntArray(@ullable String key, @Nullable int[] defaultValue)556         public int[] getIntArray(@Nullable String key, @Nullable int[] defaultValue) {
557             final int[] value = mBundle.getIntArray(key);
558             return value == null ? defaultValue : value;
559         }
560 
561         @Override
hashCode()562         public int hashCode() {
563             return getHashCode(mBundle);
564         }
565 
566         @Override
equals(Object obj)567         public boolean equals(Object obj) {
568             if (!(obj instanceof PersistableBundleWrapper)) {
569                 return false;
570             }
571 
572             final PersistableBundleWrapper other = (PersistableBundleWrapper) obj;
573 
574             return isEqual(mBundle, other.mBundle);
575         }
576 
577         @Override
toString()578         public String toString() {
579             return mBundle.toString();
580         }
581     }
582 }
583