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