/*
* 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