BundleUtil.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 androidx.appsearch.util;

import android.os.Bundle;
import android.os.Parcel;
import android.util.SparseArray;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;

import java.util.ArrayList;
import java.util.Arrays;

/**
 * Utilities for working with {@link android.os.Bundle}.
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
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}.
     */
    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.
     */
    @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();
        } finally {
            parcel.recycle();
        }
    }
}