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