304 lines
13 KiB
Java
304 lines
13 KiB
Java
/*
|
|
* Copyright 2020 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.app.appsearch.util;
|
|
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.app.appsearch.safeparcel.SafeParcelable;
|
|
import android.app.appsearch.safeparcel.SafeParcelableSerializer;
|
|
import android.os.Bundle;
|
|
import android.os.Parcel;
|
|
import android.os.Parcelable;
|
|
import android.util.SparseArray;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* Utilities for working with {@link android.os.Bundle}.
|
|
*
|
|
* @hide
|
|
*/
|
|
public final class BundleUtil {
|
|
private BundleUtil() {}
|
|
|
|
/**
|
|
* Deeply checks two bundles are equal or not.
|
|
*
|
|
* <p>Two bundles will be considered equal if they contain the same keys, and each value is also
|
|
* equal. Bundle values are compared using deepEquals.
|
|
*/
|
|
@SuppressWarnings("deprecation")
|
|
public static boolean deepEquals(@Nullable Bundle one, @Nullable Bundle two) {
|
|
if (one == null && two == null) {
|
|
return true;
|
|
}
|
|
if (one == null || two == null) {
|
|
return false;
|
|
}
|
|
if (one.size() != two.size()) {
|
|
return false;
|
|
}
|
|
if (!one.keySet().equals(two.keySet())) {
|
|
return false;
|
|
}
|
|
// Bundle inherit its equals() from Object.java, which only compare their memory address.
|
|
// We should iterate all keys and check their presents and values in both bundle.
|
|
for (String key : one.keySet()) {
|
|
if (!bundleValueEquals(one.get(key), two.get(key))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Deeply checks whether two values in a Bundle are equal or not.
|
|
*
|
|
* <p>Values of type Bundle are compared using {@link #deepEquals}.
|
|
*/
|
|
public static boolean bundleValueEquals(@Nullable Object one, @Nullable Object two) {
|
|
if (one == null && two == null) {
|
|
return true;
|
|
}
|
|
if (one == null || two == null) {
|
|
return false;
|
|
}
|
|
if (one.equals(two)) {
|
|
return true;
|
|
}
|
|
if (one instanceof Bundle && two instanceof Bundle) {
|
|
return deepEquals((Bundle) one, (Bundle) two);
|
|
} else if (one instanceof int[] && two instanceof int[]) {
|
|
return Arrays.equals((int[]) one, (int[]) two);
|
|
} else if (one instanceof byte[] && two instanceof byte[]) {
|
|
return Arrays.equals((byte[]) one, (byte[]) two);
|
|
} else if (one instanceof char[] && two instanceof char[]) {
|
|
return Arrays.equals((char[]) one, (char[]) two);
|
|
} else if (one instanceof long[] && two instanceof long[]) {
|
|
return Arrays.equals((long[]) one, (long[]) two);
|
|
} else if (one instanceof float[] && two instanceof float[]) {
|
|
return Arrays.equals((float[]) one, (float[]) two);
|
|
} else if (one instanceof short[] && two instanceof short[]) {
|
|
return Arrays.equals((short[]) one, (short[]) two);
|
|
} else if (one instanceof double[] && two instanceof double[]) {
|
|
return Arrays.equals((double[]) one, (double[]) two);
|
|
} else if (one instanceof boolean[] && two instanceof boolean[]) {
|
|
return Arrays.equals((boolean[]) one, (boolean[]) two);
|
|
} else if (one instanceof Object[] && two instanceof Object[]) {
|
|
Object[] arrayOne = (Object[]) one;
|
|
Object[] arrayTwo = (Object[]) two;
|
|
if (arrayOne.length != arrayTwo.length) {
|
|
return false;
|
|
}
|
|
if (Arrays.equals(arrayOne, arrayTwo)) {
|
|
return true;
|
|
}
|
|
for (int i = 0; i < arrayOne.length; i++) {
|
|
if (!bundleValueEquals(arrayOne[i], arrayTwo[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
} else if (one instanceof ArrayList && two instanceof ArrayList) {
|
|
ArrayList<?> listOne = (ArrayList<?>) one;
|
|
ArrayList<?> listTwo = (ArrayList<?>) two;
|
|
if (listOne.size() != listTwo.size()) {
|
|
return false;
|
|
}
|
|
for (int i = 0; i < listOne.size(); i++) {
|
|
if (!bundleValueEquals(listOne.get(i), listTwo.get(i))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
} else if (one instanceof SparseArray && two instanceof SparseArray) {
|
|
SparseArray<?> arrayOne = (SparseArray<?>) one;
|
|
SparseArray<?> arrayTwo = (SparseArray<?>) two;
|
|
if (arrayOne.size() != arrayTwo.size()) {
|
|
return false;
|
|
}
|
|
for (int i = 0; i < arrayOne.size(); i++) {
|
|
if (arrayOne.keyAt(i) != arrayTwo.keyAt(i)
|
|
|| !bundleValueEquals(arrayOne.valueAt(i), arrayTwo.valueAt(i))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Calculates the hash code for a bundle.
|
|
*
|
|
* <p>The hash code is only effected by the contents in the bundle. Bundles will get consistent
|
|
* hash code if they have same contents.
|
|
*/
|
|
@SuppressWarnings("deprecation")
|
|
public static int deepHashCode(@Nullable Bundle bundle) {
|
|
if (bundle == null) {
|
|
return 0;
|
|
}
|
|
int[] hashCodes = new int[bundle.size() + 1];
|
|
int hashCodeIdx = 0;
|
|
// Bundle inherit its hashCode() from Object.java, which only relative to their memory
|
|
// address. Bundle doesn't have an order, so we should iterate all keys and combine
|
|
// their value's hashcode into an array. And use the hashcode of the array to be
|
|
// the hashcode of the bundle.
|
|
// Because bundle.keySet() doesn't guarantee any particular order, we need to sort the keys
|
|
// in case the iteration order varies from run to run.
|
|
String[] keys = bundle.keySet().toArray(new String[0]);
|
|
Arrays.sort(keys);
|
|
// Hash the keys so we can detect key-only differences
|
|
hashCodes[hashCodeIdx++] = Arrays.hashCode(keys);
|
|
for (int keyIdx = 0; keyIdx < keys.length; keyIdx++) {
|
|
Object value = bundle.get(keys[keyIdx]);
|
|
if (value instanceof Bundle) {
|
|
hashCodes[hashCodeIdx++] = deepHashCode((Bundle) value);
|
|
} else if (value instanceof int[]) {
|
|
hashCodes[hashCodeIdx++] = Arrays.hashCode((int[]) value);
|
|
} else if (value instanceof byte[]) {
|
|
hashCodes[hashCodeIdx++] = Arrays.hashCode((byte[]) value);
|
|
} else if (value instanceof char[]) {
|
|
hashCodes[hashCodeIdx++] = Arrays.hashCode((char[]) value);
|
|
} else if (value instanceof long[]) {
|
|
hashCodes[hashCodeIdx++] = Arrays.hashCode((long[]) value);
|
|
} else if (value instanceof float[]) {
|
|
hashCodes[hashCodeIdx++] = Arrays.hashCode((float[]) value);
|
|
} else if (value instanceof short[]) {
|
|
hashCodes[hashCodeIdx++] = Arrays.hashCode((short[]) value);
|
|
} else if (value instanceof double[]) {
|
|
hashCodes[hashCodeIdx++] = Arrays.hashCode((double[]) value);
|
|
} else if (value instanceof boolean[]) {
|
|
hashCodes[hashCodeIdx++] = Arrays.hashCode((boolean[]) value);
|
|
} else if (value instanceof String[]) {
|
|
// Optimization to avoid Object[] handler creating an inner array for common cases
|
|
hashCodes[hashCodeIdx++] = Arrays.hashCode((String[]) value);
|
|
} else if (value instanceof Object[]) {
|
|
Object[] array = (Object[]) value;
|
|
int[] innerHashCodes = new int[array.length];
|
|
for (int j = 0; j < array.length; j++) {
|
|
if (array[j] instanceof Bundle) {
|
|
innerHashCodes[j] = deepHashCode((Bundle) array[j]);
|
|
} else if (array[j] != null) {
|
|
innerHashCodes[j] = array[j].hashCode();
|
|
}
|
|
}
|
|
hashCodes[hashCodeIdx++] = Arrays.hashCode(innerHashCodes);
|
|
} else if (value instanceof ArrayList) {
|
|
ArrayList<?> list = (ArrayList<?>) value;
|
|
int[] innerHashCodes = new int[list.size()];
|
|
for (int j = 0; j < innerHashCodes.length; j++) {
|
|
Object item = list.get(j);
|
|
if (item instanceof Bundle) {
|
|
innerHashCodes[j] = deepHashCode((Bundle) item);
|
|
} else if (item != null) {
|
|
innerHashCodes[j] = item.hashCode();
|
|
}
|
|
}
|
|
hashCodes[hashCodeIdx++] = Arrays.hashCode(innerHashCodes);
|
|
} else if (value instanceof SparseArray) {
|
|
SparseArray<?> array = (SparseArray<?>) value;
|
|
int[] innerHashCodes = new int[array.size() * 2];
|
|
for (int j = 0; j < array.size(); j++) {
|
|
innerHashCodes[j * 2] = array.keyAt(j);
|
|
Object item = array.valueAt(j);
|
|
if (item instanceof Bundle) {
|
|
innerHashCodes[j * 2 + 1] = deepHashCode((Bundle) item);
|
|
} else if (item != null) {
|
|
innerHashCodes[j * 2 + 1] = item.hashCode();
|
|
}
|
|
}
|
|
hashCodes[hashCodeIdx++] = Arrays.hashCode(innerHashCodes);
|
|
} else if (value != null) {
|
|
hashCodes[hashCodeIdx++] = value.hashCode();
|
|
} else {
|
|
hashCodes[hashCodeIdx++] = 0;
|
|
}
|
|
}
|
|
return Arrays.hashCode(hashCodes);
|
|
}
|
|
|
|
/**
|
|
* Deeply clones a Bundle.
|
|
*
|
|
* <p>Values which are Bundles, Lists or Arrays are deeply copied themselves.
|
|
*/
|
|
@NonNull
|
|
public static Bundle deepCopy(@NonNull Bundle bundle) {
|
|
// Write bundle to bytes
|
|
Parcel parcel = Parcel.obtain();
|
|
try {
|
|
parcel.writeBundle(bundle);
|
|
byte[] serializedMessage = parcel.marshall();
|
|
|
|
// Read bundle from bytes
|
|
parcel.unmarshall(serializedMessage, 0, serializedMessage.length);
|
|
parcel.setDataPosition(0);
|
|
return parcel.readBundle(BundleUtil.class.getClassLoader());
|
|
} finally {
|
|
parcel.recycle();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function to serialize a {@link SafeParcelable} in a Bundle.
|
|
*
|
|
* <p>Note: {@link Bundle#putParcelableArrayList(String, java.util.ArrayList)} does not preserve
|
|
* SafeParcelable semantics. Use this method instead.Failure to heed this advice will inevitably
|
|
* result in unanticipated runtime crash loops.
|
|
*
|
|
* <p>See http://shortn/_vLTA1IpXK1
|
|
*
|
|
* <p>See http://shortn/_b2QcRB4tpG
|
|
*
|
|
* @param parcel the {@link SafeParcelable} to write to the bundle.
|
|
* @param bundle the bundle containing the key to deserialize from.
|
|
* @param key the name of the key mapping to the serialized object.
|
|
*/
|
|
public static <T extends Parcelable & SafeParcelable> void putParcelable(
|
|
@NonNull T parcel, @NonNull Bundle bundle, @NonNull String key) {
|
|
// We always pass single SafeParcelable in list, we use Iterable to
|
|
// maintain code-sync with GMSCore.
|
|
SafeParcelableSerializer.serializeIterableToBundleSafe(bundle, key, Arrays.asList(parcel));
|
|
}
|
|
|
|
/**
|
|
* Helper function to deserialize a {@link SafeParcelable} from a Bundle.
|
|
*
|
|
* @param bundle the bundle containing the key to deserialize from.
|
|
* @param key the name of the key mapping to the serialized object.
|
|
*/
|
|
@Nullable
|
|
public static <T extends SafeParcelable> T getParcelable(
|
|
@NonNull Bundle bundle,
|
|
@NonNull String key,
|
|
@NonNull Parcelable.Creator<T> safeParcelableCreator) {
|
|
List<T> parcelables =
|
|
SafeParcelableSerializer.deserializeIterableFromBundleSafe(
|
|
bundle, key, safeParcelableCreator);
|
|
if (parcelables != null && !parcelables.isEmpty()) {
|
|
// We always pass single SafeParcelable in list, we use Iterable to
|
|
// maintain code-sync with GMSCore.
|
|
return parcelables.get(0);
|
|
}
|
|
return null;
|
|
}
|
|
}
|