From 114b886b4aaf3341b4072f184c96cc71d4763f76 Mon Sep 17 00:00:00 2001 From: Govinda Wasserman Date: Wed, 21 Jun 2023 14:19:39 -0400 Subject: Creates application-level and activity-level dagger graphs Provides Activities and BroadcastReceivers with the ability to use injection in their constructors. Adds ChooserActivity, ResolverActivity, IntentForwarderActivity, and ChooserActivityReEnabler to the dagger graph. Test: Existing tests still pass BUG: 242615621 Change-Id: I0ce3bb225ceab59243833535c3776d9d64672d7d --- .../intentresolver/ApplicationComponentOwner.kt | 15 +++++ .../android/intentresolver/ChooserActivity.java | 3 + .../intentresolver/ChooserActivityReEnabler.kt | 3 +- .../intentresolver/IntentForwarderActivity.java | 7 ++ .../IntentResolverAppComponentFactory.kt | 75 ++++++++++++++++++++++ .../intentresolver/IntentResolverApplication.kt | 28 ++++++++ .../android/intentresolver/ResolverActivity.java | 3 + .../intentresolver/dagger/ActivityBinderModule.kt | 30 +++++++++ .../intentresolver/dagger/ActivityModule.kt | 6 ++ .../android/intentresolver/dagger/ActivityScope.kt | 5 ++ .../intentresolver/dagger/ActivitySubComponent.kt | 18 ++++++ java/src/com/android/intentresolver/dagger/App.kt | 5 ++ .../intentresolver/dagger/ApplicationComponent.kt | 23 +++++++ .../intentresolver/dagger/ApplicationModule.kt | 23 +++++++ .../intentresolver/dagger/ReceiverBinderModule.kt | 18 ++++++ java/tests/AndroidManifest.xml | 7 +- .../com/android/intentresolver/TestApplication.kt | 25 +++++++- 17 files changed, 289 insertions(+), 5 deletions(-) create mode 100644 java/src/com/android/intentresolver/ApplicationComponentOwner.kt create mode 100644 java/src/com/android/intentresolver/IntentResolverAppComponentFactory.kt create mode 100644 java/src/com/android/intentresolver/IntentResolverApplication.kt create mode 100644 java/src/com/android/intentresolver/dagger/ActivityBinderModule.kt create mode 100644 java/src/com/android/intentresolver/dagger/ActivityModule.kt create mode 100644 java/src/com/android/intentresolver/dagger/ActivityScope.kt create mode 100644 java/src/com/android/intentresolver/dagger/ActivitySubComponent.kt create mode 100644 java/src/com/android/intentresolver/dagger/App.kt create mode 100644 java/src/com/android/intentresolver/dagger/ApplicationComponent.kt create mode 100644 java/src/com/android/intentresolver/dagger/ApplicationModule.kt create mode 100644 java/src/com/android/intentresolver/dagger/ReceiverBinderModule.kt (limited to 'java') diff --git a/java/src/com/android/intentresolver/ApplicationComponentOwner.kt b/java/src/com/android/intentresolver/ApplicationComponentOwner.kt new file mode 100644 index 00000000..fb39814c --- /dev/null +++ b/java/src/com/android/intentresolver/ApplicationComponentOwner.kt @@ -0,0 +1,15 @@ +package com.android.intentresolver + +import com.android.intentresolver.dagger.ApplicationComponent + +/** + * Interface that should be implemented by the [Application][android.app.Application] object as the + * owner of the [ApplicationComponent]. + */ +interface ApplicationComponentOwner { + /** + * Invokes the given [action] when the [ApplicationComponent] has been created. If it has + * already been created, then it invokes [action] immediately. + */ + fun doWhenApplicationComponentReady(action: (ApplicationComponent) -> Unit) +} diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index a2dff970..6067266d 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -119,6 +119,8 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; +import javax.inject.Inject; + /** * The Chooser Activity handles intent resolution specifically for sharing intents - * for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}. @@ -228,6 +230,7 @@ public class ChooserActivity extends ResolverActivity implements private boolean mExcludeSharedText = false; + @Inject public ChooserActivity() {} @Override diff --git a/java/src/com/android/intentresolver/ChooserActivityReEnabler.kt b/java/src/com/android/intentresolver/ChooserActivityReEnabler.kt index 3236c1be..8c2b3d0d 100644 --- a/java/src/com/android/intentresolver/ChooserActivityReEnabler.kt +++ b/java/src/com/android/intentresolver/ChooserActivityReEnabler.kt @@ -5,11 +5,12 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import javax.inject.Inject /** * Ensures that the unbundled version of [ChooserActivity] does not get stuck in a disabled state. */ -class ChooserActivityReEnabler : BroadcastReceiver() { +class ChooserActivityReEnabler @Inject constructor() : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action == Intent.ACTION_BOOT_COMPLETED) { diff --git a/java/src/com/android/intentresolver/IntentForwarderActivity.java b/java/src/com/android/intentresolver/IntentForwarderActivity.java index 5e8945f1..d69a6c71 100644 --- a/java/src/com/android/intentresolver/IntentForwarderActivity.java +++ b/java/src/com/android/intentresolver/IntentForwarderActivity.java @@ -57,6 +57,8 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import javax.inject.Inject; + /** * This is used in conjunction with * {@link DevicePolicyManager#addCrossProfileIntentFilter} to enable intents to @@ -84,6 +86,11 @@ public class IntentForwarderActivity extends Activity { private MetricsLogger mMetricsLogger; protected ExecutorService mExecutorService; + @Inject + public IntentForwarderActivity() { + super(); + } + @Override protected void onDestroy() { super.onDestroy(); diff --git a/java/src/com/android/intentresolver/IntentResolverAppComponentFactory.kt b/java/src/com/android/intentresolver/IntentResolverAppComponentFactory.kt new file mode 100644 index 00000000..eef49ec4 --- /dev/null +++ b/java/src/com/android/intentresolver/IntentResolverAppComponentFactory.kt @@ -0,0 +1,75 @@ +package com.android.intentresolver + +import android.app.Activity +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Intent +import android.util.Log +import androidx.core.app.AppComponentFactory +import com.android.intentresolver.dagger.ActivitySubComponent +import javax.inject.Inject +import javax.inject.Provider + +/** [AppComponentFactory] that performs dagger injection on Android components. */ +class IntentResolverAppComponentFactory : AppComponentFactory() { + + @set:Inject lateinit var activitySubComponentBuilder: Provider + @set:Inject + lateinit var receivers: Map, @JvmSuppressWildcards Provider> + + override fun instantiateApplicationCompat(cl: ClassLoader, className: String): Application { + val app = super.instantiateApplicationCompat(cl, className) + if (app !is ApplicationComponentOwner) { + throw RuntimeException("App must be ApplicationComponentOwner") + } + app.doWhenApplicationComponentReady { it.inject(this) } + return app + } + + override fun instantiateActivityCompat( + cl: ClassLoader, + className: String, + intent: Intent?, + ): Activity { + return runCatching { + val activities = activitySubComponentBuilder.get().build().activities() + instantiate(className, activities) + } + .onFailure { + if (it is UninitializedPropertyAccessException) { + // This should never happen but if it did it would cause errors that could + // be very difficult to identify, so we log it out of an abundance of + // caution. + Log.e(TAG, "Tried to instantiate $className before AppComponent", it) + } + } + .getOrNull() + ?: super.instantiateActivityCompat(cl, className, intent) + } + + override fun instantiateReceiverCompat( + cl: ClassLoader, + className: String, + intent: Intent?, + ): BroadcastReceiver { + return instantiate(className, receivers) + ?: super.instantiateReceiverCompat(cl, className, intent) + } + + private fun instantiate(className: String, providers: Map, Provider>): T? { + return runCatching { providers[Class.forName(className)]?.get() } + .onFailure { + if (it is UninitializedPropertyAccessException) { + // This should never happen but if it did it would cause errors that could + // be very difficult to identify, so we log it out of an abundance of + // caution. + Log.e(TAG, "Tried to instantiate $className before AppComponent", it) + } + } + .getOrNull() + } + + companion object { + private const val TAG = "IRAppComponentFactory" + } +} diff --git a/java/src/com/android/intentresolver/IntentResolverApplication.kt b/java/src/com/android/intentresolver/IntentResolverApplication.kt new file mode 100644 index 00000000..73d15f3c --- /dev/null +++ b/java/src/com/android/intentresolver/IntentResolverApplication.kt @@ -0,0 +1,28 @@ +package com.android.intentresolver + +import android.app.Application +import com.android.intentresolver.dagger.ApplicationComponent +import com.android.intentresolver.dagger.DaggerApplicationComponent + +/** [Application] that maintains the [ApplicationComponent]. */ +class IntentResolverApplication : Application(), ApplicationComponentOwner { + + private lateinit var applicationComponent: ApplicationComponent + + private val pendingDaggerActions = mutableSetOf<(ApplicationComponent) -> Unit>() + + override fun onCreate() { + super.onCreate() + applicationComponent = DaggerApplicationComponent.builder().application(this).build() + pendingDaggerActions.forEach { it.invoke(applicationComponent) } + pendingDaggerActions.clear() + } + + override fun doWhenApplicationComponentReady(action: (ApplicationComponent) -> Unit) { + if (this::applicationComponent.isInitialized) { + action.invoke(applicationComponent) + } else { + pendingDaggerActions.add(action) + } + } +} diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java index 57871532..dfc5a021 100644 --- a/java/src/com/android/intentresolver/ResolverActivity.java +++ b/java/src/com/android/intentresolver/ResolverActivity.java @@ -125,6 +125,8 @@ import java.util.Objects; import java.util.Set; import java.util.function.Supplier; +import javax.inject.Inject; + /** * This is a copy of ResolverActivity to support IntentResolver's ChooserActivity. This code is * *not* the resolver that is actually triggered by the system right now (you want @@ -135,6 +137,7 @@ import java.util.function.Supplier; public class ResolverActivity extends FragmentActivity implements ResolverListAdapter.ResolverListCommunicator { + @Inject public ResolverActivity() { mIsIntentPicker = getClass().equals(ResolverActivity.class); } diff --git a/java/src/com/android/intentresolver/dagger/ActivityBinderModule.kt b/java/src/com/android/intentresolver/dagger/ActivityBinderModule.kt new file mode 100644 index 00000000..59b08c2c --- /dev/null +++ b/java/src/com/android/intentresolver/dagger/ActivityBinderModule.kt @@ -0,0 +1,30 @@ +package com.android.intentresolver.dagger + +import android.app.Activity +import com.android.intentresolver.ChooserActivity +import com.android.intentresolver.IntentForwarderActivity +import com.android.intentresolver.ResolverActivity +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +/** Injection instructions for injectable [Activities][Activity]. */ +@Module +interface ActivityBinderModule { + + @Binds + @IntoMap + @ClassKey(ChooserActivity::class) + fun bindChooserActivity(activity: ChooserActivity): Activity + + @Binds + @IntoMap + @ClassKey(ResolverActivity::class) + fun bindResolverActivity(activity: ResolverActivity): Activity + + @Binds + @IntoMap + @ClassKey(IntentForwarderActivity::class) + fun bindIntentForwarderActivity(activity: IntentForwarderActivity): Activity +} diff --git a/java/src/com/android/intentresolver/dagger/ActivityModule.kt b/java/src/com/android/intentresolver/dagger/ActivityModule.kt new file mode 100644 index 00000000..a1b7551a --- /dev/null +++ b/java/src/com/android/intentresolver/dagger/ActivityModule.kt @@ -0,0 +1,6 @@ +package com.android.intentresolver.dagger + +import dagger.Module + +/** Injections for the [ActivitySubComponent] */ +@Module(includes = [ActivityBinderModule::class]) interface ActivityModule diff --git a/java/src/com/android/intentresolver/dagger/ActivityScope.kt b/java/src/com/android/intentresolver/dagger/ActivityScope.kt new file mode 100644 index 00000000..dc3a8bef --- /dev/null +++ b/java/src/com/android/intentresolver/dagger/ActivityScope.kt @@ -0,0 +1,5 @@ +package com.android.intentresolver.dagger + +import javax.inject.Scope + +@MustBeDocumented @Retention(AnnotationRetention.RUNTIME) @Scope annotation class ActivityScope diff --git a/java/src/com/android/intentresolver/dagger/ActivitySubComponent.kt b/java/src/com/android/intentresolver/dagger/ActivitySubComponent.kt new file mode 100644 index 00000000..092b9088 --- /dev/null +++ b/java/src/com/android/intentresolver/dagger/ActivitySubComponent.kt @@ -0,0 +1,18 @@ +package com.android.intentresolver.dagger + +import android.app.Activity +import dagger.Subcomponent +import javax.inject.Provider + +/** Subcomponent for injections across the life of an Activity. */ +@ActivityScope +@Subcomponent(modules = [ActivityModule::class]) +interface ActivitySubComponent { + + @Subcomponent.Builder + interface Builder { + fun build(): ActivitySubComponent + } + + fun activities(): Map, @JvmSuppressWildcards Provider> +} diff --git a/java/src/com/android/intentresolver/dagger/App.kt b/java/src/com/android/intentresolver/dagger/App.kt new file mode 100644 index 00000000..a16272e8 --- /dev/null +++ b/java/src/com/android/intentresolver/dagger/App.kt @@ -0,0 +1,5 @@ +package com.android.intentresolver.dagger + +import javax.inject.Qualifier + +@MustBeDocumented @Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class App diff --git a/java/src/com/android/intentresolver/dagger/ApplicationComponent.kt b/java/src/com/android/intentresolver/dagger/ApplicationComponent.kt new file mode 100644 index 00000000..ed337e8b --- /dev/null +++ b/java/src/com/android/intentresolver/dagger/ApplicationComponent.kt @@ -0,0 +1,23 @@ +package com.android.intentresolver.dagger + +import android.app.Application +import com.android.intentresolver.IntentResolverAppComponentFactory +import dagger.BindsInstance +import dagger.Component +import javax.inject.Singleton + +/** Top level component for injections across the life of the process. */ +@Singleton +@Component(modules = [ApplicationModule::class]) +interface ApplicationComponent { + + @Component.Builder + interface Builder { + + @BindsInstance fun application(application: Application): Builder + + fun build(): ApplicationComponent + } + + fun inject(appComponentFactory: IntentResolverAppComponentFactory) +} diff --git a/java/src/com/android/intentresolver/dagger/ApplicationModule.kt b/java/src/com/android/intentresolver/dagger/ApplicationModule.kt new file mode 100644 index 00000000..f9285c25 --- /dev/null +++ b/java/src/com/android/intentresolver/dagger/ApplicationModule.kt @@ -0,0 +1,23 @@ +package com.android.intentresolver.dagger + +import android.app.Application +import android.content.Context +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +/** Injections for the [ApplicationComponent] */ +@Module( + subcomponents = [ActivitySubComponent::class], + includes = [ReceiverBinderModule::class], +) +abstract class ApplicationModule { + + companion object { + @Provides + @Singleton + @App + fun provideApplicationContext(application: Application): Context = + application.applicationContext + } +} diff --git a/java/src/com/android/intentresolver/dagger/ReceiverBinderModule.kt b/java/src/com/android/intentresolver/dagger/ReceiverBinderModule.kt new file mode 100644 index 00000000..1f587f18 --- /dev/null +++ b/java/src/com/android/intentresolver/dagger/ReceiverBinderModule.kt @@ -0,0 +1,18 @@ +package com.android.intentresolver.dagger + +import android.content.BroadcastReceiver +import com.android.intentresolver.ChooserActivityReEnabler +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +/** Injection instructions for injectable [BroadcastReceivers][BroadcastReceiver] */ +@Module +interface ReceiverBinderModule { + + @Binds + @IntoMap + @ClassKey(ChooserActivityReEnabler::class) + fun bindChooserActivityReEnabler(receiver: ChooserActivityReEnabler): BroadcastReceiver +} diff --git a/java/tests/AndroidManifest.xml b/java/tests/AndroidManifest.xml index 05830c4c..b397db5f 100644 --- a/java/tests/AndroidManifest.xml +++ b/java/tests/AndroidManifest.xml @@ -15,7 +15,8 @@ --> + xmlns:tools="http://schemas.android.com/tools" + package="com.android.intentresolver.tests"> @@ -25,7 +26,9 @@ - + diff --git a/java/tests/src/com/android/intentresolver/TestApplication.kt b/java/tests/src/com/android/intentresolver/TestApplication.kt index 849cfbab..f0761fbd 100644 --- a/java/tests/src/com/android/intentresolver/TestApplication.kt +++ b/java/tests/src/com/android/intentresolver/TestApplication.kt @@ -19,9 +19,30 @@ package com.android.intentresolver import android.app.Application import android.content.Context import android.os.UserHandle +import com.android.intentresolver.dagger.ApplicationComponent +import com.android.intentresolver.dagger.DaggerApplicationComponent -class TestApplication : Application() { +class TestApplication : Application(), ApplicationComponentOwner { + + private lateinit var applicationComponent: ApplicationComponent + + private val pendingDaggerActions = mutableSetOf<(ApplicationComponent) -> Unit>() + + override fun onCreate() { + super.onCreate() + applicationComponent = DaggerApplicationComponent.builder().application(this).build() + pendingDaggerActions.forEach { it.invoke(applicationComponent) } + pendingDaggerActions.clear() + } + + override fun doWhenApplicationComponentReady(action: (ApplicationComponent) -> Unit) { + if (this::applicationComponent.isInitialized) { + action.invoke(applicationComponent) + } else { + pendingDaggerActions.add(action) + } + } // return the current context as a work profile doesn't really exist in these tests override fun createContextAsUser(user: UserHandle, flags: Int): Context = this -} \ No newline at end of file +} -- cgit v1.2.3-59-g8ed1b