diff --git a/java/dagger/hilt/android/BUILD b/java/dagger/hilt/android/BUILD index dee47a3b2e8..d32617f67fe 100644 --- a/java/dagger/hilt/android/BUILD +++ b/java/dagger/hilt/android/BUILD @@ -194,9 +194,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/managers:saved_state_handle_holder", "//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:activity_retained_saved_state", "//java/dagger/hilt/android/lifecycle:hilt_view_model", "//java/dagger/hilt/android/lifecycle:hilt_view_model_extensions", "//java/dagger/hilt/android/lifecycle:package_info", diff --git a/java/dagger/hilt/android/internal/builders/ActivityRetainedComponentBuilder.java b/java/dagger/hilt/android/internal/builders/ActivityRetainedComponentBuilder.java index 110b3fef10f..ff5998c01ff 100644 --- a/java/dagger/hilt/android/internal/builders/ActivityRetainedComponentBuilder.java +++ b/java/dagger/hilt/android/internal/builders/ActivityRetainedComponentBuilder.java @@ -16,11 +16,17 @@ package dagger.hilt.android.internal.builders; +import dagger.BindsInstance; import dagger.hilt.DefineComponent; import dagger.hilt.android.components.ActivityRetainedComponent; +import dagger.hilt.android.internal.managers.SavedStateHandleHolder; /** Interface for creating a {@link ActivityRetainedComponent}. */ @DefineComponent.Builder public interface ActivityRetainedComponentBuilder { + + ActivityRetainedComponentBuilder savedStateHandleHolder( + @BindsInstance SavedStateHandleHolder savedStateHandleHolder); + ActivityRetainedComponent build(); } diff --git a/java/dagger/hilt/android/internal/builders/BUILD b/java/dagger/hilt/android/internal/builders/BUILD index 598503efffc..7f26e1dce08 100644 --- a/java/dagger/hilt/android/internal/builders/BUILD +++ b/java/dagger/hilt/android/internal/builders/BUILD @@ -25,6 +25,7 @@ android_library( "//java/dagger/hilt:define_component", "//java/dagger/hilt/android:view_model_lifecycle", "//java/dagger/hilt/android/components", + "//java/dagger/hilt/android/internal/managers:saved_state_handle_holder", "@maven//:androidx_activity_activity", "@maven//:androidx_fragment_fragment", "@maven//:androidx_lifecycle_lifecycle_common", diff --git a/java/dagger/hilt/android/internal/managers/ActivityComponentManager.java b/java/dagger/hilt/android/internal/managers/ActivityComponentManager.java index 50e4ce11de5..3fa4910d012 100644 --- a/java/dagger/hilt/android/internal/managers/ActivityComponentManager.java +++ b/java/dagger/hilt/android/internal/managers/ActivityComponentManager.java @@ -70,6 +70,12 @@ public Object generatedComponent() { return component; } + public final SavedStateHandleHolder getSavedStateHandleHolder() { + // This will only be used on base activity that extends ComponentActivity. + return ((ActivityRetainedComponentManager) activityRetainedComponentManager) + .getSavedStateHandleHolder(); + } + protected Object createComponent() { if (!(activity.getApplication() instanceof GeneratedComponentManager)) { throw new IllegalStateException( diff --git a/java/dagger/hilt/android/internal/managers/ActivityRetainedComponentManager.java b/java/dagger/hilt/android/internal/managers/ActivityRetainedComponentManager.java index df63bd3d16e..dc3539c34d6 100644 --- a/java/dagger/hilt/android/internal/managers/ActivityRetainedComponentManager.java +++ b/java/dagger/hilt/android/internal/managers/ActivityRetainedComponentManager.java @@ -23,6 +23,7 @@ import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelStoreOwner; +import androidx.lifecycle.viewmodel.CreationExtras; import dagger.Module; import dagger.Provides; import dagger.hilt.EntryPoint; @@ -57,15 +58,22 @@ public interface ActivityRetainedLifecycleEntryPoint { static final class ActivityRetainedComponentViewModel extends ViewModel { private final ActivityRetainedComponent component; + private final SavedStateHandleHolder savedStateHandleHolder; - ActivityRetainedComponentViewModel(ActivityRetainedComponent component) { + ActivityRetainedComponentViewModel( + ActivityRetainedComponent component, SavedStateHandleHolder savedStateHandleHolder) { this.component = component; + this.savedStateHandleHolder = savedStateHandleHolder; } ActivityRetainedComponent getComponent() { return component; } + SavedStateHandleHolder getSavedStateHandleHolder() { + return savedStateHandleHolder; + } + @Override protected void onCleared() { super.onCleared(); @@ -95,13 +103,17 @@ private ViewModelProvider getViewModelProvider( @NonNull @Override @SuppressWarnings("unchecked") - public T create(@NonNull Class aClass) { + public T create( + @NonNull Class aClass, CreationExtras creationExtras) { + SavedStateHandleHolder savedStateHandleHolder = + new SavedStateHandleHolder(creationExtras); ActivityRetainedComponent component = EntryPointAccessors.fromApplication( - context, ActivityRetainedComponentBuilderEntryPoint.class) + context, ActivityRetainedComponentBuilderEntryPoint.class) .retainedComponentBuilder() + .savedStateHandleHolder(savedStateHandleHolder) .build(); - return (T) new ActivityRetainedComponentViewModel(component); + return (T) new ActivityRetainedComponentViewModel(component, savedStateHandleHolder); } }); } @@ -118,6 +130,12 @@ public ActivityRetainedComponent generatedComponent() { return component; } + public SavedStateHandleHolder getSavedStateHandleHolder() { + return getViewModelProvider(viewModelStoreOwner, context) + .get(ActivityRetainedComponentViewModel.class) + .getSavedStateHandleHolder(); + } + private ActivityRetainedComponent createComponent() { return getViewModelProvider(viewModelStoreOwner, context) .get(ActivityRetainedComponentViewModel.class) diff --git a/java/dagger/hilt/android/internal/managers/BUILD b/java/dagger/hilt/android/internal/managers/BUILD index e553c6e8c50..386540335ba 100644 --- a/java/dagger/hilt/android/internal/managers/BUILD +++ b/java/dagger/hilt/android/internal/managers/BUILD @@ -30,11 +30,14 @@ android_library( "ApplicationComponentManager.java", "BroadcastReceiverComponentManager.java", "FragmentComponentManager.java", + "SavedStateHandleModule.java", "ServiceComponentManager.java", "ViewComponentManager.java", ], + exports = [":saved_state_handle_holder"], deps = [ ":component_supplier", + ":saved_state_handle_holder", "//:dagger_with_compiler", "//java/dagger/hilt:entry_point", "//java/dagger/hilt:install_in", @@ -44,6 +47,7 @@ android_library( "//java/dagger/hilt/android/internal", "//java/dagger/hilt/android/internal/builders", "//java/dagger/hilt/android/internal/lifecycle", + "//java/dagger/hilt/android/lifecycle:activity_retained_saved_state", "//java/dagger/hilt/android/scopes", "//java/dagger/hilt/internal:component_manager", "//java/dagger/hilt/internal:preconditions", @@ -56,6 +60,21 @@ android_library( ], ) +android_library( + name = "saved_state_handle_holder", + srcs = ["SavedStateHandleHolder.java"], + deps = [ + "//java/dagger/hilt/android/internal", + "//java/dagger/hilt/internal:preconditions", + "@maven//:androidx_activity_activity", + "@maven//:androidx_annotation_annotation", + "@maven//:androidx_fragment_fragment", + "@maven//:androidx_lifecycle_lifecycle_common", + "@maven//:androidx_lifecycle_lifecycle_viewmodel", + "@maven//:androidx_lifecycle_lifecycle_viewmodel_savedstate", + ], +) + filegroup( name = "srcs_filegroup", srcs = glob(["*"]), diff --git a/java/dagger/hilt/android/internal/managers/SavedStateHandleHolder.java b/java/dagger/hilt/android/internal/managers/SavedStateHandleHolder.java new file mode 100644 index 00000000000..83501590028 --- /dev/null +++ b/java/dagger/hilt/android/internal/managers/SavedStateHandleHolder.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 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.managers; + +import static dagger.hilt.internal.Preconditions.checkNotNull; +import static dagger.hilt.internal.Preconditions.checkState; + +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.lifecycle.SavedStateHandle; +import androidx.lifecycle.SavedStateHandleSupport; +import androidx.lifecycle.viewmodel.CreationExtras; +import androidx.lifecycle.viewmodel.MutableCreationExtras; +import dagger.hilt.android.internal.ThreadUtil; + +/** Implementation for SavedStateHandleHolder. */ +public final class SavedStateHandleHolder { + private CreationExtras extras; + private SavedStateHandle handle; + private final boolean nonComponentActivity; + + SavedStateHandleHolder(@Nullable CreationExtras extras) { + nonComponentActivity = (extras == null); + this.extras = extras; + } + + SavedStateHandle getSavedStateHandle() { + ThreadUtil.ensureMainThread(); + checkState( + !nonComponentActivity, + "Activity that does not extend ComponentActivity cannot use SavedStateHandle"); + if (handle != null) { + return handle; + } + checkNotNull( + extras, + "The first access to SavedStateHandle should happen between super.onCreate() and" + + " super.onDestroy()"); + // Clean up default args, since those are unused and we don't want to duplicate those for each + // SavedStateHandle + MutableCreationExtras mutableExtras = new MutableCreationExtras(extras); + mutableExtras.set(SavedStateHandleSupport.DEFAULT_ARGS_KEY, Bundle.EMPTY); + extras = mutableExtras; + handle = SavedStateHandleSupport.createSavedStateHandle(extras); + + extras = null; + return handle; + } + + public void clear() { + extras = null; + } + + public void setExtras(CreationExtras extras) { + if (handle != null) { + // If handle is already created, we don't need to store CreationExtras. + return; + } + this.extras = extras; + } + + public boolean isInvalid() { + return handle == null && extras == null; + } +} diff --git a/java/dagger/hilt/android/internal/managers/SavedStateHandleModule.java b/java/dagger/hilt/android/internal/managers/SavedStateHandleModule.java new file mode 100644 index 00000000000..ee2065f90d4 --- /dev/null +++ b/java/dagger/hilt/android/internal/managers/SavedStateHandleModule.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 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.managers; + +import androidx.lifecycle.SavedStateHandle; +import dagger.Module; +import dagger.Provides; +import dagger.hilt.InstallIn; +import dagger.hilt.android.components.ActivityRetainedComponent; +import dagger.hilt.android.lifecycle.ActivityRetainedSavedState; +import dagger.hilt.android.scopes.ActivityRetainedScoped; + +/** Module providing SavedStateHandle from ActivityRetainedComponent. */ +@Module +@InstallIn(ActivityRetainedComponent.class) +abstract class SavedStateHandleModule { + @ActivityRetainedSavedState + @ActivityRetainedScoped + @Provides + static SavedStateHandle provideSavedStateHandle(SavedStateHandleHolder savedStateHandleHolder) { + return savedStateHandleHolder.getSavedStateHandle(); + } +} diff --git a/java/dagger/hilt/android/lifecycle/ActivityRetainedSavedState.java b/java/dagger/hilt/android/lifecycle/ActivityRetainedSavedState.java new file mode 100644 index 00000000000..3d666a803c5 --- /dev/null +++ b/java/dagger/hilt/android/lifecycle/ActivityRetainedSavedState.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 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.lifecycle; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import javax.inject.Qualifier; + +/** Qualifies a binding that belongs to ActivityRetainedComponent. */ +@Qualifier +@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) +public @interface ActivityRetainedSavedState {} diff --git a/java/dagger/hilt/android/lifecycle/BUILD b/java/dagger/hilt/android/lifecycle/BUILD index 388f52780d6..b13ed20bd94 100644 --- a/java/dagger/hilt/android/lifecycle/BUILD +++ b/java/dagger/hilt/android/lifecycle/BUILD @@ -56,6 +56,12 @@ android_library( ], ) +android_library( + name = "activity_retained_saved_state", + srcs = ["ActivityRetainedSavedState.java"], + deps = ["//third_party/java/jsr330_inject"], +) + compat_kt_android_library( name = "hilt_view_model_extensions", srcs = ["HiltViewModelExtensions.kt"], diff --git a/java/dagger/hilt/android/processor/BUILD b/java/dagger/hilt/android/processor/BUILD index 6f45216f161..bf5a6c3b539 100644 --- a/java/dagger/hilt/android/processor/BUILD +++ b/java/dagger/hilt/android/processor/BUILD @@ -52,6 +52,7 @@ gen_maven_artifact( "//java/dagger/hilt/processor/internal:component_names", "//java/dagger/hilt/processor/internal:components", "//java/dagger/hilt/processor/internal:hilt_processing_env_configs", + "//java/dagger/hilt/processor/internal:method_signature", "//java/dagger/hilt/processor/internal:processor_errors", "//java/dagger/hilt/processor/internal:processors", "//java/dagger/hilt/processor/internal/aggregateddeps:component_dependencies", diff --git a/java/dagger/hilt/android/processor/internal/AndroidClassNames.java b/java/dagger/hilt/android/processor/internal/AndroidClassNames.java index 7f5dec060ca..e37c6b7bd0b 100644 --- a/java/dagger/hilt/android/processor/internal/AndroidClassNames.java +++ b/java/dagger/hilt/android/processor/internal/AndroidClassNames.java @@ -99,6 +99,10 @@ public final class AndroidClassNames { get("dagger.hilt.android.internal.managers", "ServiceComponentManager"); public static final ClassName VIEW_COMPONENT_MANAGER = get("dagger.hilt.android.internal.managers", "ViewComponentManager"); + public static final ClassName SAVED_STATE_HANDLE_ENTRY_POINTS = + get("dagger.hilt.android.internal.managers", "SavedStateHandleEntryPoints"); + public static final ClassName SAVED_STATE_HANDLE_HOLDER = + get("dagger.hilt.android.internal.managers", "SavedStateHandleHolder"); public static final ClassName HAS_CUSTOM_INJECT = get("dagger.hilt.android.internal.migration", "HasCustomInject"); @@ -125,9 +129,13 @@ public final class AndroidClassNames { get("androidx.lifecycle", "ViewModelProvider", "Factory"); public static final ClassName SAVED_STATE_HANDLE = get("androidx.lifecycle", "SavedStateHandle"); - + public static final ClassName DEFAULT_LIFECYCLE_OBSERVER = + get("androidx.lifecycle", "DefaultLifecycleObserver"); + public static final ClassName LIFECYCLE_OWNER = get("androidx.lifecycle", "LifecycleOwner"); public static final ClassName ON_CONTEXT_AVAILABLE_LISTENER = get("androidx.activity.contextaware", "OnContextAvailableListener"); + public static final ClassName UI_THREAD = get("androidx.annotation", "UiThread"); + public static final ClassName INJECT_VIA_ON_CONTEXT_AVAILABLE_LISTENER = get("dagger.hilt.android", "InjectViaOnContextAvailableListener"); diff --git a/java/dagger/hilt/android/processor/internal/androidentrypoint/ActivityGenerator.java b/java/dagger/hilt/android/processor/internal/androidentrypoint/ActivityGenerator.java index 49f439ec9d3..0852551da81 100644 --- a/java/dagger/hilt/android/processor/internal/androidentrypoint/ActivityGenerator.java +++ b/java/dagger/hilt/android/processor/internal/androidentrypoint/ActivityGenerator.java @@ -16,22 +16,57 @@ package dagger.hilt.android.processor.internal.androidentrypoint; +import static com.google.common.base.Preconditions.checkState; +import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList; +import static kotlin.streams.jdk8.StreamsKt.asStream; + import androidx.room.compiler.processing.JavaPoetExtKt; +import androidx.room.compiler.processing.XAnnotated; +import androidx.room.compiler.processing.XExecutableParameterElement; import androidx.room.compiler.processing.XFiler; +import androidx.room.compiler.processing.XMethodElement; import androidx.room.compiler.processing.XProcessingEnv; import androidx.room.compiler.processing.XTypeParameterElement; +import com.google.common.base.CaseFormat; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import dagger.hilt.android.processor.internal.AndroidClassNames; +import dagger.hilt.processor.internal.ClassNames; +import dagger.hilt.processor.internal.MethodSignature; import dagger.hilt.processor.internal.Processors; +import dagger.internal.codegen.xprocessing.XElements; import java.io.IOException; import javax.lang.model.element.Modifier; /** Generates an Hilt Activity class for the @AndroidEntryPoint annotated class. */ public final class ActivityGenerator { + private enum ActivityMethod { + ON_CREATE(AndroidClassNames.BUNDLE), + ON_STOP(), + ON_DESTROY(); + + @SuppressWarnings("ImmutableEnumChecker") + private final MethodSignature signature; + + ActivityMethod(TypeName... parameterTypes) { + String methodName = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, name()); + this.signature = MethodSignature.of(methodName, parameterTypes); + } + } + + private static final FieldSpec SAVED_STATE_HANDLE_HOLDER_FIELD = + FieldSpec.builder(AndroidClassNames.SAVED_STATE_HANDLE_HOLDER, "savedStateHandleHolder") + .addModifiers(Modifier.PRIVATE) + .build(); + private final XProcessingEnv env; private final AndroidEntryPointMetadata metadata; private final ClassName generatedClassName; @@ -61,6 +96,13 @@ public void generate() throws IOException { CodeBlock.builder().addStatement("_initHiltInternal()").build(), builder); builder.addMethod(init()); + if (!metadata.overridesAndroidEntryPointClass()) { + builder + .addField(SAVED_STATE_HANDLE_HOLDER_FIELD) + .addMethod(initSavedStateHandleHolderMethod()) + .addMethod(onCreateComponentActivity()) + .addMethod(onDestroyComponentActivity()); + } metadata.baseElement().getTypeParameters().stream() .map(XTypeParameterElement::getTypeVariableName) @@ -133,4 +175,103 @@ private MethodSpec getDefaultViewModelProviderFactory() { AndroidClassNames.DEFAULT_VIEW_MODEL_FACTORIES) .build(); } + + // @Override + // public void onCreate(Bundle bundle) { + // super.onCreate(savedInstanceState); + // initSavedStateHandleHolder(); + // } + // + private MethodSpec onCreateComponentActivity() { + XMethodElement nearestOverrideMethod = + requireNearestOverrideMethod(ActivityMethod.ON_CREATE, metadata); + ParameterSpec.Builder parameterBuilder = + ParameterSpec.builder(AndroidClassNames.BUNDLE, "savedInstanceState"); + MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("onCreate"); + // If the sub class is overriding onCreate with @Nullable parameter, then this generated + // method will also prefix the parameter with @Nullable. + if (isNullable(nearestOverrideMethod.getParameters().get(0))) { + parameterBuilder.addAnnotation(AndroidClassNames.NULLABLE); + } + if (nearestOverrideMethod.hasAnnotation(AndroidClassNames.UI_THREAD)) { + methodBuilder.addAnnotation(AndroidClassNames.UI_THREAD); + } + return methodBuilder + .addAnnotation(AndroidClassNames.CALL_SUPER) + .addAnnotation(Override.class) + .addModifiers(XElements.getModifiers(nearestOverrideMethod)) + .addParameter(parameterBuilder.build()) + .addStatement("super.onCreate(savedInstanceState)") + .addStatement("initSavedStateHandleHolder()") + .build(); + } + + // private void initSavedStateHandleHolder() { + // savedStateHandleHolder = componentManager().getSavedStateHandleHolder(); + // if (savedStateHandleHolder.isInvalid()) { + // savedStateHandleHolder.setExtras(getDefaultViewModelCreationExtras()); + // } + // } + private static MethodSpec initSavedStateHandleHolderMethod() { + return MethodSpec.methodBuilder("initSavedStateHandleHolder") + .addModifiers(Modifier.PRIVATE) + .beginControlFlow( + "if (getApplication() instanceof $T)", ClassNames.GENERATED_COMPONENT_MANAGER) + .addStatement( + "$N = componentManager().getSavedStateHandleHolder()", SAVED_STATE_HANDLE_HOLDER_FIELD) + .beginControlFlow("if ($N.isInvalid())", SAVED_STATE_HANDLE_HOLDER_FIELD) + .addStatement( + "$N.setExtras(getDefaultViewModelCreationExtras())", SAVED_STATE_HANDLE_HOLDER_FIELD) + .endControlFlow() + .endControlFlow() + .build(); + } + + private static boolean isNullable(XExecutableParameterElement element) { + return hasNullableAnnotation(element) || hasNullableAnnotation(element.getType()); + } + + private static boolean hasNullableAnnotation(XAnnotated element) { + return element.getAllAnnotations().stream() + .anyMatch(annotation -> annotation.getClassName().simpleName().equals("Nullable")); + } + + // @Override + // public void onDestroy() { + // super.onDestroy(); + // if (savedStateHandleHolder != null) { + // savedStateHandleHolder.clear(); + // } + // } + private MethodSpec onDestroyComponentActivity() { + XMethodElement nearestOverrideMethod = + requireNearestOverrideMethod(ActivityMethod.ON_DESTROY, metadata); + return MethodSpec.methodBuilder("onDestroy") + .addAnnotation(Override.class) + .addModifiers(XElements.getModifiers(nearestOverrideMethod)) + .addStatement("super.onDestroy()") + .beginControlFlow("if ($N != null)", SAVED_STATE_HANDLE_HOLDER_FIELD) + .addStatement("$N.clear()", SAVED_STATE_HANDLE_HOLDER_FIELD) + .endControlFlow() + .build(); + } + + private static XMethodElement requireNearestOverrideMethod( + ActivityMethod activityMethod, AndroidEntryPointMetadata metadata) { + XMethodElement methodOnAndroidEntryPointElement = + metadata.element().getDeclaredMethods().stream() + .filter(method -> MethodSignature.of(method).equals(activityMethod.signature)) + .findFirst() + .orElse(null); + if (methodOnAndroidEntryPointElement != null) { + return methodOnAndroidEntryPointElement; + } + + ImmutableList methodOnBaseElement = + asStream(metadata.baseElement().getAllMethods()) + .filter(method -> MethodSignature.of(method).equals(activityMethod.signature)) + .collect(toImmutableList()); + checkState(methodOnBaseElement.size() >= 1); + return Iterables.getLast(methodOnBaseElement); + } } diff --git a/java/dagger/hilt/android/processor/internal/androidentrypoint/BUILD b/java/dagger/hilt/android/processor/internal/androidentrypoint/BUILD index 96beff3fd28..327412b52cd 100644 --- a/java/dagger/hilt/android/processor/internal/androidentrypoint/BUILD +++ b/java/dagger/hilt/android/processor/internal/androidentrypoint/BUILD @@ -71,11 +71,11 @@ java_library( "//java/dagger/hilt/android/processor/internal:android_classnames", "//java/dagger/hilt/processor/internal:classnames", "//java/dagger/hilt/processor/internal:component_names", + "//java/dagger/hilt/processor/internal:method_signature", "//java/dagger/hilt/processor/internal:processor_errors", "//java/dagger/hilt/processor/internal:processors", "//java/dagger/internal/codegen/extension", "//java/dagger/internal/codegen/xprocessing", - "//third_party/java/auto:common", "//third_party/java/guava/base", "//third_party/java/guava/collect", "//third_party/java/javapoet", diff --git a/java/dagger/hilt/processor/BUILD b/java/dagger/hilt/processor/BUILD index ba7c9eee4d9..ecbc2949500 100644 --- a/java/dagger/hilt/processor/BUILD +++ b/java/dagger/hilt/processor/BUILD @@ -74,6 +74,7 @@ gen_maven_artifact( "//java/dagger/hilt/processor/internal:component_names", "//java/dagger/hilt/processor/internal:components", "//java/dagger/hilt/processor/internal:hilt_processing_env_configs", + "//java/dagger/hilt/processor/internal:method_signature", "//java/dagger/hilt/processor/internal:processor_errors", "//java/dagger/hilt/processor/internal:processors", "//java/dagger/hilt/processor/internal/aggregateddeps:component_dependencies", diff --git a/java/dagger/hilt/processor/internal/BUILD b/java/dagger/hilt/processor/internal/BUILD index 50e20f8bf7f..8828227aabc 100644 --- a/java/dagger/hilt/processor/internal/BUILD +++ b/java/dagger/hilt/processor/internal/BUILD @@ -84,6 +84,20 @@ java_library( ], ) +java_library( + name = "method_signature", + srcs = [ + "MethodSignature.java", + ], + deps = [ + "//java/dagger/internal/codegen/extension", + "//java/dagger/internal/codegen/xprocessing", + "//third_party/java/auto:value", + "//third_party/java/guava/collect", + "//third_party/java/javapoet", + ], +) + java_library( name = "classnames", srcs = [ diff --git a/java/dagger/hilt/processor/internal/MethodSignature.java b/java/dagger/hilt/processor/internal/MethodSignature.java new file mode 100644 index 00000000000..7fffef2bb7d --- /dev/null +++ b/java/dagger/hilt/processor/internal/MethodSignature.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 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.processor.internal; + +import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList; +import static java.util.stream.Collectors.joining; + +import androidx.room.compiler.processing.XExecutableElement; +import androidx.room.compiler.processing.XMethodElement; +import androidx.room.compiler.processing.XMethodType; +import androidx.room.compiler.processing.XType; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; +import dagger.internal.codegen.xprocessing.XElements; + +/** Represents the method signature needed to uniquely identify a method. */ +@AutoValue +public abstract class MethodSignature { + MethodSignature() {} + + abstract String name(); + + abstract ImmutableList parameters(); + + /** Creates a {@link MethodSignature} from a method name and parameter {@link TypeNames} */ + public static MethodSignature of(String methodName, TypeName... typeNames) { + return new AutoValue_MethodSignature(methodName, ImmutableList.copyOf(typeNames)); + } + + /** Creates a {@link MethodSignature} from a {@link MethodSpec} */ + public static MethodSignature of(MethodSpec method) { + return new AutoValue_MethodSignature( + method.name, method.parameters.stream().map(p -> p.type).collect(toImmutableList())); + } + + /** Creates a {@link MethodSignature} from an {@link XExecutableElement} */ + public static MethodSignature of(XExecutableElement executableElement) { + return new AutoValue_MethodSignature( + XElements.getSimpleName(executableElement), + executableElement.getParameters().stream() + .map(p -> p.getType().getTypeName()) + .collect(toImmutableList())); + } + + /** + * Creates a {@link MethodSignature} from an {@link XMethodElement}. + * + *

This version will resolve type parameters as declared by {@code enclosing}. + */ + static MethodSignature ofDeclaredType(XMethodElement method, XType enclosing) { + XMethodType executableType = method.asMemberOf(enclosing); + return new AutoValue_MethodSignature( + XElements.getSimpleName(method), + executableType.getParameterTypes().stream() + .map(XType::getTypeName) + .collect(toImmutableList())); + } + + /** Returns a string in the format: METHOD_NAME(PARAM_TYPE1,PARAM_TYEP2,...) */ + @Override + public final String toString() { + return String.format( + "%s(%s)", name(), parameters().stream().map(Object::toString).collect(joining(","))); + } +} diff --git a/java/dagger/internal/codegen/xprocessing/XElements.java b/java/dagger/internal/codegen/xprocessing/XElements.java index 552be65f9e9..b63d4d93d6e 100644 --- a/java/dagger/internal/codegen/xprocessing/XElements.java +++ b/java/dagger/internal/codegen/xprocessing/XElements.java @@ -47,6 +47,7 @@ import androidx.room.compiler.processing.XTypeElement; import androidx.room.compiler.processing.XTypeParameterElement; import androidx.room.compiler.processing.XVariableElement; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.devtools.ksp.symbol.KSAnnotated; import com.squareup.javapoet.ClassName; @@ -54,6 +55,7 @@ import java.util.Optional; import javax.annotation.Nullable; import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Modifier; // TODO(bcorso): Consider moving these methods into XProcessing library. /** A utility class for {@link XElement} helper methods. */ @@ -383,5 +385,37 @@ public static String packageName(XElement element) { return element.getClosestMemberContainer().asClassName().getPackageName(); } + public static boolean isFinal(XExecutableElement element) { + if (element.isFinal()) { + return true; + } + if (getProcessingEnv(element).getBackend() == XProcessingEnv.Backend.KSP) { + if (toKS(element).getModifiers().contains(com.google.devtools.ksp.symbol.Modifier.FINAL)) { + return true; + } + } + return false; + } + + public static ImmutableList getModifiers(XExecutableElement element) { + ImmutableList.Builder builder = ImmutableList.builder(); + if (isFinal(element)) { + builder.add(Modifier.FINAL); + } else if (element.isAbstract()) { + builder.add(Modifier.ABSTRACT); + } + if (element.isStatic()) { + builder.add(Modifier.STATIC); + } + if (element.isPublic()) { + builder.add(Modifier.PUBLIC); + } else if (element.isPrivate()) { + builder.add(Modifier.PRIVATE); + } else if (element.isProtected()) { + builder.add(Modifier.PROTECTED); + } + return builder.build(); + } + private XElements() {} } diff --git a/javatests/dagger/hilt/android/AndroidManifest.xml b/javatests/dagger/hilt/android/AndroidManifest.xml index a170fb019f7..ed24e45cba5 100644 --- a/javatests/dagger/hilt/android/AndroidManifest.xml +++ b/javatests/dagger/hilt/android/AndroidManifest.xml @@ -93,6 +93,10 @@ android:name=".ViewModelSavedStateOwnerTest$TestActivity" android:exported="false" tools:ignore="MissingClass"/> + scenario = ActivityScenario.launch(TestActivity.class)) { + scenario.onActivity( + activity -> { + assertThat((String) activity.savedStateHandle.get("argument_key")).isNull(); + activity.savedStateHandle.set("other_key", "activity_other_key"); + }); + scenario.recreate(); + scenario.onActivity( + activity -> { + assertThat((String) activity.savedStateHandle.get("argument_key")).isNull(); + assertThat((String) activity.savedStateHandle.get("other_key")) + .isEqualTo("activity_other_key"); + }); + } + } + + @Test + public void firstTimeAccessToActivityRetainedSaveState_inActivityOnDestroy_fails() { + Exception exception = + assertThrows( + NullPointerException.class, + () -> { + try (ActivityScenario scenario = + ActivityScenario.launch(ErrorTestActivity.class)) {} + }); + assertThat(exception) + .hasMessageThat() + .contains( + "The first access to SavedStateHandle should happen between super.onCreate() and" + + " super.onDestroy()"); + } + @Test public void testViewModelSavedState() { try (ActivityScenario scenario = ActivityScenario.launch(TestActivity.class)) { @@ -156,13 +193,30 @@ private MyViewModel getViewModel(ViewModelStoreOwner owner, ViewModelProvider.Fa @AndroidEntryPoint(FragmentActivity.class) public static class TestActivity extends Hilt_ViewModelSavedStateOwnerTest_TestActivity { + @Inject @ActivityRetainedSavedState Provider provider; + SavedStateHandle savedStateHandle; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + savedStateHandle = provider.get(); setContentView(R.layout.navigation_activity); } } + @AndroidEntryPoint(FragmentActivity.class) + public static class ErrorTestActivity + extends Hilt_ViewModelSavedStateOwnerTest_ErrorTestActivity { + @Inject @ActivityRetainedSavedState Provider provider; + + @SuppressWarnings("unused") + @Override + protected void onDestroy() { + super.onDestroy(); + SavedStateHandle savedStateHandle = provider.get(); + } + } + @AndroidEntryPoint(Fragment.class) public static class TestFragment extends Hilt_ViewModelSavedStateOwnerTest_TestFragment { @Override