BoundaryInterfaceReflectionUtil.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.support_lib_boundary.util;

import android.os.Build;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collection;

/**
 * A set of utility methods used for calling across the support library boundary.
 */
// Although this is not enforced in chromium, this is a requirement enforced when this file is
// mirrored into AndroidX. See http://b/120770118 for details.
public class BoundaryInterfaceReflectionUtil {
    /**
     * Check if an object is an instance of {@code className}, resolving {@code className} in
     * the object's own ClassLoader. This is useful when {@code obj} may have been created in a
     * ClassLoader other than the current one (in which case {@code obj instanceof Foo} would fail
     * but {@code instanceOfInOwnClassLoader(obj, "Foo")} may succeed).
     */
    public static boolean instanceOfInOwnClassLoader(Object obj, String className) {
        try {
            ClassLoader loader = obj.getClass().getClassLoader();
            // We intentionally set initialize = false because instanceof shouldn't trigger
            // static initialization.
            Class<?> clazz = Class.forName(className, false, loader);
            return clazz.isInstance(obj);
        } catch (ClassNotFoundException e) {
            // If className is not in the ClassLoader, then this cannot be an instance.
            return false;
        }
    }

    /**
     * Utility method for fetching a method from {@param delegateLoader}, with the same signature
     * (package + class + method name + parameters) as a given method defined in another
     * classloader.
     */
    public static Method dupeMethod(Method method, ClassLoader delegateLoader)
            throws ClassNotFoundException, NoSuchMethodException {
        // We're converting one type to another. This is analogous to instantiating the type on the
        // other side of the Boundary, so it makes sense to perform static initialization if it
        // hasn't already happened (initialize = true).
        Class<?> declaringClass =
                Class.forName(method.getDeclaringClass().getName(), true, delegateLoader);
        // We do not need to convert parameter types across ClassLoaders because we never pass
        // BoundaryInterfaces in methods, but pass InvocationHandlers instead.
        Class[] parameterClasses = method.getParameterTypes();
        return declaringClass.getDeclaredMethod(method.getName(), parameterClasses);
    }

    /**
     * Returns an implementation of the boundary interface named clazz, by delegating method calls
     * to the {@link InvocationHandler} invocationHandler.
     *
     * <p>A {@code null} {@link InvocationHandler} is treated as representing a {@code null} object.
     *
     * @param clazz a {@link Class} object representing the desired boundary interface.
     * @param invocationHandler an {@link InvocationHandler} compatible with this boundary
     *     interface.
     */
    @Nullable
    public static <T> T castToSuppLibClass(
            @NonNull Class<T> clazz, @Nullable InvocationHandler invocationHandler) {
        if (invocationHandler == null) return null;
        return clazz.cast(
                Proxy.newProxyInstance(BoundaryInterfaceReflectionUtil.class.getClassLoader(),
                        new Class[] {clazz}, invocationHandler));
    }

    /**
     * Create an {@link InvocationHandler} that delegates method calls to {@code delegate}, making
     * sure that the {@link Method} and parameters being passed exist in the same {@link
     * ClassLoader} as {@code delegate}.
     *
     * <p>A {@code null} delegate is represented with a {@code null} {@link InvocationHandler}.
     *
     * @param delegate the object which the resulting {@link InvocationHandler} should delegate
     *     method calls to.
     * @return an InvocationHandlerWithDelegateGetter wrapping {@code delegate}
     */
    @RequiresApi(Build.VERSION_CODES.KITKAT)
    @Nullable
    public static InvocationHandler createInvocationHandlerFor(@Nullable final Object delegate) {
        if (delegate == null) return null;
        return new InvocationHandlerWithDelegateGetter(delegate);
    }

    /**
     * Plural version of {@link #createInvocationHandlerFor(Object)}. The resulting array will be
     * the same length as {@code delegates}, where the nth {@code InvocationHandler} wraps the nth
     * delegate object.
     *
     * <p>A {@code null} array of delegates is represented with a {@code null} array of {@link
     * InvocationHandler}s. Any individual {@code null} delegate is represented with a {@code null}
     * {@link InvocationHandler}.

     * @param delegates an array of objects to which to delegate.
     * @return an array of InvocationHandlerWithDelegateGetter instances, each delegating to
     *     the corresponding member of {@code delegates}.
     */
    @RequiresApi(Build.VERSION_CODES.KITKAT)
    @Nullable
    public static InvocationHandler[] createInvocationHandlersForArray(
            @Nullable final Object[] delegates) {
        if (delegates == null) return null;

        InvocationHandler[] handlers = new InvocationHandler[delegates.length];
        for (int i = 0; i < handlers.length; i++) {
            handlers[i] = createInvocationHandlerFor(delegates[i]);
        }
        return handlers;
    }

    /**
     * Assuming that the given InvocationHandler was created in the current classloader and is an
     * InvocationHandlerWithDelegateGetter, return the object the InvocationHandler delegates its
     * method calls to.
     *
     * <p>A {@code null} {@link InvocationHandler} is treated as wrapping a {@code null} delegate.
     *
     * @param invocationHandler a {@link Nullable} InvocationHandlerWithDelegateGetter.
     * @return the corresponding delegate.
     */
    @RequiresApi(Build.VERSION_CODES.KITKAT)
    @Nullable
    public static Object getDelegateFromInvocationHandler(
            @Nullable InvocationHandler invocationHandler) {
        if (invocationHandler == null) return null;
        InvocationHandlerWithDelegateGetter objectHolder =
                (InvocationHandlerWithDelegateGetter) invocationHandler;
        return objectHolder.getDelegate();
    }

    /**
     * An InvocationHandler storing the original object that method calls are delegated to.
     * This allows us to pass InvocationHandlers across the support library boundary and later
     * unwrap the objects used as delegates within those InvocationHandlers.
     */
    @RequiresApi(Build.VERSION_CODES.KITKAT)
    private static class InvocationHandlerWithDelegateGetter implements InvocationHandler {
        private final Object mDelegate;

        public InvocationHandlerWithDelegateGetter(@NonNull final Object delegate) {
            mDelegate = delegate;
        }

        @Override
        public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
            final ClassLoader delegateLoader = mDelegate.getClass().getClassLoader();
            try {
                return dupeMethod(method, delegateLoader).invoke(mDelegate, objects);
            } catch (InvocationTargetException e) {
                // If something went wrong, ensure we throw the original exception.
                throw e.getTargetException();
            } catch (ReflectiveOperationException e) {
                throw new RuntimeException("Reflection failed for method " + method, e);
            }
        }

        /**
         * Gets the delegate object (which is never {@code null}).
         */
        @NonNull
        public Object getDelegate() {
            return mDelegate;
        }
    }

    /**
     * Check if this is a debuggable build of Android. Note: we copy BuildInfo's method because we
     * cannot depend on the base-layer here (this folder is mirrored into Android).
     */
    private static boolean isDebuggable() {
        return "eng".equals(Build.TYPE) || "userdebug".equals(Build.TYPE);
    }

    /**
     * Check whether a set of features {@param features} contains a certain feature {@param
     * soughtFeature}.
     */
    public static boolean containsFeature(Collection<String> features, String soughtFeature) {
        assert !soughtFeature.endsWith(Features.DEV_SUFFIX);
        return features.contains(soughtFeature)
                || (isDebuggable() && features.contains(soughtFeature + Features.DEV_SUFFIX));
    }

    public static boolean containsFeature(String[] features, String soughtFeature) {
        return containsFeature(Arrays.asList(features), soughtFeature);
    }
}