Skip to content

Commit

Permalink
Manually pass in application instance when creating EarlySingletonCom…
Browse files Browse the repository at this point in the history
…ponent.

This fixes cases where we need to create the component before `Application#attachBaseContext()` is called. For example. this seems to be the case for Androidx's startup library.

Fixes #3356

RELNOTES=N/A
PiperOrigin-RevId: 446233053
  • Loading branch information
bcorso authored and Dagger Team committed May 3, 2022
1 parent b0aa9f1 commit d14a3d6
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 53 deletions.
2 changes: 1 addition & 1 deletion java/dagger/hilt/android/EarlyEntryPoints.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public final class EarlyEntryPoints {
// this method easier to use, since most code will use this with an Application or Context type.
@Nonnull
public static <T> T get(Context applicationContext, Class<T> entryPoint) {
Application application = Contexts.getApplication(applicationContext.getApplicationContext());
Application application = Contexts.getApplication(applicationContext);
Preconditions.checkState(
application instanceof GeneratedComponentManagerHolder,
"Expected application context to implement GeneratedComponentManagerHolder. "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package dagger.hilt.android.internal.testing;

import android.app.Application;
import java.lang.reflect.InvocationTargetException;

/** Creates a test's early component. */
Expand All @@ -28,13 +29,13 @@ public abstract class EarlySingletonComponentCreator {
+ "your test class with @HiltAndroidTest and that the processor is running over your "
+ "test.";

static Object createComponent() {
static Object createComponent(Application application) {
try {
return Class.forName(EARLY_SINGLETON_COMPONENT_CREATOR_IMPL)
.asSubclass(EarlySingletonComponentCreator.class)
.getDeclaredConstructor()
.newInstance()
.create();
.create(application);
// We catch each individual exception rather than using a multicatch because multi-catch will
// get compiled to the common but new super type ReflectiveOperationException, which is not
// allowed on API < 19. See b/187826710.
Expand All @@ -52,5 +53,5 @@ static Object createComponent() {
}

/** Creates the early test component. */
abstract Object create();
abstract Object create(Application application);
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public Object earlySingletonComponent() {
if (earlyComponent == null) {
synchronized (earlyComponentLock) {
if (earlyComponent == null) {
earlyComponent = EarlySingletonComponentCreator.createComponent();
earlyComponent = EarlySingletonComponentCreator.createComponent(application);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@

package dagger.hilt.android.processor.internal.customtestapplication;

import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.VOLATILE;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
Expand All @@ -29,9 +34,7 @@
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Modifier;

/**
* Generates an Android Application that holds the Singleton component.
*/
/** Generates an Android Application that holds the Singleton component. */
final class CustomTestApplicationGenerator {
private static final ParameterSpec COMPONENT_MANAGER =
ParameterSpec.builder(ClassNames.TEST_APPLICATION_COMPONENT_MANAGER, "componentManager")
Expand All @@ -55,8 +58,11 @@ public void generate() throws IOException {
.addSuperinterface(
ParameterizedTypeName.get(ClassNames.GENERATED_COMPONENT_MANAGER, TypeName.OBJECT))
.addSuperinterface(ClassNames.TEST_APPLICATION_COMPONENT_MANAGER_HOLDER)
.addField(
FieldSpec.builder(ClassName.OBJECT, "componentManagerLock", PRIVATE, FINAL)
.initializer("new $T()", ClassName.OBJECT)
.build())
.addField(getComponentManagerField())
.addMethod(getAttachBaseContextMethod())
.addMethod(getComponentManagerMethod())
.addMethod(getComponentMethod());

Expand All @@ -71,37 +77,15 @@ public void generate() throws IOException {
// Initialize this in attachBaseContext to not pull it into the main dex.
/** private TestApplicationComponentManager componentManager; */
private static FieldSpec getComponentManagerField() {
return FieldSpec.builder(COMPONENT_MANAGER.type, COMPONENT_MANAGER.name, Modifier.PRIVATE)
return FieldSpec.builder(COMPONENT_MANAGER.type, COMPONENT_MANAGER.name, PRIVATE, VOLATILE)
.build();
}

/**
* Initializes application fields. These fields are initialized in attachBaseContext to avoid
* potential multidexing issues.
*
* <pre><code>
* {@literal @Override} protected void attachBaseContext(Context base) {
* super.attachBaseContext(base);
* componentManager = new TestApplicationComponentManager(this);
* }
* </code></pre>
*/
private static MethodSpec getAttachBaseContextMethod() {
return MethodSpec.methodBuilder("attachBaseContext")
.addAnnotation(Override.class)
.addModifiers(Modifier.PROTECTED, Modifier.FINAL)
.addParameter(ClassNames.CONTEXT, "base")
.addStatement("super.attachBaseContext(base)")
.addStatement("$N = new $T(this)", COMPONENT_MANAGER, COMPONENT_MANAGER.type)
.build();
}

private static MethodSpec getComponentMethod() {
return MethodSpec.methodBuilder("generatedComponent")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.returns(TypeName.OBJECT)
.addStatement("return $N.generatedComponent()", COMPONENT_MANAGER)
.addStatement("return $N().generatedComponent()", COMPONENT_MANAGER)
.build();
}

Expand All @@ -110,6 +94,16 @@ private static MethodSpec getComponentManagerMethod() {
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.returns(ParameterizedTypeName.get(ClassNames.GENERATED_COMPONENT_MANAGER, TypeName.OBJECT))
// This field is initialized lazily to avoid pulling the generated component into the main
// dex. We could possibly avoid this by class loading TestComponentDataSupplier lazily
// rather than in the TestApplicationComponentManager constructor.
.beginControlFlow("if ($N == null)", COMPONENT_MANAGER)
.beginControlFlow("synchronized (componentManagerLock)")
.beginControlFlow("if ($N == null)", COMPONENT_MANAGER)
.addStatement("$N = new $T(this)", COMPONENT_MANAGER, COMPONENT_MANAGER.type)
.endControlFlow()
.endControlFlow()
.endControlFlow()
.addStatement("return $N", COMPONENT_MANAGER)
.build();
}
Expand Down
25 changes: 13 additions & 12 deletions java/dagger/hilt/android/testing/HiltTestApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package dagger.hilt.android.testing;

import android.content.Context;
import androidx.multidex.MultiDexApplication;
import dagger.hilt.android.internal.testing.TestApplicationComponentManager;
import dagger.hilt.android.internal.testing.TestApplicationComponentManagerHolder;
Expand All @@ -28,24 +27,26 @@
public final class HiltTestApplication extends MultiDexApplication
implements GeneratedComponentManager<Object>, TestApplicationComponentManagerHolder {

// This field is initialized in attachBaseContext to avoid pulling the generated component into
// the main dex. We could possibly avoid this by class loading TestComponentDataSupplier lazily
// rather than in the TestApplicationComponentManager constructor.
private TestApplicationComponentManager componentManager;

@Override
protected final void attachBaseContext(Context base) {
super.attachBaseContext(base);
componentManager = new TestApplicationComponentManager(this);
}
// This field is initialized lazily to avoid pulling the generated component into the main dex. We
// could possibly avoid this by class loading TestComponentDataSupplier lazily rather than in the
// TestApplicationComponentManager constructor.
private volatile TestApplicationComponentManager componentManager;
private final Object componentManagerLock = new Object();

@Override
public final GeneratedComponentManager<Object> componentManager() {
if (componentManager == null) {
synchronized (componentManagerLock) {
if (componentManager == null) {
componentManager = new TestApplicationComponentManager(this);
}
}
}
return componentManager;
}

@Override
public final Object generatedComponent() {
return componentManager.generatedComponent();
return componentManager().generatedComponent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,14 @@ static void generate(ProcessingEnvironment env) throws IOException {
.superclass(EARLY_SINGLETON_COMPONENT_CREATOR)
.addMethod(
MethodSpec.methodBuilder("create")
.addParameter(ClassNames.APPLICATION, "application")
.returns(ClassName.OBJECT)
.addStatement(
"return $T.builder()\n"
+ ".applicationContextModule(\n"
+ " new $T($T.getApplication($T.getApplicationContext())))\n"
+ ".applicationContextModule(new $T(application))\n"
+ ".build()",
DEFAULT_COMPONENT_IMPL,
ClassNames.APPLICATION_CONTEXT_MODULE,
ClassNames.CONTEXTS,
ClassNames.APPLICATION_PROVIDER)
ClassNames.APPLICATION_CONTEXT_MODULE)
.build());

Processors.addGeneratedAnnotation(builder, env, ClassNames.ROOT_PROCESSOR.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static com.google.common.truth.Truth.assertThat;

import android.app.Application;
import android.content.Context;
import android.os.Build;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import dagger.hilt.EntryPoint;
Expand Down Expand Up @@ -54,13 +55,18 @@ public static class BaseApplication extends Application {
Foo earlyFoo = null;
IllegalStateException entryPointsException = null;

@Override
protected final void attachBaseContext(Context base) {
// This is a regression test for https://github.com/google/dagger/issues/3356.
// Test that EarlyEntryPoints works even before the base context is attached.
earlyFoo = EarlyEntryPoints.get(this, EarlyFooEntryPoint.class).foo();
super.attachBaseContext(base);
}

@Override
public void onCreate() {
super.onCreate();

// Test that calling EarlyEntryPoints works before the test instance is created.
earlyFoo = EarlyEntryPoints.get(this, EarlyFooEntryPoint.class).foo();

// Test that calling EntryPoints fails if called before the test instance is created.
try {
EntryPoints.get(this, FooEntryPoint.class);
Expand Down

0 comments on commit d14a3d6

Please sign in to comment.