Usage

From Android P/SDK 28, Android supports developers to use AppComponentFactory to delegate the default Service/BroadcastReceiver/ClassLoader/Activity/Application initialization. For example, we can use the following example to initialize the BroadcastReceiver with custom constructor:

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:appComponentFactory=".CustomAppCompFactory"
        ...
        tools:replace="android:appComponentFactory"
        tools:targetApi="31">
        ...
        <receiver
            android:name=".CustomConstructorReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="com.robolectric.CUSTOM_CONSTRUCTOR" />
            </intent-filter>
        </receiver>
    </application>

</manifest>

CustomAppCompFactory.java:

public class CustomAppCompFactory extends AppComponentFactory {
    @NonNull
    @Override
    public BroadcastReceiver instantiateReceiver(@NonNull ClassLoader cl, @NonNull String className, @Nullable Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        if (className.contains("CustomConstructorReceiver")) {
            return new CustomConstructorReceiver(100); 
        }
        return super.instantiateReceiver(cl, className, intent);
    }
    ...
}

CustomConstructorReceiver.java:

public class CustomConstructorReceiver extends BroadcastReceiver {
    private int value;

    public CustomConstructorReceiver(int value) {
        this.value = value;
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        ...
    }
}

The AppComponentFactory is powerful, and this article will analyze how AOSP supports AppComponentFactory from Android P/SDK 28. The real supporting analysis can help me to implement similar supporting for Robolectric.

Analysis

AOSP part

Parsing AndroidManifest.xml

The ParsingPackageUtils.java parses android:appComponentFactory value from AndroidManifest.xml at its parseBaseApplication method:

String factory = sa.getNonResourceString(
    R.styleable.AndroidManifestApplication_appComponentFactory);
if (factory != null) {
    String appComponentFactory = ParsingUtils.buildClassName(pkgName, factory);
    if (appComponentFactory == null) {
        return input.error("Empty class name in package " + pkgName);
    }

    pkg.setAppComponentFactory(appComponentFactory);
}

And the parsed andorid:appComponentFactory string will be passed to ParsingPackageImpl’s appComponentFactory field:

@Nullable
@DataClass.ParcelWith(ForInternedString.class)
private String appComponentFactory;

public ApplicationInfo toAppInfoWithoutStateWithoutFlags() {
    ApplicationInfo appInfo = new ApplicationInfo();
    ...
    appInfo.appComponentFactory = appComponentFactory;
    ...
}

@Override
public ParsingPackageImpl setAppComponentFactory(@Nullable String appComponentFactory) {
    this.appComponentFactory = appComponentFactory;
    return this;
}

The ParsingPackageImpl will pass this appComponentFactory string to ApplicationInfo#appComponentFactory. This process will pass the android:appComponentFactory string in AndroidManifest.xml to ApplicationInfo#appComponentFactory, from PackageManagerService part to application’s part.

Loading AppComponentFactory instance

When AOSP passes android:appComponentFactory in AndroidManifest.xml to ApplicationInfo#appComponentFactory, the next step is to load real custom AppComponentFactory instance from this string. We can use exception in test code to get real initialization process:

java.lang.RuntimeException: utzocz customAppComponentFactory
    at com.demo.myapplication.CustomAppCompFactory.<init>(CustomAppCompFactory.java:14)
    at java.lang.Class.newInstance(Native Method)
    at android.app.LoadedApk.createAppFactory(LoadedApk.java:273)
    at android.app.LoadedApk.createOrUpdateClassLoaderLocked(LoadedApk.java:1039)
    at android.app.LoadedApk.getClassLoader(LoadedApk.java:1126)
    at android.app.LoadedApk.getResources(LoadedApk.java:1374)
    at android.app.ContextImpl.createAppContext(ContextImpl.java:3090)
    at android.app.ContextImpl.createAppContext(ContextImpl.java:3082)
    at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6650)

When ActivityThread initializes Application and its Context, it will create/update Application’s ClassLoader. Because AppComponentFactory is able to provide custom ClassLoader, so this process needs to update AppComponentFactory instance in LoadedApk:

if (mBaseClassLoader != null) {
    mDefaultClassLoader = mBaseClassLoader;
} else {
    mDefaultClassLoader = ClassLoader.getSystemClassLoader();
}
mAppComponentFactory = createAppFactory(mApplicationInfo, mDefaultClassLoader);

Using AppComponentFactory to initialize BroadcastReceiver

Although AppComponentFactory can provide different critical components of Android, this part only analyzes that how AppComponentFactory is used to initialize BroadcastReceiver. And it’s very simple. The ActivityThread#handleReceiver uses AppComponentFactory#instantiateReceiver to do this task when it needs to provide a BroadcastReceiver instance:

private void handleReceiver(ReceiverData data) {
    ...
    receiver = packageInfo.getAppFactory()
        .instantiateReceiver(cl, data.info.name, data.intent);
    ...

Application part

AppComponentFactory initializes BroadcastReceiver finally

public class CustomAppCompFactory extends AppComponentFactory {
    @NonNull
    @Override
    public BroadcastReceiver instantiateReceiver(@NonNull ClassLoader cl, @NonNull String className, @Nullable Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        if (className.contains("CustomConstructorReceiver")) {
            return new CustomConstructorReceiver(100); 
        }
        return super.instantiateReceiver(cl, className, intent);
    }
    ...
}

In CustomAppCompFactory, we can initialize different custom receivers with different constructors with different input parameters depends on passed Intent and className.