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