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

// Copyright 2020 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 static org.chromium.chrome.browser.base.SplitCompatApplication.CHROME_SPLIT_NAME;

import android.app.Activity;
import android.app.AppComponentFactory;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.os.Build;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;

import org.chromium.base.BundleUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * There are some cases where the ClassLoader for components in the chrome split does not match the
 * ClassLoader from the application Context. This can cause issues, such as ClassCastExceptions when
 * trying to cast between the two ClassLoaders. This class attempts to workaround this bug by
 * explicitly setting the activity's ClassLoader. See b/172602571 for more details.
 *
 * <p>Note: this workaround is not needed for services, since they always uses the base module's
 * ClassLoader, see b/169196314 for more details.
 */
@RequiresApi(Build.VERSION_CODES.P)
public class SplitCompatAppComponentFactory extends AppComponentFactory {
    private static final String TAG = "SplitCompat";

    @IntDef({
        ProcessCreationReason.UNINITIALIZED,
        ProcessCreationReason.PENDING,
        ProcessCreationReason.ACTIVITY,
        ProcessCreationReason.SERVICE,
        ProcessCreationReason.CONTENT_PROVIDER,
        ProcessCreationReason.BROADCAST_RECEIVER,
        ProcessCreationReason.NUM_ENTRIES
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ProcessCreationReason {
        int UNINITIALIZED = -2;
        int PENDING = -1;
        int ACTIVITY = 0;
        int SERVICE = 1;
        int CONTENT_PROVIDER = 2;
        int BROADCAST_RECEIVER = 3;
        int NUM_ENTRIES = 4;
    }

    private static @ProcessCreationReason int sProcessCreationReason =
            ProcessCreationReason.UNINITIALIZED;

    @NonNull
    @Override
    public Activity instantiateActivity(
            @NonNull ClassLoader cl, @NonNull String className, Intent intent)
            throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        setProcessCreationReason(ProcessCreationReason.ACTIVITY);

        // Activities will not call createContextForSplit() which will normally ensure the preload
        // is finished, so we have to manually ensure that here.
        SplitChromeApplication.finishPreload(CHROME_SPLIT_NAME);
        return super.instantiateActivity(getComponentClassLoader(cl, className), className, intent);
    }

    @NonNull
    @Override
    public ContentProvider instantiateProvider(@NonNull ClassLoader cl, @NonNull String className)
            throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        // Android always initializes all ContentProviders when it initializes the Application, so
        // post a task to set the reason asynchronously which will run after Android creates any
        // other components during the launch.
        if (sProcessCreationReason == ProcessCreationReason.UNINITIALIZED) {
            sProcessCreationReason = ProcessCreationReason.PENDING;
            PostTask.postTask(
                    TaskTraits.UI_DEFAULT,
                    () -> {
                        setProcessCreationReason(ProcessCreationReason.CONTENT_PROVIDER);
                    });
        }
        return super.instantiateProvider(getComponentClassLoader(cl, className), className);
    }

    @NonNull
    @Override
    public BroadcastReceiver instantiateReceiver(
            @NonNull ClassLoader cl, @NonNull String className, Intent intent)
            throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        setProcessCreationReason(ProcessCreationReason.BROADCAST_RECEIVER);

        // Receivers call createContextForSplit() on the ContextImpl, which will not run the logic
        // in SplitChromeApplication which makes sure the preload has finished.
        SplitChromeApplication.finishPreload(CHROME_SPLIT_NAME);
        return super.instantiateReceiver(getComponentClassLoader(cl, className), className, intent);
    }

    @NonNull
    @Override
    public Service instantiateService(
            @NonNull ClassLoader cl, @NonNull String className, Intent intent)
            throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        setProcessCreationReason(ProcessCreationReason.SERVICE);
        return super.instantiateService(cl, className, intent);
    }

    private static void setProcessCreationReason(@ProcessCreationReason int reason) {
        if (sProcessCreationReason > ProcessCreationReason.PENDING) return;
        sProcessCreationReason = reason;
        if (!SplitCompatApplication.isBrowserProcess()) return;
        RecordHistogram.recordEnumeratedHistogram(
                "Startup.Android.BrowserProcessCreationReason",
                reason,
                ProcessCreationReason.NUM_ENTRIES);
    }

    public static @ProcessCreationReason int getProcessCreationReason() {
        return sProcessCreationReason;
    }

    private static ClassLoader getComponentClassLoader(ClassLoader cl, String className) {
        Context appContext = ContextUtils.getApplicationContext();
        if (appContext == null) {
            Log.e(TAG, "Unexpected null Context when instantiating component: %s", className);
            return cl;
        }

        ClassLoader baseClassLoader = SplitCompatAppComponentFactory.class.getClassLoader();
        ClassLoader chromeClassLoader = appContext.getClassLoader();
        if (!cl.equals(chromeClassLoader)
                && !BundleUtils.canLoadClass(baseClassLoader, className)
                && BundleUtils.canLoadClass(chromeClassLoader, className)) {
            Log.w(TAG, "Mismatched ClassLoaders between Application and component: %s", className);
            return chromeClassLoader;
        }

        return cl;
    }
}