| /* |
| * 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. |
| * <p> |
| * 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. |
| * <p> |
| * Nuanced strategies for handling conflicting value definitions can be applied |
| * using {@link #setMergeStrategy(String, int)} and |
| * {@link #setDefaultMergeStrategy(int)}. |
| * <p> |
| * When conflicting values have <em>inconsistent</em> 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<String, Integer> 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. |
| * <p> |
| * 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)}. |
| * <p> |
| * 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<Bundle> 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<String> 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<Object>) first).compareTo(last) < 0 ? first : last; |
| } |
| |
| @SuppressWarnings("unchecked") |
| private static @NonNull Object comparableMax(@NonNull Object first, @NonNull Object last) { |
| return ((Comparable<Object>) 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<Object> firstList = (ArrayList<Object>) first; |
| final ArrayList<Object> lastList = (ArrayList<Object>) last; |
| final ArrayList<Object> res = new ArrayList<>(firstList.size() + lastList.size()); |
| res.addAll(firstList); |
| res.addAll(lastList); |
| return res; |
| } |
| |
| public static final @android.annotation.NonNull Parcelable.Creator<BundleMerger> CREATOR = |
| new Parcelable.Creator<BundleMerger>() { |
| @Override |
| public BundleMerger createFromParcel(Parcel in) { |
| return new BundleMerger(in); |
| } |
| |
| @Override |
| public BundleMerger[] newArray(int size) { |
| return new BundleMerger[size]; |
| } |
| }; |
| } |