Skip to content

Commit

Permalink
Support partial AppComponentFactory in Robolectric
Browse files Browse the repository at this point in the history
Support to use AppComponentFactory in Robolectric from SDK 28.
This CL only supports BroadcastReceiver with custom AppComponentFactory.
In Robolectric, developers use Robolectric#setupService or
Robolectric#buildService to initialize Service instance,
and these methods are static, so it's difficult to leverage
custom AppComponentFactory to initialize Service instance
with custom constructor. And I don't find proper usage
scenarios to use custom AppComponentFactory to initialize
ContentProvider, Application, Activity and ClassLoader.
So I leave them as not implemented state.

Signed-off-by: utzcoz <utzcoz@outlook.com>
  • Loading branch information
utzcoz committed Apr 8, 2023
1 parent 877eb6a commit d2340ee
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 4 deletions.
Expand Up @@ -51,6 +51,7 @@ public class AndroidManifest implements UsesSdk {
private String processName;
private String themeRef;
private String labelRef;
private String appComponentFactory; // Added from SDK 28
private Integer minSdkVersion;
private Integer targetSdkVersion;
private Integer maxSdkVersion;
Expand Down Expand Up @@ -184,6 +185,7 @@ void parseAndroidManifest() {
rClassName = packageName + ".R";

Node applicationNode = findApplicationNode(manifestDocument);
// Parse application node of the AndroidManifest.xml
if (applicationNode != null) {
NamedNodeMap attributes = applicationNode.getAttributes();
int attrCount = attributes.getLength();
Expand All @@ -197,6 +199,7 @@ void parseAndroidManifest() {
processName = applicationAttributes.get("android:process");
themeRef = applicationAttributes.get("android:theme");
labelRef = applicationAttributes.get("android:label");
appComponentFactory = applicationAttributes.get("android:appComponentFactory");

parseReceivers(applicationNode);
parseServices(applicationNode);
Expand Down Expand Up @@ -598,6 +601,11 @@ public String getLabelRef() {
return labelRef;
}

public String getAppComponentFactory() {
parseAndroidManifest();
return appComponentFactory;
}

/**
* Returns the minimum Android SDK version that this package expects to be runnable on, as
* specified in the manifest.
Expand Down
Expand Up @@ -84,6 +84,7 @@
import org.robolectric.shadows.ShadowPackageParser;
import org.robolectric.shadows.ShadowPackageParser._Package_;
import org.robolectric.shadows.ShadowView;
import org.robolectric.util.Logger;
import org.robolectric.util.PerfStatsCollector;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.Scheduler;
Expand Down Expand Up @@ -357,7 +358,7 @@ private Application installAndCreateApplication(
// Preload fonts resources
FontsContract.setApplicationContextForResources(application);
}
registerBroadcastReceivers(application, appManifest);
registerBroadcastReceivers(application, appManifest, loadedApk);

appResources.updateConfiguration(androidConfiguration, displayMetrics);
// propagate any updates to configuration via RuntimeEnvironment.setQualifiers
Expand Down Expand Up @@ -408,6 +409,11 @@ private Package loadAppPackage_measured(Config config, AndroidManifest appManife
Path packageFile = appManifest.getApkFile();
parsedPackage = ShadowPackageParser.callParsePackage(packageFile);
}
if (parsedPackage != null
&& parsedPackage.applicationInfo != null
&& RuntimeEnvironment.getApiLevel() >= P) {
parsedPackage.applicationInfo.appComponentFactory = appManifest.getAppComponentFactory();
}
return parsedPackage;
}

Expand Down Expand Up @@ -692,16 +698,39 @@ private String createTempDir(String name) {
.toString();
}

private static BroadcastReceiver newBroadcastReceiverFromP(
String receiverClassName, LoadedApk loadedApk) {
ClassLoader classLoader = Shadow.class.getClassLoader();
if (loadedApk == null || loadedApk.getAppFactory() == null) {
return (BroadcastReceiver) newInstanceOf(receiverClassName);
} else {
try {
return loadedApk.getAppFactory().instantiateReceiver(classLoader, receiverClassName, null);
} catch (ReflectiveOperationException e) {
Logger.warn(
"Failed to initialize receiver %s with AppComponentFactory %s: %s",
receiverClassName, loadedApk.getAppFactory(), e);
}
}
return null;
}

// TODO move/replace this with packageManager
@VisibleForTesting
static void registerBroadcastReceivers(Application application, AndroidManifest androidManifest) {
static void registerBroadcastReceivers(
Application application, AndroidManifest androidManifest, LoadedApk loadedApk) {
for (BroadcastReceiverData receiver : androidManifest.getBroadcastReceivers()) {
IntentFilter filter = new IntentFilter();
for (String action : receiver.getActions()) {
filter.addAction(action);
}
String receiverClassName = replaceLastDotWith$IfInnerStaticClass(receiver.getName());
application.registerReceiver((BroadcastReceiver) newInstanceOf(receiverClassName), filter);
if (loadedApk != null && RuntimeEnvironment.getApiLevel() >= P) {
application.registerReceiver(
newBroadcastReceiverFromP(receiverClassName, loadedApk), filter);
} else {
application.registerReceiver((BroadcastReceiver) newInstanceOf(receiverClassName), filter);
}
}
}

Expand Down
@@ -0,0 +1,22 @@
package org.robolectric;

import android.app.AppComponentFactory;
import android.content.BroadcastReceiver;
import android.content.Intent;
import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithEmptyActionReceiver;
import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithOneActionReceiver;

public final class CustomAppComponentFactory extends AppComponentFactory {
@Override
public BroadcastReceiver instantiateReceiver(ClassLoader cl, String className, Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
if (className != null) {
if (className.contains(CustomConstructorWithOneActionReceiver.class.getName())) {
return new CustomConstructorWithOneActionReceiver(100);
} else if (className.contains(CustomConstructorWithEmptyActionReceiver.class.getName())) {
return new CustomConstructorWithEmptyActionReceiver(100);
}
}
return super.instantiateReceiver(cl, className, intent);
}
}
@@ -0,0 +1,32 @@
package org.robolectric;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class CustomConstructorReceiverWrapper {
private static class CustomConstructorReceiver extends BroadcastReceiver {
private final int intValue;

public CustomConstructorReceiver(int intValue) {
// We don't use intValue actually, and we only want to use this class to test the
// initialization of BroadcastReceiver with a custom constructor.
this.intValue = intValue;
}

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

public static class CustomConstructorWithOneActionReceiver extends CustomConstructorReceiver {
public CustomConstructorWithOneActionReceiver(int intValue) {
super(intValue);
}
}

public static class CustomConstructorWithEmptyActionReceiver extends CustomConstructorReceiver {
public CustomConstructorWithEmptyActionReceiver(int intValue) {
super(intValue);
}
}
}
Expand Up @@ -88,7 +88,7 @@ public void shouldRegisterReceiversFromTheManifest() throws Exception {
Application application = AndroidTestEnvironment.createApplication(appManifest, null,
new ApplicationInfo());
shadowOf(application).callAttach(RuntimeEnvironment.systemContext);
registerBroadcastReceivers(application, appManifest);
registerBroadcastReceivers(application, appManifest, null);

List<ShadowApplication.Wrapper> receivers = shadowOf(application).getRegisteredReceivers();
assertThat(receivers).hasSize(1);
Expand Down
Expand Up @@ -4,6 +4,7 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.P;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
Expand Down Expand Up @@ -46,6 +47,8 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.ConfigTestReceiver;
import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithEmptyActionReceiver;
import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithOneActionReceiver;
import org.robolectric.R;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
Expand Down Expand Up @@ -94,6 +97,20 @@ public void sendBroadcastWithData_shouldSendToManifestReceiver() throws Exceptio
assertThat(receiver.intentsReceived).hasSize(1);
}

@Test
@Config(manifest = "TestAndroidManifestWithAppComponentFactory.xml", minSdk = P)
public void registerReceiver_shouldGetReceiverWithCustomConstructorEmptyAction() {
BroadcastReceiver receiver = getReceiverOfClass(CustomConstructorWithEmptyActionReceiver.class);
assertThat(receiver).isInstanceOf(CustomConstructorWithEmptyActionReceiver.class);
}

@Test
@Config(manifest = "TestAndroidManifestWithAppComponentFactory.xml", minSdk = P)
public void registerReceiver_shouldGetReceiverWithCustomConstructorAndOneAction() {
BroadcastReceiver receiver = getReceiverOfClass(CustomConstructorWithOneActionReceiver.class);
assertThat(receiver).isInstanceOf(CustomConstructorWithOneActionReceiver.class);
}

@Test
public void registerReceiver_shouldRegisterForAllIntentFilterActions() throws Exception {
BroadcastReceiver receiver = broadcastReceiver("Larry");
Expand Down
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.robolectric">
<uses-sdk android:targetSdkVersion="18"/>

<application
android:appComponentFactory="org.robolectric.CustomAppComponentFactory">
<receiver
android:name=".CustomConstructorReceiverWrapper$CustomConstructorWithOneActionReceiver">
<intent-filter>
<action android:name="org.robolectric.ACTION_CUSTOM_CONSTRUCTOR"/>
</intent-filter>
</receiver>
<receiver
android:name=".CustomConstructorReceiverWrapper$CustomConstructorWithEmptyActionReceiver" />
</application>
</manifest>
@@ -1,20 +1,38 @@
package org.robolectric.shadows;

import static org.robolectric.shadow.api.Shadow.newInstanceOf;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.app.Application;
import android.app.LoadedApk;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.os.Build.VERSION_CODES;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;

@Implements(value = LoadedApk.class, isInAndroidSdk = false)
public class ShadowLoadedApk {
@RealObject private LoadedApk realLoadedApk;
private boolean isClassLoaderInitialized = false;
private final Object classLoaderLock = new Object();

@Implementation
public ClassLoader getClassLoader() {
// The AppComponentFactory was introduced from SDK 28.
if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.P) {
synchronized (classLoaderLock) {
if (!isClassLoaderInitialized) {
isClassLoaderInitialized = true;
tryInitAppComponentFactory(realLoadedApk);
}
}
}
return this.getClass().getClassLoader();
}

Expand All @@ -23,6 +41,35 @@ public ClassLoader getSplitClassLoader(String splitName) throws NameNotFoundExce
return this.getClass().getClassLoader();
}

private void tryInitAppComponentFactory(LoadedApk realLoadedApk) {
if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.P) {
ApplicationInfo applicationInfo = realLoadedApk.getApplicationInfo();
if (applicationInfo == null || applicationInfo.appComponentFactory == null) {
return;
}
_LoadedApk_ loadedApkReflector = reflector(_LoadedApk_.class, realLoadedApk);
if (!loadedApkReflector.getIncludeCode()) {
return;
}
String fullQualifiedClassName =
calculateFullQualifiedClassName(
applicationInfo.appComponentFactory, applicationInfo.packageName);
android.app.AppComponentFactory factory =
(android.app.AppComponentFactory) newInstanceOf(fullQualifiedClassName);
if (factory == null) {
factory = new android.app.AppComponentFactory();
}
loadedApkReflector.setAppFactory(factory);
}
}

private String calculateFullQualifiedClassName(String className, String packageName) {
if (packageName == null) {
return className;
}
return className.startsWith(".") ? packageName + className : className;
}

/** Accessor interface for {@link LoadedApk}'s internals. */
@ForType(LoadedApk.class)
public interface _LoadedApk_ {
Expand All @@ -32,5 +79,11 @@ public interface _LoadedApk_ {

@Accessor("mResources")
void setResources(Resources resources);

@Accessor("mIncludeCode")
boolean getIncludeCode();

@Accessor("mAppComponentFactory")
void setAppFactory(Object appFactory);
}
}

0 comments on commit d2340ee

Please sign in to comment.