chromium/chrome/android/java/src/org/chromium/chrome/browser/base/ServiceTracingProxyProvider.java

// Copyright 2022 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.chrome.browser.base;

import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.NotificationManager;
import android.content.Context;
import android.content.ContextWrapper;
import android.hardware.display.DisplayManager;
import android.media.AudioManager;
import android.media.MediaRouter;
import android.os.Build;
import android.os.IInterface;
import android.os.SystemClock;
import android.telephony.TelephonyManager;

import androidx.annotation.Nullable;

import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.version_info.Channel;
import org.chromium.base.version_info.VersionConstants;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Proxies IInterfaces for System Services to add trace events for slow IPCs.
 *
 * <p>TODO(crbug.com/40850079): Support tracing system services cached in StaticServiceFetchers.
 * Right now we only support services cached per-context in CachedServiceFetchers.
 */
public class ServiceTracingProxyProvider {
    private static final String TAG = "TracingProxyProvider";

    // Don't trace events that are too short to avoid spamming traces.
    private static final long MINIMUM_IPC_TRACE_DURATION_MS = 2;
    private static final String TRACE_FAILED = "Failed to trace IPCs: ";
    private static final String PROXY_PREP_FAILED = "Failed to prepare service for proxying: ";

    private static final AtomicInteger sProxiesInstalled = new AtomicInteger();
    private static final AtomicInteger sProxiesAttempted = new AtomicInteger();
    private static final AtomicBoolean sProxyInstallCountHistogramRecorded = new AtomicBoolean();

    // Used to defeat Android's hidden API blocklist. I would tell you why it works, but the
    // truth is I don't know. Something to do with the calling class being loaded by the
    // boot classloader and double reflection.
    private static final Method sGetDeclaredMethod;
    private static final Method sGetMethod;
    private static final Method sGetDeclaredField;
    private static final Method sGetField;

    static {
        try {
            sGetDeclaredMethod =
                    Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);
            sGetMethod = Class.class.getDeclaredMethod("getMethod", String.class, Class[].class);
            sGetDeclaredField = Class.class.getDeclaredMethod("getDeclaredField", String.class);
            sGetField = Class.class.getDeclaredMethod("getField", String.class);
        } catch (Throwable e) {
            // These methods should always exist.
            throw new RuntimeException(e);
        }
    }

    private static final class IPCListener implements InvocationHandler {
        private final Object mSystemImpl;

        public IPCListener(Object systemImpl) {
            mSystemImpl = systemImpl;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
                if (!ThreadUtils.runningOnUiThread()) return method.invoke(mSystemImpl, args);

                long start = SystemClock.elapsedRealtime();
                Object result = method.invoke(mSystemImpl, args);
                long durationMs = SystemClock.elapsedRealtime() - start;

                if (durationMs >= MINIMUM_IPC_TRACE_DURATION_MS) {
                    TraceEvent.instantAndroidIPC(
                            mSystemImpl.getClass().getName() + "#" + method.getName(), durationMs);
                }
                return result;
            } catch (InvocationTargetException e) {
                // Need to rethrow the cause or the proxy will generate
                // UndeclaredThrowableExceptions that callers won't be expecting.
                throw e.getCause();
            }
        }
    }

    // DO NOT MODIFY THIS ARRAY. This is a reference to the service cache in ContextImpl.
    private final Object[] mServiceCache;
    // Same length as |mServiceCache|, true if the corresponding service has been proxied.
    AtomicBoolean[] mServiceCacheProxied;

    private final Context mUnwrappedBaseContext;

    private static boolean isEnabled() {
        // A lot of service bindings were uncached pre-R, so easier to start tracing at R+.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return false;

        // Don't ship this tracing to Stable.
        if (VersionConstants.CHANNEL > Channel.BETA) return false;

        // static init failed.
        if (sGetDeclaredMethod == null) return false;
        return true;
    }

    /**
     * @param baseContext The base context for an Application/Activity.
     */
    public static @Nullable ServiceTracingProxyProvider create(Context baseContext) {
        if (!isEnabled()) return null;
        while (baseContext instanceof ContextWrapper) {
            baseContext = ((ContextWrapper) baseContext).getBaseContext();
        }
        return new ServiceTracingProxyProvider(baseContext);
    }

    private ServiceTracingProxyProvider(Context unwrappedBaseContext) {
        assert unwrappedBaseContext.getClass().getName().equals("android.app.ContextImpl");
        mUnwrappedBaseContext = unwrappedBaseContext;
        Object[] serviceCache;
        try {
            serviceCache =
                    (Object[])
                            getField(
                                    mUnwrappedBaseContext,
                                    mUnwrappedBaseContext.getClass(),
                                    "mServiceCache");
            mServiceCacheProxied = new AtomicBoolean[serviceCache.length];
            for (int i = 0; i < mServiceCacheProxied.length; ++i) {
                mServiceCacheProxied[i] = new AtomicBoolean(false);
            }
            // Force the window service to be accessed and added to the service cache immediately.
            // This will make sure it is proxied before ViewRootImpl can cache an unproxied
            // WindowSession.
            unwrappedBaseContext.getSystemService(Context.WINDOW_SERVICE);
        } catch (Throwable throwable) {
            Log.d(TAG, TRACE_FAILED, throwable);
            serviceCache = new Object[0];
        }
        mServiceCache = serviceCache;
    }

    public void traceSystemServices() {
        for (int i = 0; i < mServiceCache.length; ++i) {
            if (mServiceCache[i] != null && !mServiceCacheProxied[i].get()) {
                sProxiesAttempted.incrementAndGet();
                if (traceService(
                        mUnwrappedBaseContext, mServiceCache[i], mServiceCacheProxied[i])) {
                    sProxiesInstalled.incrementAndGet();
                }
            }
        }
        int attempts = sProxiesAttempted.get();
        if (attempts >= 40
                && sProxyInstallCountHistogramRecorded.compareAndSet(
                        /* expectedValue= */ false, true)) {
            RecordHistogram.recordSparseHistogram(
                    "Android.ServiceTracingProxyProvider.SuccessesOutOfInitialForty",
                    sProxiesInstalled.get());
        }
    }

    private static synchronized boolean traceService(
            Context context, Object service, AtomicBoolean serviceCacheProxied) {
        if (serviceCacheProxied.get()) return false;
        try {
            Log.d(TAG, "Attempting to proxy " + service.getClass().getName());
            service = prepareServiceForProxying(service);
        } catch (Throwable throwable) {
            Log.d(TAG, PROXY_PREP_FAILED, throwable);
        }
        boolean success = proxyService(context, service);
        serviceCacheProxied.set(true);
        if (!success) {
            Log.d(TAG, "Could not trace service: " + service.getClass().getName());
        }
        return success;
    }

    // Most services just store their interfaces as members in the service class, but some store
    // them in harder to find places, or don't initialize them at creation time.
    private static Object prepareServiceForProxying(Object service) throws Throwable {
        if (service.getClass().equals(DisplayManager.class)) {
            // Class defers to DisplayManagerGlobal.
            Class clazz = Class.forName("android.hardware.display.DisplayManagerGlobal");
            return callNoArgMethod(null, clazz, "getInstance");
        }
        if (service.getClass().getName().equals("android.view.WindowManagerImpl")) {
            // Class defers to WindowManagerGlobal.
            Class clazz = Class.forName("android.view.WindowManagerGlobal");
            Object managerGlobal = callNoArgMethod(null, clazz, "getInstance");
            // Static service and WindowSession are unpopulated until used. Access them now so that
            // the fields are populated when we inspect them for proxying.
            callNoArgMethod(null, managerGlobal.getClass(), "getWindowManagerService");
            callNoArgMethod(null, managerGlobal.getClass(), "getWindowSession");
            return managerGlobal;
        }
        if (service.getClass().equals(ActivityManager.class)) {
            // Service is stored in static singleton.
            Object singletonInstance =
                    getField(null, service.getClass(), "IActivityManagerSingleton");
            callNoArgMethod(singletonInstance, singletonInstance.getClass(), "get");
            return singletonInstance;
        }
        if (service.getClass().equals(NotificationManager.class)) {
            // Service member is unpopulated until used.
            callNoArgMethod(null, service.getClass(), "getService");
            return service;
        }
        if (service.getClass().equals(TelephonyManager.class)) {
            // Service member is unpopulated until used.
            try {
                callNoArgMethod(null, service.getClass(), "getSubscriberInfoService");
            } catch (Throwable e) {
                Log.d(TAG, PROXY_PREP_FAILED, e);
            }
            try {
                callNoArgMethod(null, service.getClass(), "getSubscriptionService");
            } catch (Throwable e) {
                Log.d(TAG, PROXY_PREP_FAILED, e);
            }
            try {
                callNoArgMethod(null, service.getClass(), "getSmsService");
            } catch (Throwable e) {
                Log.d(TAG, PROXY_PREP_FAILED, e);
            }
            try {
                callNoArgMethod(service, service.getClass(), "getITelephony");
            } catch (Throwable e) {
                Log.d(TAG, PROXY_PREP_FAILED, e);
            }
            return service;
        }
        if (service.getClass().equals(MediaRouter.class)) {
            // Service is stored in static singleton.
            return getField(null, service.getClass(), "sStatic");
        }
        if (service.getClass().equals(AudioManager.class)) {
            // Static service is unpopulated until used.
            callNoArgMethod(null, service.getClass(), "getService");
            return service;
        }
        return service;
    }

    private static Object callNoArgMethod(Object instance, Class<?> clazz, String methodName)
            throws Exception {
        Method method;
        try {
            method = (Method) sGetDeclaredMethod.invoke(clazz, methodName, null);
        } catch (Throwable e) {
            method = (Method) sGetMethod.invoke(clazz, methodName, null);
        }
        method.setAccessible(true);
        return method.invoke(instance);
    }

    private static Object getField(Object instance, Class<?> clazz, String fieldName)
            throws Exception {
        Field field;
        try {
            field = (Field) sGetDeclaredField.invoke(clazz, fieldName);
        } catch (Throwable e) {
            field = (Field) sGetField.invoke(clazz, fieldName);
        }
        field.setAccessible(true);
        return field.get(instance);
    }

    @SuppressLint("NewApi") // Class requires API level 30.
    private static boolean proxyService(Context context, Object service) {
        boolean ret = false;
        try {
            // Search through the class's fields to find Interfaces for Binders.
            Field[] fields = service.getClass().getDeclaredFields();
            // For generic classes like Singleton<? implements IInterface> we need to get the
            // fields from the superclass. Note that if the types are defined on the class itself
            // and not the superclass, it's impossible to get them.
            boolean isGenericClass =
                    service.getClass().getGenericSuperclass() instanceof ParameterizedType;
            // For simplicity, only check the first generic type.
            String genericTypeName = "";
            if (isGenericClass) {
                fields = service.getClass().getSuperclass().getDeclaredFields();
                genericTypeName =
                        service.getClass().getSuperclass().getTypeParameters()[0].getName();
            }
            for (Field field : fields) {
                field.setAccessible(true);
                Class<?> type;
                if (isGenericClass) {
                    if (!field.getGenericType().getTypeName().equals(genericTypeName)) continue;
                    type =
                            (Class<?>)
                                    ((ParameterizedType) service.getClass().getGenericSuperclass())
                                            .getActualTypeArguments()[0];
                } else {
                    type = field.getType();
                }
                if (IInterface.class.isAssignableFrom(type) && type.isInterface()) {
                    Object impl = field.get(service);
                    if (impl == null) {
                        Log.d(TAG, TRACE_FAILED + type + " is null");
                        continue;
                    }
                    // Avoid double-proxying for shared/static bindings.
                    if (Proxy.isProxyClass(impl.getClass())) continue;
                    Object listener =
                            Proxy.newProxyInstance(
                                    context.getClassLoader(),
                                    new Class<?>[] {type},
                                    new IPCListener(impl));
                    field.set(service, listener);
                    Log.d(TAG, "Tracing Proxy installed on: " + type);
                    ret = true;
                }
            }
        } catch (Throwable throwable) {
            Log.d(TAG, TRACE_FAILED, throwable);
        }
        return ret;
    }
}