• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 android.os;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.util.ArrayMap;
23 import android.util.ArraySet;
24 import android.util.Log;
25 
26 import com.android.internal.annotations.VisibleForTesting;
27 
28 import java.lang.annotation.Retention;
29 import java.lang.annotation.RetentionPolicy;
30 import java.lang.reflect.Array;
31 import java.util.ArrayList;
32 import java.util.Objects;
33 import java.util.function.BinaryOperator;
34 
35 /**
36  * Configured rules for merging two {@link Bundle} instances.
37  * <p>
38  * By default, values from both {@link Bundle} instances are blended together on
39  * a key-wise basis, and conflicting value definitions for a key are dropped.
40  * <p>
41  * Nuanced strategies for handling conflicting value definitions can be applied
42  * using {@link #setMergeStrategy(String, int)} and
43  * {@link #setDefaultMergeStrategy(int)}.
44  * <p>
45  * When conflicting values have <em>inconsistent</em> data types (such as trying
46  * to merge a {@link String} and a {@link Integer}), both conflicting values are
47  * rejected and the key becomes undefined, regardless of the requested strategy.
48  *
49  * @hide
50  */
51 @android.ravenwood.annotation.RavenwoodKeepWholeClass
52 public class BundleMerger implements Parcelable {
53     private static final String TAG = "BundleMerger";
54 
55     private @Strategy int mDefaultStrategy = STRATEGY_REJECT;
56 
57     private final ArrayMap<String, Integer> mStrategies = new ArrayMap<>();
58 
59     /**
60      * Merge strategy that rejects both conflicting values.
61      */
62     public static final int STRATEGY_REJECT = 0;
63 
64     /**
65      * Merge strategy that selects the first of conflicting values.
66      */
67     public static final int STRATEGY_FIRST = 1;
68 
69     /**
70      * Merge strategy that selects the last of conflicting values.
71      */
72     public static final int STRATEGY_LAST = 2;
73 
74     /**
75      * Merge strategy that selects the "minimum" of conflicting values which are
76      * {@link Comparable} with each other.
77      */
78     public static final int STRATEGY_COMPARABLE_MIN = 3;
79 
80     /**
81      * Merge strategy that selects the "maximum" of conflicting values which are
82      * {@link Comparable} with each other.
83      */
84     public static final int STRATEGY_COMPARABLE_MAX = 4;
85 
86     /**
87      * Merge strategy that numerically adds both conflicting values.
88      */
89     public static final int STRATEGY_NUMBER_ADD = 10;
90 
91     /**
92      * Merge strategy that numerically increments the first conflicting value by
93      * {@code 1} and ignores the last conflicting value.
94      */
95     public static final int STRATEGY_NUMBER_INCREMENT_FIRST = 20;
96 
97     /**
98      * Merge strategy that numerically increments the first conflicting value by
99      * {@code 1} and also numerically adds both conflicting values.
100      */
101     public static final int STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD = 25;
102 
103     /**
104      * Merge strategy that combines conflicting values using a boolean "and"
105      * operation.
106      */
107     public static final int STRATEGY_BOOLEAN_AND = 30;
108 
109     /**
110      * Merge strategy that combines conflicting values using a boolean "or"
111      * operation.
112      */
113     public static final int STRATEGY_BOOLEAN_OR = 40;
114 
115     /**
116      * Merge strategy that combines two conflicting array values by appending
117      * the last array after the first array.
118      */
119     public static final int STRATEGY_ARRAY_APPEND = 50;
120 
121     /**
122      * Merge strategy that combines two conflicting array values by creating a new array
123      * containing all unique elements from both arrays.
124      */
125     public static final int STRATEGY_ARRAY_UNION = 55;
126 
127     /**
128      * Merge strategy that combines two conflicting {@link ArrayList} values by
129      * appending the last {@link ArrayList} after the first {@link ArrayList}.
130      */
131     public static final int STRATEGY_ARRAY_LIST_APPEND = 60;
132 
133     /**
134      * Merge strategy that combines two conflicting {@link String} values by
135      * appending the last {@link String} after the first {@link String}.
136      */
137     public static final int STRATEGY_STRING_APPEND = 70;
138 
139     @IntDef(flag = false, prefix = { "STRATEGY_" }, value = {
140             STRATEGY_REJECT,
141             STRATEGY_FIRST,
142             STRATEGY_LAST,
143             STRATEGY_COMPARABLE_MIN,
144             STRATEGY_COMPARABLE_MAX,
145             STRATEGY_NUMBER_ADD,
146             STRATEGY_NUMBER_INCREMENT_FIRST,
147             STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD,
148             STRATEGY_BOOLEAN_AND,
149             STRATEGY_BOOLEAN_OR,
150             STRATEGY_ARRAY_APPEND,
151             STRATEGY_ARRAY_UNION,
152             STRATEGY_ARRAY_LIST_APPEND,
153             STRATEGY_STRING_APPEND,
154     })
155     @Retention(RetentionPolicy.SOURCE)
156     public @interface Strategy {}
157 
158     /**
159      * Create a empty set of rules for merging two {@link Bundle} instances.
160      */
BundleMerger()161     public BundleMerger() {
162     }
163 
BundleMerger(@onNull Parcel in)164     private BundleMerger(@NonNull Parcel in) {
165         mDefaultStrategy = in.readInt();
166         final int N = in.readInt();
167         for (int i = 0; i < N; i++) {
168             mStrategies.put(in.readString(), in.readInt());
169         }
170     }
171 
172     @Override
writeToParcel(@onNull Parcel out, int flags)173     public void writeToParcel(@NonNull Parcel out, int flags) {
174         out.writeInt(mDefaultStrategy);
175         final int N = mStrategies.size();
176         out.writeInt(N);
177         for (int i = 0; i < N; i++) {
178             out.writeString(mStrategies.keyAt(i));
179             out.writeInt(mStrategies.valueAt(i));
180         }
181     }
182 
183     @Override
describeContents()184     public int describeContents() {
185         return 0;
186     }
187 
188     /**
189      * Configure the default merge strategy to be used when there isn't a
190      * more-specific strategy defined for a particular key via
191      * {@link #setMergeStrategy(String, int)}.
192      */
setDefaultMergeStrategy(@trategy int strategy)193     public void setDefaultMergeStrategy(@Strategy int strategy) {
194         mDefaultStrategy = strategy;
195     }
196 
197     /**
198      * Configure the merge strategy to be used for the given key.
199      * <p>
200      * Subsequent calls for the same key will overwrite any previously
201      * configured strategy.
202      */
setMergeStrategy(@onNull String key, @Strategy int strategy)203     public void setMergeStrategy(@NonNull String key, @Strategy int strategy) {
204         mStrategies.put(key, strategy);
205     }
206 
207     /**
208      * Return the merge strategy to be used for the given key, as defined by
209      * {@link #setMergeStrategy(String, int)}.
210      * <p>
211      * If no specific strategy has been configured for the given key, this
212      * returns {@link #setDefaultMergeStrategy(int)}.
213      */
getMergeStrategy(@onNull String key)214     public @Strategy int getMergeStrategy(@NonNull String key) {
215         return (int) mStrategies.getOrDefault(key, mDefaultStrategy);
216     }
217 
218     /**
219      * Return a {@link BinaryOperator} which applies the strategies configured
220      * in this object to merge the two given {@link Bundle} arguments.
221      */
asBinaryOperator()222     public BinaryOperator<Bundle> asBinaryOperator() {
223         return this::merge;
224     }
225 
226     /**
227      * Apply the strategies configured in this object to merge the two given
228      * {@link Bundle} arguments.
229      *
230      * @return the merged {@link Bundle} result. If one argument is {@code null}
231      *         it will return the other argument. If both arguments are null it
232      *         will return {@code null}.
233      */
234     @SuppressWarnings("deprecation")
merge(@ullable Bundle first, @Nullable Bundle last)235     public @Nullable Bundle merge(@Nullable Bundle first, @Nullable Bundle last) {
236         if (first == null && last == null) {
237             return null;
238         }
239         if (first == null) {
240             first = Bundle.EMPTY;
241         }
242         if (last == null) {
243             last = Bundle.EMPTY;
244         }
245 
246         // Start by bulk-copying all values without attempting to unpack any
247         // custom parcelables; we'll circle back to handle conflicts below
248         final Bundle res = new Bundle();
249         res.putAll(first);
250         res.putAll(last);
251 
252         final ArraySet<String> conflictingKeys = new ArraySet<>();
253         conflictingKeys.addAll(first.keySet());
254         conflictingKeys.retainAll(last.keySet());
255         for (int i = 0; i < conflictingKeys.size(); i++) {
256             final String key = conflictingKeys.valueAt(i);
257             final int strategy = getMergeStrategy(key);
258             final Object firstValue = first.get(key);
259             final Object lastValue = last.get(key);
260             try {
261                 res.putObject(key, merge(strategy, firstValue, lastValue));
262             } catch (Exception e) {
263                 Log.w(TAG, "Failed to merge key " + key + " with " + firstValue + " and "
264                         + lastValue + " using strategy " + strategy, e);
265             }
266         }
267         return res;
268     }
269 
270     /**
271      * Merge the two given values. If only one of the values is defined, it
272      * always wins, otherwise the given strategy is applied.
273      *
274      * @hide
275      */
276     @VisibleForTesting
merge(@trategy int strategy, @Nullable Object first, @Nullable Object last)277     public static @Nullable Object merge(@Strategy int strategy,
278             @Nullable Object first, @Nullable Object last) {
279         if (first == null) return last;
280         if (last == null) return first;
281 
282         if (first.getClass() != last.getClass()) {
283             throw new IllegalArgumentException("Merging requires consistent classes; first "
284                     + first.getClass() + " last " + last.getClass());
285         }
286 
287         switch (strategy) {
288             case STRATEGY_REJECT:
289                 // Only actually reject when the values are different
290                 if (Objects.deepEquals(first, last)) {
291                     return first;
292                 } else {
293                     return null;
294                 }
295             case STRATEGY_FIRST:
296                 return first;
297             case STRATEGY_LAST:
298                 return last;
299             case STRATEGY_COMPARABLE_MIN:
300                 return comparableMin(first, last);
301             case STRATEGY_COMPARABLE_MAX:
302                 return comparableMax(first, last);
303             case STRATEGY_NUMBER_ADD:
304                 return numberAdd(first, last);
305             case STRATEGY_NUMBER_INCREMENT_FIRST:
306                 return numberIncrementFirst(first, last);
307             case STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD:
308                 return numberAdd(numberIncrementFirst(first, last), last);
309             case STRATEGY_BOOLEAN_AND:
310                 return booleanAnd(first, last);
311             case STRATEGY_BOOLEAN_OR:
312                 return booleanOr(first, last);
313             case STRATEGY_ARRAY_APPEND:
314                 return arrayAppend(first, last);
315             case STRATEGY_ARRAY_UNION:
316                 return arrayUnion(first, last);
317             case STRATEGY_ARRAY_LIST_APPEND:
318                 return arrayListAppend(first, last);
319             case STRATEGY_STRING_APPEND:
320                 return stringAppend(first, last);
321             default:
322                 throw new UnsupportedOperationException();
323         }
324     }
325 
326     @SuppressWarnings("unchecked")
comparableMin(@onNull Object first, @NonNull Object last)327     private static @NonNull Object comparableMin(@NonNull Object first, @NonNull Object last) {
328         return ((Comparable<Object>) first).compareTo(last) < 0 ? first : last;
329     }
330 
331     @SuppressWarnings("unchecked")
comparableMax(@onNull Object first, @NonNull Object last)332     private static @NonNull Object comparableMax(@NonNull Object first, @NonNull Object last) {
333         return ((Comparable<Object>) first).compareTo(last) >= 0 ? first : last;
334     }
335 
numberAdd(@onNull Object first, @NonNull Object last)336     private static @NonNull Object numberAdd(@NonNull Object first, @NonNull Object last) {
337         if (first instanceof Integer) {
338             return ((Integer) first) + ((Integer) last);
339         } else if (first instanceof Long) {
340             return ((Long) first) + ((Long) last);
341         } else if (first instanceof Float) {
342             return ((Float) first) + ((Float) last);
343         } else if (first instanceof Double) {
344             return ((Double) first) + ((Double) last);
345         } else {
346             throw new IllegalArgumentException("Unable to add " + first.getClass());
347         }
348     }
349 
numberIncrementFirst(@onNull Object first, @NonNull Object last)350     private static @NonNull Number numberIncrementFirst(@NonNull Object first,
351             @NonNull Object last) {
352         if (first instanceof Integer) {
353             return ((Integer) first) + 1;
354         } else if (first instanceof Long) {
355             return ((Long) first) + 1L;
356         } else {
357             throw new IllegalArgumentException("Unable to add " + first.getClass());
358         }
359     }
360 
booleanAnd(@onNull Object first, @NonNull Object last)361     private static @NonNull Object booleanAnd(@NonNull Object first, @NonNull Object last) {
362         return ((Boolean) first) && ((Boolean) last);
363     }
364 
booleanOr(@onNull Object first, @NonNull Object last)365     private static @NonNull Object booleanOr(@NonNull Object first, @NonNull Object last) {
366         return ((Boolean) first) || ((Boolean) last);
367     }
368 
arrayAppend(@onNull Object first, @NonNull Object last)369     private static @NonNull Object arrayAppend(@NonNull Object first, @NonNull Object last) {
370         if (!first.getClass().isArray()) {
371             throw new IllegalArgumentException("Unable to append " + first.getClass());
372         }
373         final Class<?> clazz = first.getClass().getComponentType();
374         final int firstLength = Array.getLength(first);
375         final int lastLength = Array.getLength(last);
376         final Object res = Array.newInstance(clazz, firstLength + lastLength);
377         System.arraycopy(first, 0, res, 0, firstLength);
378         System.arraycopy(last, 0, res, firstLength, lastLength);
379         return res;
380     }
381 
arrayUnion(@onNull Object first, @NonNull Object last)382     private static @NonNull Object arrayUnion(@NonNull Object first, @NonNull Object last) {
383         if (!first.getClass().isArray()) {
384             throw new IllegalArgumentException("Unable to union " + first.getClass());
385         }
386         final int firstLength = Array.getLength(first);
387         final int lastLength = Array.getLength(last);
388         final ArrayList<Object> list = new ArrayList<>(firstLength + lastLength);
389         final ArraySet<Object> set = new ArraySet<>();
390         for (int i = 0; i < firstLength; i++) {
391             set.add(Array.get(first, i));
392         }
393         for (int i = 0; i < lastLength; i++) {
394             set.add(Array.get(last, i));
395         }
396         final Class<?> clazz = first.getClass().getComponentType();
397         final int setSize = set.size();
398         final Object res = Array.newInstance(clazz, setSize);
399         for (int i = 0; i < setSize; i++) {
400             Array.set(res, i, set.valueAt(i));
401         }
402         return res;
403     }
404 
405     @SuppressWarnings("unchecked")
arrayListAppend(@onNull Object first, @NonNull Object last)406     private static @NonNull Object arrayListAppend(@NonNull Object first, @NonNull Object last) {
407         if (!(first instanceof ArrayList)) {
408             throw new IllegalArgumentException("Unable to append " + first.getClass());
409         }
410         final ArrayList<Object> firstList = (ArrayList<Object>) first;
411         final ArrayList<Object> lastList = (ArrayList<Object>) last;
412         final ArrayList<Object> res = new ArrayList<>(firstList.size() + lastList.size());
413         res.addAll(firstList);
414         res.addAll(lastList);
415         return res;
416     }
417 
stringAppend(@onNull Object first, @NonNull Object last)418     private static @NonNull Object stringAppend(@NonNull Object first, @NonNull Object last) {
419         if (!(first instanceof String)) {
420             throw new IllegalArgumentException("Unable to append " + first.getClass());
421         }
422         return ((String) first) + ((String) last);
423     }
424 
425     public static final @android.annotation.NonNull Parcelable.Creator<BundleMerger> CREATOR =
426             new Parcelable.Creator<BundleMerger>() {
427                 @Override
428                 public BundleMerger createFromParcel(Parcel in) {
429                     return new BundleMerger(in);
430                 }
431 
432                 @Override
433                 public BundleMerger[] newArray(int size) {
434                     return new BundleMerger[size];
435                 }
436             };
437 }
438