blob: e77043f3867cd97f20e15ad50a68ef64df9ddce4 [file] [log] [blame]
/*
* 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.os.Bundle;
import android.os.Parcel;
import android.util.SparseArray;
import java.util.ArrayList;
import java.util.Arrays;
/**
* 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.
*/
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}.
*/
private 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.
*/
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 {
hashCodes[hashCodeIdx++] = value.hashCode();
}
}
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();
} finally {
parcel.recycle();
}
}
}