/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.os; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Objects; import java.util.function.BinaryOperator; /** * Configured rules for merging two {@link Bundle} instances. *

* By default, values from both {@link Bundle} instances are blended together on * a key-wise basis, and conflicting value definitions for a key are dropped. *

* Nuanced strategies for handling conflicting value definitions can be applied * using {@link #setMergeStrategy(String, int)} and * {@link #setDefaultMergeStrategy(int)}. *

* When conflicting values have inconsistent data types (such as trying * to merge a {@link String} and a {@link Integer}), both conflicting values are * rejected and the key becomes undefined, regardless of the requested strategy. * * @hide */ @android.ravenwood.annotation.RavenwoodKeepWholeClass public class BundleMerger implements Parcelable { private static final String TAG = "BundleMerger"; private @Strategy int mDefaultStrategy = STRATEGY_REJECT; private final ArrayMap mStrategies = new ArrayMap<>(); /** * Merge strategy that rejects both conflicting values. */ public static final int STRATEGY_REJECT = 0; /** * Merge strategy that selects the first of conflicting values. */ public static final int STRATEGY_FIRST = 1; /** * Merge strategy that selects the last of conflicting values. */ public static final int STRATEGY_LAST = 2; /** * Merge strategy that selects the "minimum" of conflicting values which are * {@link Comparable} with each other. */ public static final int STRATEGY_COMPARABLE_MIN = 3; /** * Merge strategy that selects the "maximum" of conflicting values which are * {@link Comparable} with each other. */ public static final int STRATEGY_COMPARABLE_MAX = 4; /** * Merge strategy that numerically adds both conflicting values. */ public static final int STRATEGY_NUMBER_ADD = 10; /** * Merge strategy that numerically increments the first conflicting value by * {@code 1} and ignores the last conflicting value. */ public static final int STRATEGY_NUMBER_INCREMENT_FIRST = 20; /** * Merge strategy that numerically increments the first conflicting value by * {@code 1} and also numerically adds both conflicting values. */ public static final int STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD = 25; /** * Merge strategy that combines conflicting values using a boolean "and" * operation. */ public static final int STRATEGY_BOOLEAN_AND = 30; /** * Merge strategy that combines conflicting values using a boolean "or" * operation. */ public static final int STRATEGY_BOOLEAN_OR = 40; /** * Merge strategy that combines two conflicting array values by appending * the last array after the first array. */ public static final int STRATEGY_ARRAY_APPEND = 50; /** * Merge strategy that combines two conflicting {@link ArrayList} values by * appending the last {@link ArrayList} after the first {@link ArrayList}. */ public static final int STRATEGY_ARRAY_LIST_APPEND = 60; @IntDef(flag = false, prefix = { "STRATEGY_" }, value = { STRATEGY_REJECT, STRATEGY_FIRST, STRATEGY_LAST, STRATEGY_COMPARABLE_MIN, STRATEGY_COMPARABLE_MAX, STRATEGY_NUMBER_ADD, STRATEGY_NUMBER_INCREMENT_FIRST, STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD, STRATEGY_BOOLEAN_AND, STRATEGY_BOOLEAN_OR, STRATEGY_ARRAY_APPEND, STRATEGY_ARRAY_LIST_APPEND, }) @Retention(RetentionPolicy.SOURCE) public @interface Strategy {} /** * Create a empty set of rules for merging two {@link Bundle} instances. */ public BundleMerger() { } private BundleMerger(@NonNull Parcel in) { mDefaultStrategy = in.readInt(); final int N = in.readInt(); for (int i = 0; i < N; i++) { mStrategies.put(in.readString(), in.readInt()); } } @Override public void writeToParcel(@NonNull Parcel out, int flags) { out.writeInt(mDefaultStrategy); final int N = mStrategies.size(); out.writeInt(N); for (int i = 0; i < N; i++) { out.writeString(mStrategies.keyAt(i)); out.writeInt(mStrategies.valueAt(i)); } } @Override public int describeContents() { return 0; } /** * Configure the default merge strategy to be used when there isn't a * more-specific strategy defined for a particular key via * {@link #setMergeStrategy(String, int)}. */ public void setDefaultMergeStrategy(@Strategy int strategy) { mDefaultStrategy = strategy; } /** * Configure the merge strategy to be used for the given key. *

* Subsequent calls for the same key will overwrite any previously * configured strategy. */ public void setMergeStrategy(@NonNull String key, @Strategy int strategy) { mStrategies.put(key, strategy); } /** * Return the merge strategy to be used for the given key, as defined by * {@link #setMergeStrategy(String, int)}. *

* If no specific strategy has been configured for the given key, this * returns {@link #setDefaultMergeStrategy(int)}. */ public @Strategy int getMergeStrategy(@NonNull String key) { return (int) mStrategies.getOrDefault(key, mDefaultStrategy); } /** * Return a {@link BinaryOperator} which applies the strategies configured * in this object to merge the two given {@link Bundle} arguments. */ public BinaryOperator asBinaryOperator() { return this::merge; } /** * Apply the strategies configured in this object to merge the two given * {@link Bundle} arguments. * * @return the merged {@link Bundle} result. If one argument is {@code null} * it will return the other argument. If both arguments are null it * will return {@code null}. */ @SuppressWarnings("deprecation") public @Nullable Bundle merge(@Nullable Bundle first, @Nullable Bundle last) { if (first == null && last == null) { return null; } if (first == null) { first = Bundle.EMPTY; } if (last == null) { last = Bundle.EMPTY; } // Start by bulk-copying all values without attempting to unpack any // custom parcelables; we'll circle back to handle conflicts below final Bundle res = new Bundle(); res.putAll(first); res.putAll(last); final ArraySet conflictingKeys = new ArraySet<>(); conflictingKeys.addAll(first.keySet()); conflictingKeys.retainAll(last.keySet()); for (int i = 0; i < conflictingKeys.size(); i++) { final String key = conflictingKeys.valueAt(i); final int strategy = getMergeStrategy(key); final Object firstValue = first.get(key); final Object lastValue = last.get(key); try { res.putObject(key, merge(strategy, firstValue, lastValue)); } catch (Exception e) { Log.w(TAG, "Failed to merge key " + key + " with " + firstValue + " and " + lastValue + " using strategy " + strategy, e); } } return res; } /** * Merge the two given values. If only one of the values is defined, it * always wins, otherwise the given strategy is applied. * * @hide */ @VisibleForTesting public static @Nullable Object merge(@Strategy int strategy, @Nullable Object first, @Nullable Object last) { if (first == null) return last; if (last == null) return first; if (first.getClass() != last.getClass()) { throw new IllegalArgumentException("Merging requires consistent classes; first " + first.getClass() + " last " + last.getClass()); } switch (strategy) { case STRATEGY_REJECT: // Only actually reject when the values are different if (Objects.deepEquals(first, last)) { return first; } else { return null; } case STRATEGY_FIRST: return first; case STRATEGY_LAST: return last; case STRATEGY_COMPARABLE_MIN: return comparableMin(first, last); case STRATEGY_COMPARABLE_MAX: return comparableMax(first, last); case STRATEGY_NUMBER_ADD: return numberAdd(first, last); case STRATEGY_NUMBER_INCREMENT_FIRST: return numberIncrementFirst(first, last); case STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD: return numberAdd(numberIncrementFirst(first, last), last); case STRATEGY_BOOLEAN_AND: return booleanAnd(first, last); case STRATEGY_BOOLEAN_OR: return booleanOr(first, last); case STRATEGY_ARRAY_APPEND: return arrayAppend(first, last); case STRATEGY_ARRAY_LIST_APPEND: return arrayListAppend(first, last); default: throw new UnsupportedOperationException(); } } @SuppressWarnings("unchecked") private static @NonNull Object comparableMin(@NonNull Object first, @NonNull Object last) { return ((Comparable) first).compareTo(last) < 0 ? first : last; } @SuppressWarnings("unchecked") private static @NonNull Object comparableMax(@NonNull Object first, @NonNull Object last) { return ((Comparable) first).compareTo(last) >= 0 ? first : last; } private static @NonNull Object numberAdd(@NonNull Object first, @NonNull Object last) { if (first instanceof Integer) { return ((Integer) first) + ((Integer) last); } else if (first instanceof Long) { return ((Long) first) + ((Long) last); } else if (first instanceof Float) { return ((Float) first) + ((Float) last); } else if (first instanceof Double) { return ((Double) first) + ((Double) last); } else { throw new IllegalArgumentException("Unable to add " + first.getClass()); } } private static @NonNull Number numberIncrementFirst(@NonNull Object first, @NonNull Object last) { if (first instanceof Integer) { return ((Integer) first) + 1; } else if (first instanceof Long) { return ((Long) first) + 1L; } else { throw new IllegalArgumentException("Unable to add " + first.getClass()); } } private static @NonNull Object booleanAnd(@NonNull Object first, @NonNull Object last) { return ((Boolean) first) && ((Boolean) last); } private static @NonNull Object booleanOr(@NonNull Object first, @NonNull Object last) { return ((Boolean) first) || ((Boolean) last); } private static @NonNull Object arrayAppend(@NonNull Object first, @NonNull Object last) { if (!first.getClass().isArray()) { throw new IllegalArgumentException("Unable to append " + first.getClass()); } final Class clazz = first.getClass().getComponentType(); final int firstLength = Array.getLength(first); final int lastLength = Array.getLength(last); final Object res = Array.newInstance(clazz, firstLength + lastLength); System.arraycopy(first, 0, res, 0, firstLength); System.arraycopy(last, 0, res, firstLength, lastLength); return res; } @SuppressWarnings("unchecked") private static @NonNull Object arrayListAppend(@NonNull Object first, @NonNull Object last) { if (!(first instanceof ArrayList)) { throw new IllegalArgumentException("Unable to append " + first.getClass()); } final ArrayList firstList = (ArrayList) first; final ArrayList lastList = (ArrayList) last; final ArrayList res = new ArrayList<>(firstList.size() + lastList.size()); res.addAll(firstList); res.addAll(lastList); return res; } public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public BundleMerger createFromParcel(Parcel in) { return new BundleMerger(in); } @Override public BundleMerger[] newArray(int size) { return new BundleMerger[size]; } }; }