summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
author Mark Renouf <mrenouf@google.com> 2023-08-08 19:03:40 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2023-08-08 19:03:40 +0000
commitfcfe392286b58180f58e4244879d65bbb5a51617 (patch)
treecdbe544ef61710190909cc1161eb4aa8fc83146d /java
parentc9fcdb3f856038348950c5a3a71885174cad62f1 (diff)
parent35a34bcaf236597703208cf0472cb268b42abb1c (diff)
Merge "Adds Dagger support for ViewModel scope" into udc-qpr-dev
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