• 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 {@link ArrayList} values by
123      * appending the last {@link ArrayList} after the first {@link ArrayList}.
124      */
125     public static final int STRATEGY_ARRAY_LIST_APPEND = 60;
126 
127     @IntDef(flag = false, prefix = { "STRATEGY_" }, value = {
128             STRATEGY_REJECT,
129             STRATEGY_FIRST,
130             STRATEGY_LAST,
131             STRATEGY_COMPARABLE_MIN,
132             STRATEGY_COMPARABLE_MAX,
133             STRATEGY_NUMBER_ADD,
134             STRATEGY_NUMBER_INCREMENT_FIRST,
135             STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD,
136             STRATEGY_BOOLEAN_AND,
137             STRATEGY_BOOLEAN_OR,
138             STRATEGY_ARRAY_APPEND,
139             STRATEGY_ARRAY_LIST_APPEND,
140     })
141     @Retention(RetentionPolicy.SOURCE)
142     public @interface Strategy {}
143 
144     /**
145      * Create a empty set of rules for merging two {@link Bundle} instances.
146      */
BundleMerger()147     public BundleMerger() {
148     }
149 
BundleMerger(@onNull Parcel in)150     private BundleMerger(@NonNull Parcel in) {
151         mDefaultStrategy = in.readInt();
152         final int N = in.readInt();
153         for (int i = 0; i < N; i++) {
154             mStrategies.put(in.readString(), in.readInt());
155         }
156     }
157 
158     @Override
writeToParcel(@onNull Parcel out, int flags)159     public void writeToParcel(@NonNull Parcel out, int flags) {
160         out.writeInt(mDefaultStrategy);
161         final int N = mStrategies.size();
162         out.writeInt(N);
163         for (int i = 0; i < N; i++) {
164             out.writeString(mStrategies.keyAt(i));
165             out.writeInt(mStrategies.valueAt(i));
166         }
167     }
168 
169     @Override
describeContents()170     public int describeContents() {
171         return 0;
172     }
173 
174     /**
175      * Configure the default merge strategy to be used when there isn't a
176      * more-specific strategy defined for a particular key via
177      * {@link #setMergeStrategy(String, int)}.
178      */
setDefaultMergeStrategy(@trategy int strategy)179     public void setDefaultMergeStrategy(@Strategy int strategy) {
180         mDefaultStrategy = strategy;
181     }
182 
183     /**
184      * Configure the merge strategy to be used for the given key.
185      * <p>
186      * Subsequent calls for the same key will overwrite any previously
187      * configured strategy.
188      */
setMergeStrategy(@onNull String key, @Strategy int strategy)189     public void setMergeStrategy(@NonNull String key, @Strategy int strategy) {
190         mStrategies.put(key, strategy);
191     }
192 
193     /**
194      * Return the merge strategy to be used for the given key, as defined by
195      * {@link #setMergeStrategy(String, int)}.
196      * <p>
197      * If no specific strategy has been configured for the given key, this
198      * returns {@link #setDefaultMergeStrategy(int)}.
199      */
getMergeStrategy(@onNull String key)200     public @Strategy int getMergeStrategy(@NonNull String key) {
201         return (int) mStrategies.getOrDefault(key, mDefaultStrategy);
202     }
203 
204     /**
205      * Return a {@link BinaryOperator} which applies the strategies configured
206      * in this object to merge the two given {@link Bundle} arguments.
207      */
asBinaryOperator()208     public BinaryOperator<Bundle> asBinaryOperator() {
209         return this::merge;
210     }
211 
212     /**
213      * Apply the strategies configured in this object to merge the two given
214      * {@link Bundle} arguments.
215      *
216      * @return the merged {@link Bundle} result. If one argument is {@code null}
217      *         it will return the other argument. If both arguments are null it
218      *         will return {@code null}.
219      */
220     @SuppressWarnings("deprecation")
merge(@ullable Bundle first, @Nullable Bundle last)221     public @Nullable Bundle merge(@Nullable Bundle first, @Nullable Bundle last) {
222         if (first == null && last == null) {
223             return null;
224         }
225         if (first == null) {
226             first = Bundle.EMPTY;
227         }
228         if (last == null) {
229             last = Bundle.EMPTY;
230         }
231 
232         // Start by bulk-copying all values without attempting to unpack any
233         // custom parcelables; we'll circle back to handle conflicts below
234         final Bundle res = new Bundle();
235         res.putAll(first);
236         res.putAll(last);
237 
238         final ArraySet<String> conflictingKeys = new ArraySet<>();
239         conflictingKeys.addAll(first.keySet());
240         conflictingKeys.retainAll(last.keySet());
241         for (int i = 0; i < conflictingKeys.size(); i++) {
242             final String key = conflictingKeys.valueAt(i);
243             final int strategy = getMergeStrategy(key);
244             final Object firstValue = first.get(key);
245             final Object lastValue = last.get(key);
246             try {
247                 res.putObject(key, merge(strategy, firstValue, lastValue));
248             } catch (Exception e) {
249                 Log.w(TAG, "Failed to merge key " + key + " with " + firstValue + " and "
250                         + lastValue + " using strategy " + strategy, e);
251             }
252         }
253         return res;
254     }
255 
256     /**
257      * Merge the two given values. If only one of the values is defined, it
258      * always wins, otherwise the given strategy is applied.
259      *
260      * @hide
261      */
262     @VisibleForTesting
merge(@trategy int strategy, @Nullable Object first, @Nullable Object last)263     public static @Nullable Object merge(@Strategy int strategy,
264             @Nullable Object first, @Nullable Object last) {
265         if (first == null) return last;
266         if (last == null) return first;
267 
268         if (first.getClass() != last.getClass()) {
269             throw new IllegalArgumentException("Merging requires consistent classes; first "
270                     + first.getClass() + " last " + last.getClass());
271         }
272 
273         switch (strategy) {
274             case STRATEGY_REJECT:
275                 // Only actually reject when the values are different
276                 if (Objects.deepEquals(first, last)) {
277                     return first;
278                 } else {
279                     return null;
280                 }
281             case STRATEGY_FIRST:
282                 return first;
283             case STRATEGY_LAST:
284                 return last;
285             case STRATEGY_COMPARABLE_MIN:
286                 return comparableMin(first, last);
287             case STRATEGY_COMPARABLE_MAX:
288                 return comparableMax(first, last);
289             case STRATEGY_NUMBER_ADD:
290                 return numberAdd(first, last);
291             case STRATEGY_NUMBER_INCREMENT_FIRST:
292                 return numberIncrementFirst(first, last);
293             case STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD:
294                 return numberAdd(numberIncrementFirst(first, last), last);
295             case STRATEGY_BOOLEAN_AND:
296                 return booleanAnd(first, last);
297             case STRATEGY_BOOLEAN_OR:
298                 return booleanOr(first, last);
299             case STRATEGY_ARRAY_APPEND:
300                 return arrayAppend(first, last);
301             case STRATEGY_ARRAY_LIST_APPEND:
302                 return arrayListAppend(first, last);
303             default:
304                 throw new UnsupportedOperationException();
305         }
306     }
307 
308     @SuppressWarnings("unchecked")
comparableMin(@onNull Object first, @NonNull Object last)309     private static @NonNull Object comparableMin(@NonNull Object first, @NonNull Object last) {
310         return ((Comparable<Object>) first).compareTo(last) < 0 ? first : last;
311     }
312 
313     @SuppressWarnings("unchecked")
comparableMax(@onNull Object first, @NonNull Object last)314     private static @NonNull Object comparableMax(@NonNull Object first, @NonNull Object last) {
315         return ((Comparable<Object>) first).compareTo(last) >= 0 ? first : last;
316     }
317 
numberAdd(@onNull Object first, @NonNull Object last)318     private static @NonNull Object numberAdd(@NonNull Object first, @NonNull Object last) {
319         if (first instanceof Integer) {
320             return ((Integer) first) + ((Integer) last);
321         } else if (first instanceof Long) {
322             return ((Long) first) + ((Long) last);
323         } else if (first instanceof Float) {
324             return ((Float) first) + ((Float) last);
325         } else if (first instanceof Double) {
326             return ((Double) first) + ((Double) last);
327         } else {
328             throw new IllegalArgumentException("Unable to add " + first.getClass());
329         }
330     }
331 
numberIncrementFirst(@onNull Object first, @NonNull Object last)332     private static @NonNull Number numberIncrementFirst(@NonNull Object first,
333             @NonNull Object last) {
334         if (first instanceof Integer) {
335             return ((Integer) first) + 1;
336         } else if (first instanceof Long) {
337             return ((Long) first) + 1L;
338         } else {
339             throw new IllegalArgumentException("Unable to add " + first.getClass());
340         }
341     }
342 
booleanAnd(@onNull Object first, @NonNull Object last)343     private static @NonNull Object booleanAnd(@NonNull Object first, @NonNull Object last) {
344         return ((Boolean) first) && ((Boolean) last);
345     }
346 
booleanOr(@onNull Object first, @NonNull Object last)347     private static @NonNull Object booleanOr(@NonNull Object first, @NonNull Object last) {
348         return ((Boolean) first) || ((Boolean) last);
349     }
350 
arrayAppend(@onNull Object first, @NonNull Object last)351     private static @NonNull Object arrayAppend(@NonNull Object first, @NonNull Object last) {
352         if (!first.getClass().isArray()) {
353             throw new IllegalArgumentException("Unable to append " + first.getClass());
354         }
355         final Class<?> clazz = first.getClass().getComponentType();
356         final int firstLength = Array.getLength(first);
357         final int lastLength = Array.getLength(last);
358         final Object res = Array.newInstance(clazz, firstLength + lastLength);
359         System.arraycopy(first, 0, res, 0, firstLength);
360         System.arraycopy(last, 0, res, firstLength, lastLength);
361         return res;
362     }
363 
364     @SuppressWarnings("unchecked")
arrayListAppend(@onNull Object first, @NonNull Object last)365     private static @NonNull Object arrayListAppend(@NonNull Object first, @NonNull Object last) {
366         if (!(first instanceof ArrayList)) {
367             throw new IllegalArgumentException("Unable to append " + first.getClass());
368         }
369         final ArrayList<Object> firstList = (ArrayList<Object>) first;
370         final ArrayList<Object> lastList = (ArrayList<Object>) last;
371         final ArrayList<Object> res = new ArrayList<>(firstList.size() + lastList.size());
372         res.addAll(firstList);
373         res.addAll(lastList);
374         return res;
375     }
376 
377     public static final @android.annotation.NonNull Parcelable.Creator<BundleMerger> CREATOR =
378             new Parcelable.Creator<BundleMerger>() {
379                 @Override
380                 public BundleMerger createFromParcel(Parcel in) {
381                     return new BundleMerger(in);
382                 }
383 
384                 @Override
385                 public BundleMerger[] newArray(int size) {
386                     return new BundleMerger[size];
387                 }
388             };
389 }
390