Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support partial AppComponentFactory in Robolectric #8004

Merged
merged 1 commit into from Apr 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
utzcoz marked this conversation as resolved.
Show resolved Hide resolved
&& 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);
}
}