summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
author Mark Renouf <mrenouf@google.com> 2023-07-20 14:06:27 -0400
committer Mark Renouf <mrenouf@google.com> 2023-08-08 16:39:46 +0000
commit35a34bcaf236597703208cf0472cb268b42abb1c (patch)
treea9e27ad3b38d606301ded87bdaeb7fd234706ba3 /java
parent5b05c285bc00a34f847037dfe176b6f478eb768d (diff)
Adds Dagger support for ViewModel scope
Dependencies may now be bound to ViewModelScope which survives across Activity configuration restarts. ChooserViewModel is available in ChooserActivity. ViewModel components should be provided by ViewModelComponent to wire up the core parts of the app. A @ViewModel CoroutineScope is also available to inject from within ViewModelScope. * Injects [Resolver|Chooser](Wrapper)Activity in tests * Injects Intent args (Bundle) into ViewModel * Injects Referrer (Uri) into ViewModel Adjusts module structure slightly to accomodate the current state. Further restructuring will be done to simplify the test modules & components required. Test: atest IntentResolverUnitTests Change-Id: Ia249cf07796a9993f2cd021930c52faadb046ca8
Diffstat (limited to 'java')
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java37
-rw-r--r--java/src/com/android/intentresolver/IntentResolverApplication.kt6
-rw-r--r--java/src/com/android/intentresolver/dagger/ActivityBinderModule.kt3
-rw-r--r--java/src/com/android/intentresolver/dagger/ActivityComponent.kt21
-rw-r--r--java/src/com/android/intentresolver/dagger/ActivityModule.kt4
-rw-r--r--java/src/com/android/intentresolver/dagger/ActivityScope.kt5
-rw-r--r--java/src/com/android/intentresolver/dagger/ActivitySubComponent.kt18
-rw-r--r--java/src/com/android/intentresolver/dagger/App.kt5
-rw-r--r--java/src/com/android/intentresolver/dagger/ApplicationComponent.kt4
-rw-r--r--java/src/com/android/intentresolver/dagger/ApplicationModule.kt18
-rw-r--r--java/src/com/android/intentresolver/dagger/CoroutinesModule.kt51
-rw-r--r--java/src/com/android/intentresolver/dagger/InjectedAppComponentFactory.kt (renamed from java/src/com/android/intentresolver/IntentResolverAppComponentFactory.kt)31
-rw-r--r--java/src/com/android/intentresolver/dagger/InjectedViewModelFactory.kt84
-rw-r--r--java/src/com/android/intentresolver/dagger/ViewModelBinderModule.kt34
-rw-r--r--java/src/com/android/intentresolver/dagger/ViewModelComponent.kt57
-rw-r--r--java/src/com/android/intentresolver/dagger/ViewModelModule.kt6
-rw-r--r--java/src/com/android/intentresolver/dagger/qualifiers/Qualifiers.kt37
-rw-r--r--java/src/com/android/intentresolver/ui/ChooserViewModel.kt34
-rw-r--r--java/tests/Android.bp11
-rw-r--r--java/tests/AndroidManifest.xml5
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java8
-rw-r--r--java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java3
-rw-r--r--java/tests/src/com/android/intentresolver/TestApplication.kt26
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java5
-rw-r--r--java/tests/src/com/android/intentresolver/dagger/TestActivityBinderModule.kt40
-rw-r--r--java/tests/src/com/android/intentresolver/dagger/TestActivityComponent.kt30
-rw-r--r--java/tests/src/com/android/intentresolver/dagger/TestApplicationComponent.kt34
-rw-r--r--java/tests/src/com/android/intentresolver/dagger/TestApplicationModule.kt38
-rw-r--r--java/tests/src/com/android/intentresolver/dagger/TestViewModelComponent.kt29
-rw-r--r--java/tests/src/com/android/intentresolver/dagger/TestViewModelModule.kt6
30 files changed, 607 insertions, 83 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index b041475b..8edbba08 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -72,6 +72,7 @@ import android.view.animation.LinearInterpolator;
import android.widget.TextView;
import androidx.annotation.MainThread;
+import androidx.lifecycle.HasDefaultViewModelProviderFactory;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -87,6 +88,8 @@ import com.android.intentresolver.contentpreview.BasePreviewViewModel;
import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl;
import com.android.intentresolver.contentpreview.PreviewViewModel;
+import com.android.intentresolver.dagger.InjectedViewModelFactory;
+import com.android.intentresolver.dagger.ViewModelComponent;
import com.android.intentresolver.flags.FeatureFlagRepository;
import com.android.intentresolver.flags.FeatureFlagRepositoryFactory;
import com.android.intentresolver.grid.ChooserGridAdapter;
@@ -99,11 +102,14 @@ import com.android.intentresolver.model.AppPredictionServiceResolverComparator;
import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
import com.android.intentresolver.shortcuts.AppPredictorFactory;
import com.android.intentresolver.shortcuts.ShortcutLoader;
+import com.android.intentresolver.ui.ChooserViewModel;
import com.android.intentresolver.widget.ImagePreviewView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import org.jetbrains.annotations.NotNull;
+
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -127,7 +133,7 @@ import javax.inject.Inject;
*
*/
public class ChooserActivity extends ResolverActivity implements
- ResolverListAdapter.ResolverListCommunicator {
+ ResolverListAdapter.ResolverListCommunicator, HasDefaultViewModelProviderFactory {
private static final String TAG = "ChooserActivity";
/**
@@ -167,6 +173,11 @@ public class ChooserActivity extends ResolverActivity implements
private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1;
private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2;
+ private ViewModelProvider.Factory mViewModelFactory;
+ private final ViewModelComponent.Builder mViewModelComponentBuilder;
+
+ private ChooserViewModel mViewModel;
+
@IntDef(flag = false, prefix = { "TARGET_TYPE_" }, value = {
TARGET_TYPE_DEFAULT,
TARGET_TYPE_CHOOSER_TARGET,
@@ -229,16 +240,32 @@ public class ChooserActivity extends ResolverActivity implements
private boolean mExcludeSharedText = false;
@Inject
- public ChooserActivity() {}
+ public ChooserActivity(ViewModelComponent.Builder builder) {
+ mViewModelComponentBuilder = builder;
+ }
+
+ @NotNull
+ @Override
+ public final ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
+ if (mViewModelFactory == null) {
+ mViewModelFactory = new InjectedViewModelFactory(mViewModelComponentBuilder,
+ getDefaultViewModelCreationExtras(),
+ getReferrer());
+ }
+ return mViewModelFactory;
+ }
@Override
protected void onCreate(Bundle savedInstanceState) {
+ Log.d(TAG, "onCreate");
Tracer.INSTANCE.markLaunched();
final long intentReceivedTime = System.currentTimeMillis();
mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
getEventLog().logSharesheetTriggered();
+ mViewModel = new ViewModelProvider(this).get(ChooserViewModel.class);
+
mFeatureFlagRepository = createFeatureFlagRepository();
mIntegratedDeviceComponents = getIntegratedDeviceComponents();
@@ -255,7 +282,9 @@ public class ChooserActivity extends ResolverActivity implements
return;
}
- mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class);
+ // Note: Uses parent ViewModelProvider.Factory because RefinementManager is not injectable
+ mRefinementManager = new ViewModelProvider(this, super.getDefaultViewModelProviderFactory())
+ .get(ChooserRefinementManager.class);
mRefinementManager.getRefinementCompletion().observe(this, completion -> {
if (completion.consume()) {
@@ -277,7 +306,7 @@ public class ChooserActivity extends ResolverActivity implements
BasePreviewViewModel previewViewModel =
new ViewModelProvider(this, createPreviewViewModelFactory())
- .get(BasePreviewViewModel.class);
+ .get(PreviewViewModel.class);
mChooserContentPreviewUi = new ChooserContentPreviewUi(
getLifecycle(),
previewViewModel.createOrReuseProvider(mChooserRequest),
diff --git a/java/src/com/android/intentresolver/IntentResolverApplication.kt b/java/src/com/android/intentresolver/IntentResolverApplication.kt
index 73d15f3c..61df7fff 100644
--- a/java/src/com/android/intentresolver/IntentResolverApplication.kt
+++ b/java/src/com/android/intentresolver/IntentResolverApplication.kt
@@ -5,15 +5,17 @@ import com.android.intentresolver.dagger.ApplicationComponent
import com.android.intentresolver.dagger.DaggerApplicationComponent
/** [Application] that maintains the [ApplicationComponent]. */
-class IntentResolverApplication : Application(), ApplicationComponentOwner {
+open class IntentResolverApplication : Application(), ApplicationComponentOwner {
private lateinit var applicationComponent: ApplicationComponent
private val pendingDaggerActions = mutableSetOf<(ApplicationComponent) -> Unit>()
+ open fun createApplicationComponentBuilder() = DaggerApplicationComponent.builder()
+
override fun onCreate() {
super.onCreate()
- applicationComponent = DaggerApplicationComponent.builder().application(this).build()
+ applicationComponent = createApplicationComponentBuilder().application(this).build()
pendingDaggerActions.forEach { it.invoke(applicationComponent) }
pendingDaggerActions.clear()
}
diff --git a/java/src/com/android/intentresolver/dagger/ActivityBinderModule.kt b/java/src/com/android/intentresolver/dagger/ActivityBinderModule.kt
index 59b08c2c..7c997ef7 100644
--- a/java/src/com/android/intentresolver/dagger/ActivityBinderModule.kt
+++ b/java/src/com/android/intentresolver/dagger/ActivityBinderModule.kt
@@ -16,15 +16,18 @@ interface ActivityBinderModule {
@Binds
@IntoMap
@ClassKey(ChooserActivity::class)
+ @ActivityScope
fun bindChooserActivity(activity: ChooserActivity): Activity
@Binds
@IntoMap
@ClassKey(ResolverActivity::class)
+ @ActivityScope
fun bindResolverActivity(activity: ResolverActivity): Activity
@Binds
@IntoMap
@ClassKey(IntentForwarderActivity::class)
+ @ActivityScope
fun bindIntentForwarderActivity(activity: IntentForwarderActivity): Activity
}
diff --git a/java/src/com/android/intentresolver/dagger/ActivityComponent.kt b/java/src/com/android/intentresolver/dagger/ActivityComponent.kt
new file mode 100644
index 00000000..bf5ff761
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/ActivityComponent.kt
@@ -0,0 +1,21 @@
+package com.android.intentresolver.dagger
+
+import android.app.Activity
+import dagger.Subcomponent
+import javax.inject.Provider
+import javax.inject.Scope
+
+@MustBeDocumented @Retention(AnnotationRetention.RUNTIME) @Scope annotation class ActivityScope
+
+/** Subcomponent for injections across the life of an Activity. */
+@ActivityScope
+@Subcomponent(modules = [ActivityModule::class, ActivityBinderModule::class])
+interface ActivityComponent {
+
+ @Subcomponent.Factory
+ interface Factory {
+ fun create(): ActivityComponent
+ }
+
+ fun activities(): Map<Class<*>, @JvmSuppressWildcards Provider<Activity>>
+}
diff --git a/java/src/com/android/intentresolver/dagger/ActivityModule.kt b/java/src/com/android/intentresolver/dagger/ActivityModule.kt
index a1b7551a..f6a2229d 100644
--- a/java/src/com/android/intentresolver/dagger/ActivityModule.kt
+++ b/java/src/com/android/intentresolver/dagger/ActivityModule.kt
@@ -2,5 +2,5 @@ package com.android.intentresolver.dagger
import dagger.Module
-/** Injections for the [ActivitySubComponent] */
-@Module(includes = [ActivityBinderModule::class]) interface ActivityModule
+/** Bindings provided to [@ActivityScope][ActivityScope]. */
+@Module interface ActivityModule
diff --git a/java/src/com/android/intentresolver/dagger/ActivityScope.kt b/java/src/com/android/intentresolver/dagger/ActivityScope.kt
deleted file mode 100644
index dc3a8bef..00000000
--- a/java/src/com/android/intentresolver/dagger/ActivityScope.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-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
deleted file mode 100644
index 092b9088..00000000
--- a/java/src/com/android/intentresolver/dagger/ActivitySubComponent.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-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<Class<*>, @JvmSuppressWildcards Provider<Activity>>
-}
diff --git a/java/src/com/android/intentresolver/dagger/App.kt b/java/src/com/android/intentresolver/dagger/App.kt
deleted file mode 100644
index a16272e8..00000000
--- a/java/src/com/android/intentresolver/dagger/App.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-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
index ed337e8b..9fc57712 100644
--- a/java/src/com/android/intentresolver/dagger/ApplicationComponent.kt
+++ b/java/src/com/android/intentresolver/dagger/ApplicationComponent.kt
@@ -1,7 +1,6 @@
package com.android.intentresolver.dagger
import android.app.Application
-import com.android.intentresolver.IntentResolverAppComponentFactory
import dagger.BindsInstance
import dagger.Component
import javax.inject.Singleton
@@ -13,11 +12,10 @@ interface ApplicationComponent {
@Component.Builder
interface Builder {
-
@BindsInstance fun application(application: Application): Builder
fun build(): ApplicationComponent
}
- fun inject(appComponentFactory: IntentResolverAppComponentFactory)
+ fun inject(appComponentFactory: InjectedAppComponentFactory)
}
diff --git a/java/src/com/android/intentresolver/dagger/ApplicationModule.kt b/java/src/com/android/intentresolver/dagger/ApplicationModule.kt
index f9285c25..4986d7e1 100644
--- a/java/src/com/android/intentresolver/dagger/ApplicationModule.kt
+++ b/java/src/com/android/intentresolver/dagger/ApplicationModule.kt
@@ -2,22 +2,28 @@ package com.android.intentresolver.dagger
import android.app.Application
import android.content.Context
+import com.android.intentresolver.dagger.qualifiers.App
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
-/** Injections for the [ApplicationComponent] */
+/**
+ * Bindings provided to [ApplicationComponent] and children.
+ *
+ * These are all @Singleton scope, one for the duration of the process.
+ */
@Module(
- subcomponents = [ActivitySubComponent::class],
- includes = [ReceiverBinderModule::class],
+ subcomponents = [ActivityComponent::class, ViewModelComponent::class],
+ includes = [ReceiverBinderModule::class, CoroutinesModule::class],
)
-abstract class ApplicationModule {
+interface ApplicationModule {
companion object {
+
+ @JvmStatic
@Provides
@Singleton
@App
- fun provideApplicationContext(application: Application): Context =
- application.applicationContext
+ fun applicationContext(app: Application): Context = app.applicationContext
}
}
diff --git a/java/src/com/android/intentresolver/dagger/CoroutinesModule.kt b/java/src/com/android/intentresolver/dagger/CoroutinesModule.kt
new file mode 100644
index 00000000..5fda2c30
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/CoroutinesModule.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.intentresolver.dagger
+
+import com.android.intentresolver.dagger.qualifiers.Background
+import com.android.intentresolver.dagger.qualifiers.Main
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+
+@Module
+interface CoroutinesModule {
+ companion object {
+ @JvmStatic
+ @Provides
+ @Singleton
+ @Main
+ fun mainDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
+
+ @JvmStatic
+ @Provides
+ @Singleton
+ @Main
+ fun mainCoroutineScope(@Main mainDispatcher: CoroutineDispatcher) =
+ CoroutineScope(SupervisorJob() + mainDispatcher)
+
+ @JvmStatic
+ @Provides
+ @Singleton
+ @Background
+ fun backgroundDispatcher(): CoroutineDispatcher = Dispatchers.IO
+ }
+}
diff --git a/java/src/com/android/intentresolver/IntentResolverAppComponentFactory.kt b/java/src/com/android/intentresolver/dagger/InjectedAppComponentFactory.kt
index eef49ec4..db209ef0 100644
--- a/java/src/com/android/intentresolver/IntentResolverAppComponentFactory.kt
+++ b/java/src/com/android/intentresolver/dagger/InjectedAppComponentFactory.kt
@@ -1,4 +1,20 @@
-package com.android.intentresolver
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.intentresolver.dagger
import android.app.Activity
import android.app.Application
@@ -6,14 +22,15 @@ import android.content.BroadcastReceiver
import android.content.Intent
import android.util.Log
import androidx.core.app.AppComponentFactory
-import com.android.intentresolver.dagger.ActivitySubComponent
+import com.android.intentresolver.ApplicationComponentOwner
import javax.inject.Inject
import javax.inject.Provider
-/** [AppComponentFactory] that performs dagger injection on Android components. */
-class IntentResolverAppComponentFactory : AppComponentFactory() {
+/** Provides instances of application components, delegates construction to Dagger. */
+class InjectedAppComponentFactory : AppComponentFactory() {
+
+ @set:Inject lateinit var activityComponentBuilder: ActivityComponent.Factory
- @set:Inject lateinit var activitySubComponentBuilder: Provider<ActivitySubComponent.Builder>
@set:Inject
lateinit var receivers: Map<Class<*>, @JvmSuppressWildcards Provider<BroadcastReceiver>>
@@ -32,7 +49,7 @@ class IntentResolverAppComponentFactory : AppComponentFactory() {
intent: Intent?,
): Activity {
return runCatching {
- val activities = activitySubComponentBuilder.get().build().activities()
+ val activities = activityComponentBuilder.create().activities()
instantiate(className, activities)
}
.onFailure {
@@ -70,6 +87,6 @@ class IntentResolverAppComponentFactory : AppComponentFactory() {
}
companion object {
- private const val TAG = "IRAppComponentFactory"
+ private const val TAG = "AppComponentFactory"
}
}
diff --git a/java/src/com/android/intentresolver/dagger/InjectedViewModelFactory.kt b/java/src/com/android/intentresolver/dagger/InjectedViewModelFactory.kt
new file mode 100644
index 00000000..f0906d3e
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/InjectedViewModelFactory.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.intentresolver.dagger
+
+import android.net.Uri
+import android.os.Bundle
+import androidx.lifecycle.DEFAULT_ARGS_KEY
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewmodel.CreationExtras
+import java.io.Closeable
+import javax.inject.Provider
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.isActive
+
+/** Instantiates new ViewModel instances using Dagger. */
+class InjectedViewModelFactory(
+ private val viewModelComponentBuilder: ViewModelComponent.Builder,
+ creationExtras: CreationExtras,
+ private val referrer: Uri,
+) : ViewModelProvider.Factory {
+
+ private val defaultArgs = creationExtras[DEFAULT_ARGS_KEY] ?: Bundle()
+
+ private fun viewModelScope(viewModelClass: Class<*>) =
+ CloseableCoroutineScope(
+ SupervisorJob() + CoroutineName(viewModelClass.simpleName) + Dispatchers.Main.immediate
+ )
+
+ private fun <T> newViewModel(
+ providerMap: Map<Class<*>, Provider<ViewModel>>,
+ modelClass: Class<T>
+ ): T {
+ val provider =
+ providerMap[modelClass]
+ ?: error(
+ "Unable to create an instance of $modelClass. " +
+ "Does the class have a binding in ViewModelComponent?"
+ )
+ return modelClass.cast(provider.get())
+ }
+
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ val viewModelScope = viewModelScope(modelClass)
+ val viewModelComponent =
+ viewModelComponentBuilder
+ .coroutineScope(viewModelScope)
+ .intentExtras(defaultArgs)
+ .referrer(referrer)
+ .build()
+ val viewModel = newViewModel(viewModelComponent.viewModels(), modelClass)
+ viewModel.addCloseable(viewModelScope)
+ return viewModel
+ }
+}
+
+internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
+ override val coroutineContext: CoroutineContext = context
+
+ override fun close() {
+ if (isActive) {
+ coroutineContext.cancel()
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/dagger/ViewModelBinderModule.kt b/java/src/com/android/intentresolver/dagger/ViewModelBinderModule.kt
new file mode 100644
index 00000000..91ba039c
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/ViewModelBinderModule.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.intentresolver.dagger
+
+import androidx.lifecycle.ViewModel
+import com.android.intentresolver.ui.ChooserViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+/** Defines a map of injectable ViewModel classes. */
+@Module
+interface ViewModelBinderModule {
+ @Binds
+ @IntoMap
+ @ClassKey(ChooserViewModel::class)
+ @ViewModelScope
+ fun chooserViewModel(viewModel: ChooserViewModel): ViewModel
+}
diff --git a/java/src/com/android/intentresolver/dagger/ViewModelComponent.kt b/java/src/com/android/intentresolver/dagger/ViewModelComponent.kt
new file mode 100644
index 00000000..3e2e2681
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/ViewModelComponent.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.intentresolver.dagger
+
+import android.net.Uri
+import android.os.Bundle
+import com.android.intentresolver.dagger.qualifiers.Referrer
+import com.android.intentresolver.dagger.qualifiers.ViewModel
+import dagger.BindsInstance
+import dagger.Subcomponent
+import javax.inject.Provider
+import javax.inject.Scope
+import kotlin.annotation.AnnotationRetention.RUNTIME
+import kotlinx.coroutines.CoroutineScope
+
+@Scope @Retention(RUNTIME) @MustBeDocumented annotation class ViewModelScope
+
+/**
+ * Provides dependencies within [ViewModelScope] within a [ViewModel].
+ *
+ * @see InjectedViewModelFactory
+ */
+@ViewModelScope
+@Subcomponent(modules = [ViewModelModule::class, ViewModelBinderModule::class])
+interface ViewModelComponent {
+
+ /**
+ * Binds instance values from the creating Activity to make them available for injection within
+ * [ViewModelScope].
+ */
+ @Subcomponent.Builder
+ interface Builder {
+ @BindsInstance fun intentExtras(@ViewModel intentExtras: Bundle): Builder
+
+ @BindsInstance fun referrer(@Referrer uri: Uri): Builder
+
+ @BindsInstance fun coroutineScope(@ViewModel scope: CoroutineScope): Builder
+
+ fun build(): ViewModelComponent
+ }
+
+ fun viewModels(): Map<Class<*>, @JvmSuppressWildcards Provider<androidx.lifecycle.ViewModel>>
+}
diff --git a/java/src/com/android/intentresolver/dagger/ViewModelModule.kt b/java/src/com/android/intentresolver/dagger/ViewModelModule.kt
new file mode 100644
index 00000000..23320311
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/ViewModelModule.kt
@@ -0,0 +1,6 @@
+package com.android.intentresolver.dagger
+
+import dagger.Module
+
+/** Provides bindings shared among components within [@ViewModelScope][ViewModelScope]. */
+@Module abstract class ViewModelModule
diff --git a/java/src/com/android/intentresolver/dagger/qualifiers/Qualifiers.kt b/java/src/com/android/intentresolver/dagger/qualifiers/Qualifiers.kt
new file mode 100644
index 00000000..fa50170e
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/qualifiers/Qualifiers.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.intentresolver.dagger.qualifiers
+
+import javax.inject.Qualifier
+
+// Note: 'qualifiers' package avoids name collisions in Dagger code.
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class App
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Activity
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ViewModel
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Main
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Background
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Delegate
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Default
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Referrer
diff --git a/java/src/com/android/intentresolver/ui/ChooserViewModel.kt b/java/src/com/android/intentresolver/ui/ChooserViewModel.kt
new file mode 100644
index 00000000..817f0b6c
--- /dev/null
+++ b/java/src/com/android/intentresolver/ui/ChooserViewModel.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.intentresolver.ui
+
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import com.android.intentresolver.dagger.qualifiers.ViewModel as ViewModelQualifier
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.launch
+
+const val TAG = "ChooserViewModel"
+
+/** The primary container for ViewModelScope dependencies. */
+class ChooserViewModel
+@Inject
+constructor(
+ @ViewModelQualifier val viewModelScope: CoroutineScope,
+) : ViewModel() \ No newline at end of file
diff --git a/java/tests/Android.bp b/java/tests/Android.bp
index c381d0a8..bb287eb2 100644
--- a/java/tests/Android.bp
+++ b/java/tests/Android.bp
@@ -19,18 +19,21 @@ android_test {
static_libs: [
"IntentResolver-core",
- "androidx.test.rules",
+ "androidx.test.core",
"androidx.test.ext.junit",
+ "androidx.test.ext.truth",
"androidx.test.espresso.contrib",
- "mockito-target-minus-junit4",
"androidx.test.espresso.core",
+ "androidx.test.rules",
"androidx.lifecycle_lifecycle-common-java8",
"androidx.lifecycle_lifecycle-extensions",
"androidx.lifecycle_lifecycle-runtime-ktx",
- "truth-prebuilt",
- "testables",
"kotlinx_coroutines_test",
+ "mockito-target-minus-junit4",
+ "testables",
+ "truth-prebuilt",
],
+ plugins: ["dagger2-compiler"],
test_suites: ["general-tests"],
sdk_version: "core_platform",
compile_multilib: "both",
diff --git a/java/tests/AndroidManifest.xml b/java/tests/AndroidManifest.xml
index b397db5f..9f8dd41c 100644
--- a/java/tests/AndroidManifest.xml
+++ b/java/tests/AndroidManifest.xml
@@ -27,8 +27,9 @@
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<application
- tools:replace="android:name"
- android:name="com.android.intentresolver.TestApplication" >
+ android:name="com.android.intentresolver.TestApplication"
+ tools:replace="android:appComponentFactory"
+ android:appComponentFactory="com.android.intentresolver.dagger.InjectedAppComponentFactory">
<uses-library android:name="android.test.runner" />
<activity android:name="com.android.intentresolver.ChooserWrapperActivity" />
<activity android:name="com.android.intentresolver.ResolverWrapperActivity" />
diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
index 8608cf72..49305a6c 100644
--- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
+++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
@@ -37,6 +37,7 @@ import androidx.lifecycle.ViewModelProvider;
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.dagger.TestViewModelComponent;
import com.android.intentresolver.flags.FeatureFlagRepository;
import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.intentresolver.icons.TargetDataLoader;
@@ -47,6 +48,8 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import java.util.List;
import java.util.function.Consumer;
+import javax.inject.Inject;
+
/**
* Simple wrapper around chooser activity to be able to initiate it under test. For more
* information, see {@code com.android.internal.app.ChooserWrapperActivity}.
@@ -56,6 +59,11 @@ public class ChooserWrapperActivity
static final ChooserActivityOverrideData sOverrides = ChooserActivityOverrideData.getInstance();
private UsageStatsManager mUsm;
+ @Inject
+ public ChooserWrapperActivity(TestViewModelComponent.Builder builder) {
+ super(builder);
+ }
+
// ResolverActivity (the base class of ChooserActivity) inspects the launched-from UID at
// onCreate and needs to see some non-negative value in the test.
@Override
diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java
index 401ede26..11e7dffd 100644
--- a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java
+++ b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java
@@ -44,6 +44,8 @@ import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
+import javax.inject.Inject;
+
/*
* Simple wrapper around chooser activity to be able to initiate it under test
*/
@@ -53,6 +55,7 @@ public class ResolverWrapperActivity extends ResolverActivity {
private final CountingIdlingResource mLabelIdlingResource =
new CountingIdlingResource("LoadLabelTask");
+ @Inject
public ResolverWrapperActivity() {
super(/* isIntentPicker= */ true);
}
diff --git a/java/tests/src/com/android/intentresolver/TestApplication.kt b/java/tests/src/com/android/intentresolver/TestApplication.kt
index f0761fbd..4f5aefb9 100644
--- a/java/tests/src/com/android/intentresolver/TestApplication.kt
+++ b/java/tests/src/com/android/intentresolver/TestApplication.kt
@@ -16,32 +16,12 @@
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
+import com.android.intentresolver.dagger.DaggerTestApplicationComponent
-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)
- }
- }
+class TestApplication : IntentResolverApplication() {
+ override fun createApplicationComponentBuilder() = DaggerTestApplicationComponent.builder()
// return the current context as a work profile doesn't really exist in these tests
override fun createContextAsUser(user: UserHandle, flags: Int): Context = this
diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
index ecd05b46..28a45051 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
@@ -864,7 +864,7 @@ public class UnbundledChooserActivityTest {
}
@Test
- public void copyTextToClipboard() throws Exception {
+ public void copyTextToClipboard() {
Intent sendIntent = createSendTextIntent();
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -879,7 +879,8 @@ public class UnbundledChooserActivityTest {
ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(
Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
- assertThat("testing intent sending", is(clipData.getItemAt(0).getText()));
+ assertThat(clipData).isNotNull();
+ assertThat(clipData.getItemAt(0).getText()).isEqualTo("testing intent sending");
ClipDescription clipDescription = clipData.getDescription();
assertThat("text/plain", is(clipDescription.getMimeType(0)));
diff --git a/java/tests/src/com/android/intentresolver/dagger/TestActivityBinderModule.kt b/java/tests/src/com/android/intentresolver/dagger/TestActivityBinderModule.kt
new file mode 100644
index 00000000..c08bc3b2
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/dagger/TestActivityBinderModule.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.intentresolver.dagger
+
+import android.app.Activity
+import com.android.intentresolver.ChooserWrapperActivity
+import com.android.intentresolver.ResolverWrapperActivity
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+interface TestActivityBinderModule {
+ @Binds
+ @IntoMap
+ @ClassKey(ResolverWrapperActivity::class)
+ @ActivityScope
+ fun resolverWrapperActivity(activity: ResolverWrapperActivity): Activity
+
+ @Binds
+ @IntoMap
+ @ClassKey(ChooserWrapperActivity::class)
+ @ActivityScope
+ fun chooserWrapperActivity(activity: ChooserWrapperActivity): Activity
+}
diff --git a/java/tests/src/com/android/intentresolver/dagger/TestActivityComponent.kt b/java/tests/src/com/android/intentresolver/dagger/TestActivityComponent.kt
new file mode 100644
index 00000000..4416c852
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/dagger/TestActivityComponent.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.intentresolver.dagger
+
+import dagger.Subcomponent
+
+@ActivityScope
+@Subcomponent(
+ modules = [ActivityModule::class, ActivityBinderModule::class, TestActivityBinderModule::class]
+)
+interface TestActivityComponent : ActivityComponent {
+ @Subcomponent.Factory
+ interface Factory : ActivityComponent.Factory {
+ override fun create(): TestActivityComponent
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/dagger/TestApplicationComponent.kt b/java/tests/src/com/android/intentresolver/dagger/TestApplicationComponent.kt
new file mode 100644
index 00000000..224c46c6
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/dagger/TestApplicationComponent.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.intentresolver.dagger
+
+import android.app.Application
+import dagger.BindsInstance
+import dagger.Component
+import javax.inject.Singleton
+
+@Singleton
+@Component(modules = [TestApplicationModule::class])
+interface TestApplicationComponent : ApplicationComponent {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder {
+ @BindsInstance
+ override fun application(application: Application): TestApplicationComponent.Builder
+
+ override fun build(): TestApplicationComponent
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/dagger/TestApplicationModule.kt b/java/tests/src/com/android/intentresolver/dagger/TestApplicationModule.kt
new file mode 100644
index 00000000..714748d2
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/dagger/TestApplicationModule.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.intentresolver.dagger
+
+import dagger.Binds
+import dagger.Module
+import javax.inject.Singleton
+
+@Module(
+ subcomponents = [TestActivityComponent::class, TestViewModelComponent::class],
+ includes = [ReceiverBinderModule::class, CoroutinesModule::class]
+)
+interface TestApplicationModule : ApplicationModule {
+
+ /** Required to support field injection of [InjectedAppComponentFactory] */
+ @Binds
+ @Singleton
+ fun activityComponent(component: TestActivityComponent.Factory): ActivityComponent.Factory
+
+ /** Required to support injection into Activity instances */
+ @Binds
+ @Singleton
+ fun viewModelComponent(component: TestViewModelComponent.Builder): ViewModelComponent.Builder
+}
diff --git a/java/tests/src/com/android/intentresolver/dagger/TestViewModelComponent.kt b/java/tests/src/com/android/intentresolver/dagger/TestViewModelComponent.kt
new file mode 100644
index 00000000..539b3f36
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/dagger/TestViewModelComponent.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.intentresolver.dagger
+
+import dagger.Subcomponent
+
+/** A ViewModelComponent for tests which replaces ViewModelModule -> TestViewModelModule */
+@ViewModelScope
+@Subcomponent(modules = [TestViewModelModule::class, ViewModelBinderModule::class])
+interface TestViewModelComponent : ViewModelComponent {
+ @Subcomponent.Builder
+ interface Builder : ViewModelComponent.Builder {
+ override fun build(): TestViewModelComponent
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/dagger/TestViewModelModule.kt b/java/tests/src/com/android/intentresolver/dagger/TestViewModelModule.kt
new file mode 100644
index 00000000..28f4fa73
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/dagger/TestViewModelModule.kt
@@ -0,0 +1,6 @@
+package com.android.intentresolver.dagger
+
+import dagger.Module
+
+/** Provides bindings shared among components within [@ViewModelScope][ViewModelScope]. */
+@Module abstract class TestViewModelModule