Skip to content

Commit

Permalink
Add a new @CustomInject annotation to control when the Application is…
Browse files Browse the repository at this point in the history
… injected.

Fixes #3122.

RELNOTES=Add @CustomInject
PiperOrigin-RevId: 419019914
  • Loading branch information
Chang-Eric authored and Dagger Team committed Dec 31, 2021
1 parent 667f1c5 commit 36c17bb
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 1 deletion.
3 changes: 3 additions & 0 deletions java/dagger/hilt/android/BUILD
Expand Up @@ -151,6 +151,7 @@ android_library(
":hilt_android_app",
":package_info",
"//java/dagger/hilt:artifact-core-lib",
"//java/dagger/hilt/android/migration:custom_inject",
"//java/dagger/hilt/android/migration:optional_inject",
"//java/dagger/lint:lint-android-artifact-lib",
],
Expand Down Expand Up @@ -178,9 +179,11 @@ gen_maven_artifact(
"//java/dagger/hilt/android/internal/lifecycle",
"//java/dagger/hilt/android/internal/managers",
"//java/dagger/hilt/android/internal/managers:component_supplier",
"//java/dagger/hilt/android/internal/migration:has_custom_inject",
"//java/dagger/hilt/android/internal/migration:injected_by_hilt",
"//java/dagger/hilt/android/internal/modules",
"//java/dagger/hilt/android/lifecycle",
"//java/dagger/hilt/android/migration:custom_inject",
"//java/dagger/hilt/android/migration:optional_inject",
"//java/dagger/hilt/android/migration:package_info",
"//java/dagger/hilt/android/qualifiers",
Expand Down
5 changes: 5 additions & 0 deletions java/dagger/hilt/android/internal/migration/BUILD
Expand Up @@ -17,6 +17,11 @@

package(default_visibility = ["//:src"])

android_library(
name = "has_custom_inject",
srcs = ["HasCustomInject.java"],
)

android_library(
name = "injected_by_hilt",
srcs = ["InjectedByHilt.java"],
Expand Down
25 changes: 25 additions & 0 deletions java/dagger/hilt/android/internal/migration/HasCustomInject.java
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2020 The Dagger Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dagger.hilt.android.internal.migration;

/**
* Do not use except in Hilt generated code. Internal interface for application's using
* {@code CustomInject}.
*/
public interface HasCustomInject {
void customInject();
}
17 changes: 17 additions & 0 deletions java/dagger/hilt/android/migration/BUILD
Expand Up @@ -39,6 +39,23 @@ android_library(
],
)

android_library(
name = "custom_inject",
srcs = [
"CustomInject.java",
"CustomInjection.java",
],
exports = [
"//java/dagger/hilt/android/internal/migration:has_custom_inject",
],
deps = [
":package_info",
"//java/dagger/hilt/android/internal/migration:has_custom_inject",
"//java/dagger/hilt/internal:preconditions",
"@maven//:androidx_annotation_annotation",
],
)

java_library(
name = "package_info",
srcs = ["package-info.java"],
Expand Down
55 changes: 55 additions & 0 deletions java/dagger/hilt/android/migration/CustomInject.java
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2021 The Dagger Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dagger.hilt.android.migration;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

/**
* When used on a {@link dagger.hilt.android.HiltAndroidApp}-annotated application, this causes the
* application to no longer inject itself in onCreate and instead allows it to be injected at some
* other time.
*
* <p>When using this annotation, you can use {@link CustomInjection#inject} to inject the
* application class. Additionally, this annotation will also cause a method, {@code customInject}
* to be generated in the Hilt base class as well, that behaves the same as
* {@link CustomInjection#inject}. The method is available to users that extend the Hilt base class
* directly and don't use the Gradle plugin.
*
* <p> Example usage:
*
* <pre><code>
* {@literal @}CustomInject
* {@literal @}HiltAndroidApp(Application.class)
* public final class MyApplication extends Hilt_MyApplication {
*
* {@literal @}Inject Foo foo;
*
* {@literal @}Override
* public void onCreate() {
* // Injection would normally happen in this super.onCreate() call, but won't now because this
* // is using CustomInject.
* super.onCreate();
* doSomethingBeforeInjection();
* // This call now injects the fields in the Application, like the foo field above.
* CustomInject.inject(this);
* }
* }
* </code></pre>
*/
@Target(ElementType.TYPE)
public @interface CustomInject {}
43 changes: 43 additions & 0 deletions java/dagger/hilt/android/migration/CustomInjection.java
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2021 The Dagger Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dagger.hilt.android.migration;

import android.app.Application;
import androidx.annotation.NonNull;
import dagger.hilt.android.internal.migration.HasCustomInject;
import dagger.hilt.internal.Preconditions;

/**
* Utility methods for injecting the application when using {@link CustomInject}.
*
* @see OptionalInject
*/
public final class CustomInjection {

/** Injects the passed in application. */
public static void inject(@NonNull Application app) {
Preconditions.checkNotNull(app);
Preconditions.checkArgument(
app instanceof HasCustomInject,
"'%s' is not a custom inject application. Check that you have annotated"
+ " the application with both @HiltAndroidApp and @CustomInject.",
app.getClass());
((HasCustomInject) app).customInject();
}

private CustomInjection() {}
}
Expand Up @@ -58,6 +58,10 @@ public final class AndroidClassNames {
get("dagger.hilt.android", "WithFragmentBindings");
public static final ClassName HILT_ANDROID_APP =
get("dagger.hilt.android", "HiltAndroidApp");
public static final ClassName CUSTOM_INJECT =
get("dagger.hilt.android.migration", "CustomInject");
public static final ClassName CUSTOM_INJECTION =
get("dagger.hilt.android.migration", "CustomInjection");
public static final ClassName OPTIONAL_INJECT =
get("dagger.hilt.android.migration", "OptionalInject");

Expand Down Expand Up @@ -96,6 +100,8 @@ public final class AndroidClassNames {
public static final ClassName VIEW_COMPONENT_MANAGER =
get("dagger.hilt.android.internal.managers", "ViewComponentManager");

public static final ClassName HAS_CUSTOM_INJECT =
get("dagger.hilt.android.internal.migration", "HasCustomInject");
public static final ClassName INJECTED_BY_HILT =
get("dagger.hilt.android.internal.migration", "InjectedByHilt");

Expand Down
Expand Up @@ -16,7 +16,10 @@

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

import static javax.lang.model.element.Modifier.ABSTRACT;
import static javax.lang.model.element.Modifier.PROTECTED;

import com.google.common.collect.ImmutableSet;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
Expand All @@ -28,10 +31,16 @@
import com.squareup.javapoet.TypeVariableName;
import dagger.hilt.android.processor.internal.AndroidClassNames;
import dagger.hilt.processor.internal.ComponentNames;
import dagger.hilt.processor.internal.ProcessorErrors;
import dagger.hilt.processor.internal.Processors;
import java.io.IOException;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.util.ElementFilter;

/** Generates an Hilt Application for an @AndroidEntryPoint app class. */
public final class ApplicationGenerator {
Expand Down Expand Up @@ -71,13 +80,46 @@ public void generate() throws IOException {
Generators.copySuppressAnnotations(metadata.element(), typeSpecBuilder);
Generators.addComponentOverride(metadata, typeSpecBuilder);

if (hasCustomInject()) {
typeSpecBuilder.addSuperinterface(AndroidClassNames.HAS_CUSTOM_INJECT);
typeSpecBuilder.addMethod(customInjectMethod());
} else {
typeSpecBuilder.addMethod(onCreateMethod());
}

JavaFile.builder(metadata.elementClassName().packageName(), typeSpecBuilder.build())
.build()
.writeTo(env.getFiler());
}

private boolean hasCustomInject() {
boolean hasCustomInject =
Processors.hasAnnotation(metadata.element(), AndroidClassNames.CUSTOM_INJECT);
if (hasCustomInject) {
// Check that the Hilt base class does not already define a customInject implementation.
Set<ExecutableElement> customInjectMethods =
ElementFilter.methodsIn(
ImmutableSet.<Element>builder()
.addAll(metadata.element().getEnclosedElements())
.addAll(env.getElementUtils().getAllMembers(metadata.baseElement()))
.build())
.stream()
.filter(method -> method.getSimpleName().contentEquals("customInject"))
.filter(method -> method.getParameters().isEmpty())
.collect(Collectors.toSet());

for (ExecutableElement customInjectMethod : customInjectMethods) {
ProcessorErrors.checkState(
customInjectMethod.getModifiers().containsAll(ImmutableSet.of(ABSTRACT, PROTECTED)),
customInjectMethod,
"%s#%s, must have modifiers `abstract` and `protected` when using @CustomInject.",
customInjectMethod.getEnclosingElement(),
customInjectMethod);
}
}
return hasCustomInject;
}

// private final ApplicationComponentManager<ApplicationComponent> componentManager =
// new ApplicationComponentManager(/* creatorType */);
private FieldSpec componentManagerField() {
Expand Down Expand Up @@ -148,7 +190,21 @@ private MethodSpec onCreateMethod() {
.build();
}

// // This is a known unsafe cast but should be fine if the only use is
// @Override
// public final void customInject() {
// // This is a known unsafe cast but is safe in the only correct use case:
// // $APP extends Hilt_$APP
// generatedComponent().inject(($APP) this);
// }
private MethodSpec customInjectMethod() {
return MethodSpec.methodBuilder("customInject")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addCode(injectCodeBlock())
.build();
}

// // This is a known unsafe cast but is safe in the only correct use case:
// // $APP extends Hilt_$APP
// generatedComponent().inject$APP(($APP) this);
private CodeBlock injectCodeBlock() {
Expand Down
28 changes: 28 additions & 0 deletions javatests/dagger/hilt/android/BUILD
Expand Up @@ -114,6 +114,34 @@ android_local_test(
],
)

android_library(
name = "custom_inject_classes",
srcs = ["CustomInjectClasses.java"],
deps = [
"//:dagger_with_compiler",
"//java/dagger/hilt:install_in",
"//java/dagger/hilt/android:hilt_android_app",
"//java/dagger/hilt/android:package_info",
"//java/dagger/hilt/android/migration:custom_inject",
"//third_party/java/jsr330_inject",
],
)

android_local_test(
name = "CustomInjectTest",
size = "small",
srcs = ["CustomInjectTest.java"],
manifest_values = {
"minSdkVersion": "14",
},
deps = [
":custom_inject_classes",
"//:android_local_test_exports",
"//java/dagger/hilt/android:package_info",
"//third_party/java/truth",
],
)

android_local_test(
name = "EarlyEntryPointHiltAndroidAppRuntimeTest",
size = "small",
Expand Down
54 changes: 54 additions & 0 deletions javatests/dagger/hilt/android/CustomInjectClasses.java
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2021 The Dagger Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dagger.hilt.android;

import android.app.Application;
import dagger.Module;
import dagger.Provides;
import dagger.hilt.InstallIn;
import dagger.hilt.android.migration.CustomInject;
import dagger.hilt.android.migration.CustomInjection;
import dagger.hilt.components.SingletonComponent;
import javax.inject.Inject;

/**
* Classes for CustomInjectTest. This is in a separate build target because otherwise
* robolectric does not recognize the application class as extending application due to order of
* class generation.
*/
final class CustomInjectClasses {

@Module
@InstallIn(SingletonComponent.class)
static final class TestModule {
@Provides
static Integer provideInt() {
return 9;
}
}

@CustomInject
@HiltAndroidApp(Application.class)
static final class TestApplication extends Hilt_CustomInjectClasses_TestApplication {

@Inject Integer intValue;

void inject() {
CustomInjection.inject(this);
}
}
}

0 comments on commit 36c17bb

Please sign in to comment.