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