Skip to content

Commit

Permalink
Support AppComponentFactory in Robolectric
Browse files Browse the repository at this point in the history
Signed-off-by: utzcoz <utzcoz@outlook.com>
  • Loading branch information
utzcoz committed Mar 4, 2023
1 parent 42af661 commit d80410a
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 5 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 @@ -13,6 +13,7 @@
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
Expand Down Expand Up @@ -352,7 +353,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 @@ -403,6 +404,11 @@ private Package loadAppPackage_measured(Config config, AndroidManifest appManife
Path packageFile = appManifest.getApkFile();
parsedPackage = ShadowPackageParser.callParsePackage(packageFile);
}
if (parsedPackage != null
&& parsedPackage.applicationInfo != null
&& Build.VERSION.SDK_INT >= P) {
parsedPackage.applicationInfo.appComponentFactory = appManifest.getAppComponentFactory();
}
return parsedPackage;
}

Expand Down Expand Up @@ -684,14 +690,40 @@ private String createTempDir(String name) {

// 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();
Intent intent = new Intent();
for (String action : receiver.getActions()) {
filter.addAction(action);
intent.setAction(action);
}
String receiverClassName = replaceLastDotWith$IfInnerStaticClass(receiver.getName());
application.registerReceiver((BroadcastReceiver) newInstanceOf(receiverClassName), filter);
BroadcastReceiver broadcastReceiver;
if (loadedApk != null && Build.VERSION.SDK_INT >= P) {
ClassLoader classLoader;
if (application.getBaseContext() != null) {
classLoader = application.getBaseContext().getClassLoader();
} else {
classLoader = loadedApk.getClassLoader();
}
// TODO utzcoz How about one receiver has multiple actions?
try {
broadcastReceiver =
loadedApk.getAppFactory().instantiateReceiver(classLoader, receiverClassName, intent);
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
System.err.println(
"Failed to initialize receiver "
+ receiverClassName
+ " with AppComponentFactory "
+ loadedApk.getAppFactory());
continue;
}
} else {
broadcastReceiver = (BroadcastReceiver) newInstanceOf(receiverClassName);
}
application.registerReceiver(broadcastReceiver, filter);
}
}

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

import android.app.AppComponentFactory;
import android.content.BroadcastReceiver;
import android.content.Intent;

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

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

public 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) {}
}
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,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.ConfigTestReceiver;
import org.robolectric.CustomConstructorReceiver;
import org.robolectric.R;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
Expand Down Expand Up @@ -94,6 +96,13 @@ public void sendBroadcastWithData_shouldSendToManifestReceiver() throws Exceptio
assertThat(receiver.intentsReceived).hasSize(1);
}

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

@Test
public void registerReceiver_shouldRegisterForAllIntentFilterActions() throws Exception {
BroadcastReceiver receiver = broadcastReceiver("Larry");
Expand Down
@@ -0,0 +1,13 @@
<?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=".CustomAppComponentFactory">
<receiver android:name=".CustomConstructorReceiver">
<intent-filter>
<action android:name="org.robolectric.ACTION_CUSTOM_CONSTRUCTOR"/>
</intent-filter>
</receiver>
</application>
</manifest>
@@ -1,28 +1,79 @@
package org.robolectric.shadows;

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;
import android.os.Build.VERSION_CODES;
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() {
return this.getClass().getClassLoader();
ClassLoader classLoader = this.getClass().getClassLoader();
// The AppComponentFactory was introduced from SDK 28.
if (Build.VERSION.SDK_INT >= VERSION_CODES.P) {
synchronized (classLoaderLock) {
if (!isClassLoaderInitialized) {
isClassLoaderInitialized = true;
tryInitAppComponentFactory(realLoadedApk, classLoader);
}
}
}
return classLoader;
}

@Implementation(minSdk = VERSION_CODES.O)
public ClassLoader getSplitClassLoader(String splitName) throws NameNotFoundException {
return this.getClass().getClassLoader();
}

private void tryInitAppComponentFactory(LoadedApk realLoadedApk, ClassLoader classLoader) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.P) {
ApplicationInfo applicationInfo = realLoadedApk.getApplicationInfo();
if (applicationInfo == null || applicationInfo.appComponentFactory == null) {
return;
}
_LoadedApk_ loadedApkReflector = reflector(_LoadedApk_.class, realLoadedApk);
if (!loadedApkReflector.getIncludeCode()) {
return;
}
try {
String fullQualifiedClassName =
calculateFullQualifiedClassName(
applicationInfo.appComponentFactory, applicationInfo.packageName);
android.app.AppComponentFactory factory =
(android.app.AppComponentFactory)
classLoader.loadClass(fullQualifiedClassName).newInstance();
loadedApkReflector.setAppFactory(factory);
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
System.err.println(
"Unable to instantiate appComponentFactory because of " + e.getMessage());
e.printStackTrace(System.err);
}
}
}

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 +83,11 @@ public interface _LoadedApk_ {

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

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

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

0 comments on commit d80410a

Please sign in to comment.