Introduce ChooserRequestRepository

- Replace TargetIntentRepository with ChooserRequestRepository, using the ChooserRequest as the source of truth for the target intent.

  - Caveat: custom actions are tracked separately to facilitate with testing; long-term we will want to update/replace ChooserRequest so that it isn't relying on un-mockable/un-fakeable types.

- Remove concept of "initialization" from repositories.

  - Usages are better captured as "events", and so are handled in interactor codepaths that flow *into* the repositories.

Bug: 302691505
Flag: ACONFIG android.service.chooser.chooser_payload_toggling DEVELOPMENT
Test: atest IntentResolver-tests-unit
Change-Id: I8451a495478dbe750a44e6b049d4751fa7badf81
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/SelectionRecord.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/SelectionRecord.kt
deleted file mode 100644
index c8fcb9d..0000000
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/SelectionRecord.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright 2024 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.contentpreview.payloadtoggle.data.model
-
-import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
-
-data class SelectionRecord(
-    val type: SelectionRecordType,
-    val selection: Set<PreviewModel>,
-)
-
-enum class SelectionRecordType {
-    Uninitialized,
-    Initial,
-    Updated,
-}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/ChooserParamsUpdateRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/ChooserParamsUpdateRepository.kt
deleted file mode 100644
index 1a4f2b8..0000000
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/ChooserParamsUpdateRepository.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2024 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.contentpreview.payloadtoggle.data.repository
-
-import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate
-import dagger.hilt.android.scopes.ViewModelScoped
-import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableStateFlow
-
-/** Chooser parameters Updates received from the sharing application payload change callback */
-// TODO: a scaffolding repository to deliver chooser parameter updates before we developed some
-//  other, more thought-through solution.
-@ViewModelScoped
-class ChooserParamsUpdateRepository @Inject constructor() {
-    val updates = MutableStateFlow<ShareouselUpdate?>(null)
-
-    fun setUpdates(update: ShareouselUpdate) {
-        updates.tryEmit(update)
-    }
-}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PendingSelectionCallbackRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PendingSelectionCallbackRepository.kt
new file mode 100644
index 0000000..1745cd9
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PendingSelectionCallbackRepository.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 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.contentpreview.payloadtoggle.data.repository
+
+import android.content.Intent
+import dagger.hilt.android.scopes.ActivityRetainedScoped
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/** Tracks active async communication with sharing app to notify of target intent update. */
+@ActivityRetainedScoped
+class PendingSelectionCallbackRepository @Inject constructor() {
+    /**
+     * The target [Intent] that is has an active update request with the sharing app, or `null` if
+     * there is no active request.
+     */
+    val pendingTargetIntent: MutableStateFlow<Intent?> = MutableStateFlow(null)
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt
index b461d10..9aecc98 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt
@@ -16,52 +16,13 @@
 
 package com.android.intentresolver.contentpreview.payloadtoggle.data.repository
 
-import android.util.Log
-import com.android.intentresolver.contentpreview.payloadtoggle.data.model.SelectionRecord
-import com.android.intentresolver.contentpreview.payloadtoggle.data.model.SelectionRecordType.Initial
-import com.android.intentresolver.contentpreview.payloadtoggle.data.model.SelectionRecordType.Uninitialized
-import com.android.intentresolver.contentpreview.payloadtoggle.data.model.SelectionRecordType.Updated
 import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
 import dagger.hilt.android.scopes.ViewModelScoped
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.update
-
-private const val TAG = "PreviewSelectionsRep"
 
 /** Stores set of selected previews. */
 @ViewModelScoped
 class PreviewSelectionsRepository @Inject constructor() {
-    private val _selections = MutableStateFlow(SelectionRecord(Uninitialized, emptySet()))
-
-    /** Selected previews data */
-    val selections: StateFlow<SelectionRecord> = _selections.asStateFlow()
-
-    fun setSelection(selection: Set<PreviewModel>) {
-        _selections.value = SelectionRecord(Initial, selection)
-    }
-
-    fun select(item: PreviewModel) {
-        _selections.update { record ->
-            if (record.type == Uninitialized) {
-                Log.w(TAG, "Changing selection before it is initialized")
-                record
-            } else {
-                SelectionRecord(Updated, record.selection + item)
-            }
-        }
-    }
-
-    fun unselect(item: PreviewModel) {
-        _selections.update { record ->
-            if (record.type == Uninitialized) {
-                Log.w(TAG, "Changing selection before it is initialized")
-                record
-            } else {
-                SelectionRecord(Updated, record.selection - item)
-            }
-        }
-    }
+    val selections = MutableStateFlow(emptySet<PreviewModel>())
 }
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/TargetIntentRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/TargetIntentRepository.kt
deleted file mode 100644
index bb43323..0000000
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/TargetIntentRepository.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2024 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.contentpreview.payloadtoggle.data.repository
-
-import android.content.Intent
-import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel
-import com.android.intentresolver.contentpreview.payloadtoggle.data.model.TargetIntentRecord
-import com.android.intentresolver.inject.TargetIntent
-import dagger.hilt.android.scopes.ViewModelScoped
-import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableStateFlow
-
-/** Stores the target intent of the share sheet, and custom actions derived from the intent. */
-@ViewModelScoped
-class TargetIntentRepository
-@Inject
-constructor(
-    @TargetIntent initialIntent: Intent,
-    initialActions: List<CustomActionModel>,
-) {
-    val targetIntent = MutableStateFlow(TargetIntentRecord(isInitial = true, initialIntent))
-
-    // TODO: this can probably be derived from [targetIntent]; right now, the [initialActions] are
-    //  coming from a different place (ChooserRequest) than later ones (SelectionChangeCallback)
-    //  and so this serves as the source of truth between the two.
-    val customActions = MutableStateFlow(initialActions)
-
-    fun updateTargetIntent(intent: Intent) {
-        targetIntent.value = TargetIntentRecord(isInitial = false, intent)
-    }
-}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifier.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifier.kt
index 577dc34..4a2a693 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifier.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifier.kt
@@ -32,7 +32,7 @@
 
 /** Modifies target intent based on current payload selection. */
 fun interface TargetIntentModifier<Item> {
-    fun onSelectionChanged(selection: Collection<Item>): Intent
+    fun intentFromSelection(selection: Collection<Item>): Intent
 }
 
 class TargetIntentModifierImpl<Item>(
@@ -40,7 +40,7 @@
     private val getUri: Item.() -> Uri,
     private val getMimeType: Item.() -> String?,
 ) : TargetIntentModifier<Item> {
-    override fun onSelectionChanged(selection: Collection<Item>): Intent {
+    override fun intentFromSelection(selection: Collection<Item>): Intent {
         val uris = selection.mapTo(ArrayList()) { it.getUri() }
         val targetMimeType =
             selection.fold(null) { target: String?, item: Item ->
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ChooserRequestInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ChooserRequestInteractor.kt
new file mode 100644
index 0000000..61c04ac
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ChooserRequestInteractor.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 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.contentpreview.payloadtoggle.domain.interactor
+
+import android.content.Intent
+import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel
+import com.android.intentresolver.v2.data.repository.ChooserRequestRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.map
+
+/** Stores the target intent of the share sheet, and custom actions derived from the intent. */
+class ChooserRequestInteractor
+@Inject
+constructor(
+    private val repository: ChooserRequestRepository,
+) {
+    val targetIntent: Flow<Intent>
+        get() = repository.chooserRequest.map { it.targetIntent }
+
+    val customActions: Flow<List<CustomActionModel>>
+        get() = repository.customActions.asSharedFlow()
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractor.kt
index 56f781f..e973e84 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractor.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractor.kt
@@ -21,7 +21,6 @@
 import android.content.pm.PackageManager
 import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel
 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ActivityResultRepository
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.TargetIntentRepository
 import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ActionModel
 import com.android.intentresolver.icon.toComposeIcon
 import com.android.intentresolver.inject.Background
@@ -41,12 +40,12 @@
     private val contentResolver: ContentResolver,
     private val eventLog: EventLog,
     private val packageManager: PackageManager,
-    private val targetIntentRepo: TargetIntentRepository,
+    private val chooserRequestInteractor: ChooserRequestInteractor,
 ) {
     /** List of [ActionModel] that can be presented in Shareousel. */
     val customActions: Flow<List<ActionModel>>
         get() =
-            targetIntentRepo.customActions
+            chooserRequestInteractor.customActions
                 .map { actions ->
                     actions.map { action ->
                         ActionModel(
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt
index a7749c9..9bc7ae6 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt
@@ -41,10 +41,10 @@
     private val uriMetadataReader: UriMetadataReader,
     @PayloadToggle private val cursorResolver: CursorResolver<@JvmSuppressWildcards Uri?>,
 ) {
-    suspend fun launch() = coroutineScope {
+    suspend fun activate() = coroutineScope {
         val cursor = async { cursorResolver.getCursor() }
         val initialPreviewMap: Set<PreviewModel> = getInitialPreviews()
-        selectionRepository.setSelection(initialPreviewMap)
+        selectionRepository.selections.value = initialPreviewMap
         setCursorPreviews.setPreviews(
             previewsByKey = initialPreviewMap,
             startIndex = focusedItemIdx,
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ProcessTargetIntentUpdatesInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ProcessTargetIntentUpdatesInteractor.kt
new file mode 100644
index 0000000..04416a3
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ProcessTargetIntentUpdatesInteractor.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 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.contentpreview.payloadtoggle.domain.interactor
+
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.SelectionChangeCallback
+import javax.inject.Inject
+import kotlinx.coroutines.flow.collectLatest
+
+/** Communicates with the sharing application to notify of changes to the target intent. */
+class ProcessTargetIntentUpdatesInteractor
+@Inject
+constructor(
+    private val selectionCallback: SelectionChangeCallback,
+    private val repository: PendingSelectionCallbackRepository,
+    private val chooserRequestInteractor: UpdateChooserRequestInteractor,
+) {
+    /** Listen for events and update state. */
+    suspend fun activate() {
+        repository.pendingTargetIntent.collectLatest { targetIntent ->
+            targetIntent ?: return@collectLatest
+            selectionCallback.onSelectionChanged(targetIntent)?.let { update ->
+                chooserRequestInteractor.applyUpdate(update)
+            }
+            repository.pendingTargetIntent.compareAndSet(targetIntent, null)
+        }
+    }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt
index 3b5b0dd..55a995f 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt
@@ -17,7 +17,6 @@
 package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
 
 import android.net.Uri
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository
 import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
@@ -25,19 +24,19 @@
 /** An individual preview in Shareousel. */
 class SelectablePreviewInteractor(
     private val key: PreviewModel,
-    private val selectionRepo: PreviewSelectionsRepository,
+    private val selectionInteractor: SelectionInteractor,
 ) {
     val uri: Uri = key.uri
 
     /** Whether or not this preview is selected by the user. */
-    val isSelected: Flow<Boolean> = selectionRepo.selections.map { key in it.selection }
+    val isSelected: Flow<Boolean> = selectionInteractor.selections.map { key in it }
 
     /** Sets whether this preview is selected by the user. */
     fun setSelected(isSelected: Boolean) {
         if (isSelected) {
-            selectionRepo.select(key)
+            selectionInteractor.select(key)
         } else {
-            selectionRepo.unselect(key)
+            selectionInteractor.unselect(key)
         }
     }
 }
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractor.kt
index 78e208f..a578d0e 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractor.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractor.kt
@@ -17,7 +17,6 @@
 package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
 
 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.CursorPreviewsRepository
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository
 import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
 import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
 import javax.inject.Inject
@@ -27,7 +26,7 @@
 @Inject
 constructor(
     private val previewsRepo: CursorPreviewsRepository,
-    private val selectionRepo: PreviewSelectionsRepository,
+    private val selectionInteractor: SelectionInteractor,
 ) {
     /** Keys of previews available for display in Shareousel. */
     val previews: Flow<PreviewsModel?>
@@ -37,6 +36,5 @@
      * Returns a [SelectablePreviewInteractor] that can be used to interact with the individual
      * preview associated with [key].
      */
-    fun preview(key: PreviewModel) =
-        SelectablePreviewInteractor(key = key, selectionRepo = selectionRepo)
+    fun preview(key: PreviewModel) = SelectablePreviewInteractor(key, selectionInteractor)
 }
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt
index 0b8bcdd..a570f36 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt
@@ -17,15 +17,38 @@
 package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
 
 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.TargetIntentModifier
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.updateAndGet
 
 class SelectionInteractor
 @Inject
 constructor(
-    selectionRepo: PreviewSelectionsRepository,
+    private val selectionsRepo: PreviewSelectionsRepository,
+    private val targetIntentModifier: TargetIntentModifier<PreviewModel>,
+    private val updateTargetIntentInteractor: UpdateTargetIntentInteractor,
 ) {
+    /** Set of selected previews. */
+    val selections: StateFlow<Set<PreviewModel>>
+        get() = selectionsRepo.selections
+
     /** Amount of selected previews. */
-    val amountSelected: Flow<Int> = selectionRepo.selections.map { it.selection.size }
+    val amountSelected: Flow<Int> = selectionsRepo.selections.map { it.size }
+
+    fun select(model: PreviewModel) {
+        updateChooserRequest(selectionsRepo.selections.updateAndGet { it + model })
+    }
+
+    fun unselect(model: PreviewModel) {
+        updateChooserRequest(selectionsRepo.selections.updateAndGet { it - model })
+    }
+
+    private fun updateChooserRequest(selections: Set<PreviewModel>) {
+        val intent = targetIntentModifier.intentFromSelection(selections)
+        updateTargetIntentInteractor.updateTargetIntent(intent)
+    }
 }
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractor.kt
new file mode 100644
index 0000000..9e48cd2
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractor.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 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.contentpreview.payloadtoggle.domain.interactor
+
+import android.content.Intent
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.CustomAction
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.PendingIntentSender
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.toCustomActionModel
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.getOrDefault
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.onValue
+import com.android.intentresolver.v2.data.repository.ChooserRequestRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.update
+
+/** Updates the tracked chooser request. */
+class UpdateChooserRequestInteractor
+@Inject
+constructor(
+    private val repository: ChooserRequestRepository,
+    @CustomAction private val pendingIntentSender: PendingIntentSender,
+) {
+    fun applyUpdate(update: ShareouselUpdate) {
+        repository.chooserRequest.update { current ->
+            current.copy(
+                callerChooserTargets =
+                    update.callerTargets.getOrDefault(current.callerChooserTargets),
+                modifyShareAction =
+                    update.modifyShareAction.getOrDefault(current.modifyShareAction),
+                additionalTargets = update.alternateIntents.getOrDefault(current.additionalTargets),
+                chosenComponentSender =
+                    update.resultIntentSender.getOrDefault(current.chosenComponentSender),
+                refinementIntentSender =
+                    update.refinementIntentSender.getOrDefault(current.refinementIntentSender),
+                metadataText = update.metadataText.getOrDefault(current.metadataText),
+                chooserActions = update.customActions.getOrDefault(current.chooserActions),
+            )
+        }
+        update.customActions.onValue { actions ->
+            repository.customActions.value =
+                actions.map { it.toCustomActionModel(pendingIntentSender) }
+        }
+    }
+
+    fun setTargetIntent(targetIntent: Intent) {
+        repository.chooserRequest.update { it.copy(targetIntent = targetIntent) }
+    }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt
index 06e28cb..429e34e 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt
@@ -14,64 +14,24 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
 package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
 
-import com.android.intentresolver.contentpreview.payloadtoggle.data.model.SelectionRecordType
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ChooserParamsUpdateRepository
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.TargetIntentRepository
-import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.CustomAction
-import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.PendingIntentSender
-import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.TargetIntentModifier
-import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.toCustomActionModel
-import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.onValue
-import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.SelectionChangeCallback
-import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import android.content.Intent
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository
 import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.launch
 
-/** Updates [TargetIntentRepository] in reaction to user selection changes. */
 class UpdateTargetIntentInteractor
 @Inject
 constructor(
-    private val intentRepository: TargetIntentRepository,
-    private val chooserParamsUpdateRepository: ChooserParamsUpdateRepository,
-    @CustomAction private val pendingIntentSender: PendingIntentSender,
-    private val selectionCallback: SelectionChangeCallback,
-    private val selectionRepo: PreviewSelectionsRepository,
-    private val targetIntentModifier: TargetIntentModifier<PreviewModel>,
+    private val repository: PendingSelectionCallbackRepository,
+    private val chooserRequestInteractor: UpdateChooserRequestInteractor,
 ) {
-    /** Listen for events and update state. */
-    suspend fun launch(): Unit = coroutineScope {
-        launch {
-            intentRepository.targetIntent
-                .filter { !it.isInitial }
-                .mapLatest { record -> selectionCallback.onSelectionChanged(record.intent) }
-                .filterNotNull()
-                .collect { updates ->
-                    updates.customActions.onValue { actions ->
-                        intentRepository.customActions.value =
-                            actions.map { it.toCustomActionModel(pendingIntentSender) }
-                    }
-                    chooserParamsUpdateRepository.setUpdates(updates)
-                }
-        }
-        launch {
-            selectionRepo.selections
-                .filter { it.type == SelectionRecordType.Updated }
-                .collectLatest {
-                    intentRepository.updateTargetIntent(
-                        targetIntentModifier.onSelectionChanged(it.selection)
-                    )
-                }
-        }
+    /**
+     * Updates the target intent for the chooser. This will kick off an asynchronous IPC with the
+     * sharing application, so that it can react to the new intent.
+     */
+    fun updateTargetIntent(targetIntent: Intent) {
+        chooserRequestInteractor.setTargetIntent(targetIntent)
+        repository.pendingTargetIntent.value = targetIntent
     }
 }
diff --git a/java/src/com/android/intentresolver/inject/ActivityModelModule.kt b/java/src/com/android/intentresolver/inject/ActivityModelModule.kt
index c08c7f4..ff2bb14 100644
--- a/java/src/com/android/intentresolver/inject/ActivityModelModule.kt
+++ b/java/src/com/android/intentresolver/inject/ActivityModelModule.kt
@@ -21,8 +21,8 @@
 import android.service.chooser.ChooserAction
 import androidx.lifecycle.SavedStateHandle
 import com.android.intentresolver.util.ownedByCurrentUser
+import com.android.intentresolver.v2.data.model.ChooserRequest
 import com.android.intentresolver.v2.ui.model.ActivityModel
-import com.android.intentresolver.v2.ui.model.ChooserRequest
 import com.android.intentresolver.v2.ui.viewmodel.readChooserRequest
 import com.android.intentresolver.v2.validation.Valid
 import com.android.intentresolver.v2.validation.ValidationResult
@@ -48,12 +48,20 @@
 
     @Provides
     @ViewModelScoped
-    fun provideChooserRequest(
+    fun provideInitialRequest(
         activityModel: ActivityModel,
         flags: ChooserServiceFlags,
     ): ValidationResult<ChooserRequest> = readChooserRequest(activityModel, flags)
 
     @Provides
+    fun provideChooserRequest(
+        initialRequest: ValidationResult<ChooserRequest>,
+    ): ChooserRequest =
+        requireNotNull((initialRequest as? Valid)?.value) {
+            "initialRequest is Invalid, no chooser request available"
+        }
+
+    @Provides
     @TargetIntent
     fun targetIntent(chooserReq: ValidationResult<ChooserRequest>): Intent =
         requireNotNull((chooserReq as? Valid)?.value?.targetIntent) { "no target intent available" }
diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java
index ffa0469..d624c9e 100644
--- a/java/src/com/android/intentresolver/v2/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java
@@ -131,6 +131,7 @@
 import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
 import com.android.intentresolver.shortcuts.AppPredictorFactory;
 import com.android.intentresolver.shortcuts.ShortcutLoader;
+import com.android.intentresolver.v2.data.model.ChooserRequest;
 import com.android.intentresolver.v2.data.repository.DevicePolicyResources;
 import com.android.intentresolver.v2.domain.interactor.UserInteractor;
 import com.android.intentresolver.v2.emptystate.NoAppsAvailableEmptyStateProvider;
@@ -151,7 +152,6 @@
 import com.android.intentresolver.v2.ui.ShareResultSender;
 import com.android.intentresolver.v2.ui.ShareResultSenderFactory;
 import com.android.intentresolver.v2.ui.model.ActivityModel;
-import com.android.intentresolver.v2.ui.model.ChooserRequest;
 import com.android.intentresolver.v2.ui.viewmodel.ChooserViewModel;
 import com.android.intentresolver.widget.ActionRow;
 import com.android.intentresolver.widget.ImagePreviewView;
diff --git a/java/src/com/android/intentresolver/v2/ChooserHelper.kt b/java/src/com/android/intentresolver/v2/ChooserHelper.kt
index f2a2726..503e46d 100644
--- a/java/src/com/android/intentresolver/v2/ChooserHelper.kt
+++ b/java/src/com/android/intentresolver/v2/ChooserHelper.kt
@@ -29,9 +29,9 @@
 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ActivityResultRepository
 import com.android.intentresolver.inject.Background
 import com.android.intentresolver.v2.annotation.JavaInterop
+import com.android.intentresolver.v2.data.model.ChooserRequest
 import com.android.intentresolver.v2.domain.interactor.UserInteractor
 import com.android.intentresolver.v2.shared.model.Profile
-import com.android.intentresolver.v2.ui.model.ChooserRequest
 import com.android.intentresolver.v2.ui.viewmodel.ChooserViewModel
 import com.android.intentresolver.v2.validation.Invalid
 import com.android.intentresolver.v2.validation.Valid
diff --git a/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt b/java/src/com/android/intentresolver/v2/data/model/ChooserRequest.kt
similarity index 99%
rename from java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt
rename to java/src/com/android/intentresolver/v2/data/model/ChooserRequest.kt
index 4f3cf3c..7c9c861 100644
--- a/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt
+++ b/java/src/com/android/intentresolver/v2/data/model/ChooserRequest.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.intentresolver.v2.ui.model
+package com.android.intentresolver.v2.data.model
 
 import android.content.ComponentName
 import android.content.Intent
diff --git a/java/src/com/android/intentresolver/v2/data/repository/ChooserRequestRepository.kt b/java/src/com/android/intentresolver/v2/data/repository/ChooserRequestRepository.kt
new file mode 100644
index 0000000..d23e07e
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/data/repository/ChooserRequestRepository.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 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.v2.data.repository
+
+import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel
+import com.android.intentresolver.v2.data.model.ChooserRequest
+import dagger.hilt.android.scopes.ViewModelScoped
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+@ViewModelScoped
+class ChooserRequestRepository
+@Inject
+constructor(
+    initialRequest: ChooserRequest,
+    initialActions: List<CustomActionModel>,
+) {
+    /** All information from the sharing application pertaining to the chooser. */
+    val chooserRequest: MutableStateFlow<ChooserRequest> = MutableStateFlow(initialRequest)
+
+    /** Custom actions from the sharing app to be presented in the chooser. */
+    // NOTE: this could be derived directly from chooserRequest, but that would require working
+    //  directly with PendingIntents, which complicates testing.
+    val customActions: MutableStateFlow<List<CustomActionModel>> = MutableStateFlow(initialActions)
+}
diff --git a/java/src/com/android/intentresolver/v2/domain/interactor/ChooserRequestUpdateInteractor.kt b/java/src/com/android/intentresolver/v2/domain/interactor/ChooserRequestUpdateInteractor.kt
deleted file mode 100644
index 3721340..0000000
--- a/java/src/com/android/intentresolver/v2/domain/interactor/ChooserRequestUpdateInteractor.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright 2024 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.v2.domain.interactor
-
-import android.content.Intent
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ChooserParamsUpdateRepository
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.TargetIntentRepository
-import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate
-import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.getOrDefault
-import com.android.intentresolver.v2.ui.model.ChooserRequest
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-import dagger.hilt.android.scopes.ViewModelScoped
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-
-/** Updates updates ChooserRequest with a new target intent */
-// TODO: make fully injectable
-class ChooserRequestUpdateInteractor
-@AssistedInject
-constructor(
-    private val targetIntentRepository: TargetIntentRepository,
-    private val paramsUpdateRepository: ChooserParamsUpdateRepository,
-    // TODO: replace with a proper repository, when available
-    @Assisted private val chooserRequestRepository: MutableStateFlow<ChooserRequest>,
-) {
-
-    suspend fun launch() {
-        coroutineScope {
-            launch {
-                targetIntentRepository.targetIntent
-                    .filter { !it.isInitial }
-                    .map { it.intent }
-                    .collect(::updateTargetIntent)
-            }
-
-            launch {
-                paramsUpdateRepository.updates.filterNotNull().collect(::updateChooserParameters)
-            }
-        }
-    }
-
-    private fun updateTargetIntent(targetIntent: Intent) {
-        chooserRequestRepository.update { current -> current.copy(targetIntent = targetIntent) }
-    }
-
-    private fun updateChooserParameters(update: ShareouselUpdate) {
-        chooserRequestRepository.update { current ->
-            current.copy(
-                callerChooserTargets =
-                    update.callerTargets.getOrDefault(current.callerChooserTargets),
-                modifyShareAction =
-                    update.modifyShareAction.getOrDefault(current.modifyShareAction),
-                additionalTargets = update.alternateIntents.getOrDefault(current.additionalTargets),
-                chosenComponentSender =
-                    update.resultIntentSender.getOrDefault(current.chosenComponentSender),
-                refinementIntentSender =
-                    update.refinementIntentSender.getOrDefault(current.refinementIntentSender),
-                metadataText = update.metadataText.getOrDefault(current.metadataText),
-            )
-        }
-    }
-}
-
-@AssistedFactory
-@ViewModelScoped
-interface ChooserRequestUpdateInteractorFactory {
-    fun create(
-        chooserRequestRepository: MutableStateFlow<ChooserRequest>
-    ): ChooserRequestUpdateInteractor
-}
diff --git a/java/src/com/android/intentresolver/v2/ui/model/ActivityModel.kt b/java/src/com/android/intentresolver/v2/ui/model/ActivityModel.kt
index 07b1743..67c2a25 100644
--- a/java/src/com/android/intentresolver/v2/ui/model/ActivityModel.kt
+++ b/java/src/com/android/intentresolver/v2/ui/model/ActivityModel.kt
@@ -20,6 +20,7 @@
 import android.net.Uri
 import android.os.Parcel
 import android.os.Parcelable
+import com.android.intentresolver.v2.data.model.ANDROID_APP_SCHEME
 import com.android.intentresolver.v2.ext.readParcelable
 import com.android.intentresolver.v2.ext.requireParcelable
 import java.util.Objects
diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt
index 7ebf65a..a25fcbe 100644
--- a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt
+++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt
@@ -44,10 +44,10 @@
 import com.android.intentresolver.R
 import com.android.intentresolver.inject.ChooserServiceFlags
 import com.android.intentresolver.util.hasValidIcon
+import com.android.intentresolver.v2.data.model.ChooserRequest
 import com.android.intentresolver.v2.ext.hasSendAction
 import com.android.intentresolver.v2.ext.ifMatch
 import com.android.intentresolver.v2.ui.model.ActivityModel
-import com.android.intentresolver.v2.ui.model.ChooserRequest
 import com.android.intentresolver.v2.validation.Validation
 import com.android.intentresolver.v2.validation.ValidationResult
 import com.android.intentresolver.v2.validation.types.IntentOrUri
diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt
index 4431a54..e39329b 100644
--- a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt
+++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt
@@ -20,21 +20,21 @@
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.FetchPreviewsInteractor
-import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.UpdateTargetIntentInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.ProcessTargetIntentUpdatesInteractor
 import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselViewModel
 import com.android.intentresolver.inject.Background
 import com.android.intentresolver.inject.ChooserServiceFlags
-import com.android.intentresolver.v2.domain.interactor.ChooserRequestUpdateInteractorFactory
+import com.android.intentresolver.v2.data.model.ChooserRequest
+import com.android.intentresolver.v2.data.repository.ChooserRequestRepository
 import com.android.intentresolver.v2.ui.model.ActivityModel
 import com.android.intentresolver.v2.ui.model.ActivityModel.Companion.ACTIVITY_MODEL_KEY
-import com.android.intentresolver.v2.ui.model.ChooserRequest
 import com.android.intentresolver.v2.validation.Invalid
 import com.android.intentresolver.v2.validation.Valid
+import com.android.intentresolver.v2.validation.ValidationResult
 import dagger.Lazy
 import dagger.hilt.android.lifecycle.HiltViewModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.launch
@@ -47,11 +47,17 @@
 constructor(
     args: SavedStateHandle,
     private val shareouselViewModelProvider: Lazy<ShareouselViewModel>,
-    private val updateTargetIntentInteractor: Lazy<UpdateTargetIntentInteractor>,
+    private val processUpdatesInteractor: Lazy<ProcessTargetIntentUpdatesInteractor>,
     private val fetchPreviewsInteractor: Lazy<FetchPreviewsInteractor>,
     @Background private val bgDispatcher: CoroutineDispatcher,
-    private val chooserRequestUpdateInteractorFactory: ChooserRequestUpdateInteractorFactory,
     private val flags: ChooserServiceFlags,
+    /**
+     * Provided only for the express purpose of early exit in the event of an invalid request.
+     *
+     * Note: [request] can only be safely accessed after checking if this value is [Valid].
+     */
+    val initialRequest: ValidationResult<ChooserRequest>,
+    private val chooserRequestRepository: Lazy<ChooserRequestRepository>,
 ) : ViewModel() {
 
     /** Parcelable-only references provided from the creating Activity */
@@ -60,43 +66,29 @@
             "ActivityModel missing in SavedStateHandle! ($ACTIVITY_MODEL_KEY)"
         }
 
-    val shareouselViewModel by lazy {
+    val shareouselViewModel: ShareouselViewModel by lazy {
         // TODO: consolidate this logic, this would require a consolidated preview view model but
         //  for now just postpone starting the payload selection preview machinery until it's needed
         assert(flags.chooserPayloadToggling()) {
             "An attempt to use payload selection preview with the disabled flag"
         }
 
-        viewModelScope.launch(bgDispatcher) { updateTargetIntentInteractor.get().launch() }
-        viewModelScope.launch(bgDispatcher) { fetchPreviewsInteractor.get().launch() }
-        viewModelScope.launch { chooserRequestUpdateInteractorFactory.create(_request).launch() }
+        viewModelScope.launch(bgDispatcher) { processUpdatesInteractor.get().activate() }
+        viewModelScope.launch(bgDispatcher) { fetchPreviewsInteractor.get().activate() }
         shareouselViewModelProvider.get()
     }
 
     /**
-     * Provided only for the express purpose of early exit in the event of an invalid request.
-     *
-     * Note: [request] can only be safely accessed after checking if this value is [Valid].
-     */
-    internal val initialRequest = readChooserRequest(activityModel, flags)
-
-    private lateinit var _request: MutableStateFlow<ChooserRequest>
-
-    /**
      * A [StateFlow] of [ChooserRequest].
      *
      * Note: Only safe to access after checking if [initialRequest] is [Valid].
      */
-    lateinit var request: StateFlow<ChooserRequest>
-        private set
+    val request: StateFlow<ChooserRequest>
+        get() = chooserRequestRepository.get().chooserRequest.asStateFlow()
 
     init {
-        when (initialRequest) {
-            is Valid -> {
-                _request = MutableStateFlow(initialRequest.value)
-                request = _request.asStateFlow()
-            }
-            is Invalid -> Log.w(TAG, "initialRequest is Invalid, initialization failed")
+        if (initialRequest is Invalid) {
+            Log.w(TAG, "initialRequest is Invalid, initialization failed")
         }
     }
 }
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepositoryTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepositoryTest.kt
deleted file mode 100644
index 48c8b58..0000000
--- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepositoryTest.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 2024 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.contentpreview.payloadtoggle.data.repository
-
-import android.net.Uri
-import com.android.intentresolver.contentpreview.payloadtoggle.data.model.SelectionRecordType.Initial
-import com.android.intentresolver.contentpreview.payloadtoggle.data.model.SelectionRecordType.Uninitialized
-import com.android.intentresolver.contentpreview.payloadtoggle.data.model.SelectionRecordType.Updated
-import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-
-class PreviewSelectionsRepositoryTest {
-
-    @Test
-    fun testSelectionStatus() {
-        val testSubject = PreviewSelectionsRepository()
-
-        assertThat(testSubject.selections.value.type).isEqualTo(Uninitialized)
-
-        testSubject.setSelection(setOf(PreviewModel(Uri.parse("content://pkg/1.png"), "image/png")))
-
-        assertThat(testSubject.selections.value.type).isEqualTo(Initial)
-
-        testSubject.select(PreviewModel(Uri.parse("content://pkg/2.png"), "image/png"))
-
-        assertThat(testSubject.selections.value.type).isEqualTo(Updated)
-    }
-}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifierImplTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifierImplTest.kt
index f4be47e..7c36ef5 100644
--- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifierImplTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifierImplTest.kt
@@ -32,14 +32,14 @@
 
         val u1 = createUri(1)
         val u2 = createUri(2)
-        testSubject.onSelectionChanged(listOf(u1, u2)).let { intent ->
+        testSubject.intentFromSelection(listOf(u1, u2)).let { intent ->
             assertThat(intent.action).isEqualTo(ACTION_SEND_MULTIPLE)
             assertThat(intent.getParcelableArrayListExtra(EXTRA_STREAM, Uri::class.java))
                 .containsExactly(u1, u2)
                 .inOrder()
         }
 
-        testSubject.onSelectionChanged(listOf(u1)).let { intent ->
+        testSubject.intentFromSelection(listOf(u1)).let { intent ->
             assertThat(intent.action).isEqualTo(ACTION_SEND)
             assertThat(intent.getParcelableExtra(EXTRA_STREAM, Uri::class.java)).isEqualTo(u1)
         }
@@ -52,20 +52,22 @@
 
         val u1 = createUri(1)
         val u2 = createUri(2)
-        testSubject.onSelectionChanged(listOf(u1 to "image/png", u2 to "image/png")).let { intent ->
+        testSubject.intentFromSelection(listOf(u1 to "image/png", u2 to "image/png")).let { intent
+            ->
             assertThat(intent.type).isEqualTo("image/png")
         }
 
-        testSubject.onSelectionChanged(listOf(u1 to "image/png", u2 to "image/jpg")).let { intent ->
+        testSubject.intentFromSelection(listOf(u1 to "image/png", u2 to "image/jpg")).let { intent
+            ->
             assertThat(intent.type).isEqualTo("image/*")
         }
 
-        testSubject.onSelectionChanged(listOf(u1 to "image/png", u2 to "video/mpeg")).let { intent
+        testSubject.intentFromSelection(listOf(u1 to "image/png", u2 to "video/mpeg")).let { intent
             ->
             assertThat(intent.type).isEqualTo("*/*")
         }
 
-        testSubject.onSelectionChanged(listOf(u1 to "image/png", u2 to null)).let { intent ->
+        testSubject.intentFromSelection(listOf(u1 to "image/png", u2 to null)).let { intent ->
             assertThat(intent.type).isEqualTo("*/*")
         }
     }
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractorTest.kt
index 95ad966..ceb20da 100644
--- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractorTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractorTest.kt
@@ -19,16 +19,16 @@
 package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
 
 import android.app.Activity
-import android.content.Intent
 import android.graphics.Bitmap
 import android.graphics.drawable.Icon
 import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel
 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ActivityResultRepository
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.TargetIntentRepository
 import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ActionModel
 import com.android.intentresolver.icon.BitmapIcon
 import com.android.intentresolver.mock
 import com.android.intentresolver.util.comparingElementsUsingTransform
+import com.android.intentresolver.v2.data.model.fakeChooserRequest
+import com.android.intentresolver.v2.data.repository.ChooserRequestRepository
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.StateFlow
@@ -47,7 +47,14 @@
         runTest(testDispatcher) {
             val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)
             val icon = Icon.createWithBitmap(bitmap)
-            val chooserActions = listOf(CustomActionModel("label1", icon) {})
+            val chooserRequestRepository =
+                ChooserRequestRepository(
+                    initialRequest = fakeChooserRequest(),
+                    initialActions =
+                        listOf(
+                            CustomActionModel(label = "label1", icon = icon, performAction = {}),
+                        ),
+                )
             val underTest =
                 CustomActionsInteractor(
                     activityResultRepo = ActivityResultRepository(),
@@ -55,11 +62,8 @@
                     contentResolver = mock {},
                     eventLog = mock {},
                     packageManager = mock {},
-                    targetIntentRepo =
-                        TargetIntentRepository(
-                            initialIntent = Intent(),
-                            initialActions = chooserActions,
-                        ),
+                    chooserRequestInteractor =
+                        ChooserRequestInteractor(repository = chooserRequestRepository),
                 )
             val customActions: StateFlow<List<ActionModel>> =
                 underTest.customActions.stateIn(backgroundScope)
@@ -80,9 +84,9 @@
     @Test
     fun customActions_tracksRepoUpdates() =
         runTest(testDispatcher) {
-            val targetIntentRepository =
-                TargetIntentRepository(
-                    initialIntent = Intent(),
+            val chooserRequestRepository =
+                ChooserRequestRepository(
+                    initialRequest = fakeChooserRequest(),
                     initialActions = emptyList(),
                 )
             val underTest =
@@ -92,7 +96,8 @@
                     contentResolver = mock {},
                     eventLog = mock {},
                     packageManager = mock {},
-                    targetIntentRepo = targetIntentRepository,
+                    chooserRequestInteractor =
+                        ChooserRequestInteractor(repository = chooserRequestRepository),
                 )
 
             val customActions: StateFlow<List<ActionModel>> =
@@ -100,7 +105,7 @@
             val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)
             val icon = Icon.createWithBitmap(bitmap)
             val chooserActions = listOf(CustomActionModel("label1", icon) {})
-            targetIntentRepository.customActions.value = chooserActions
+            chooserRequestRepository.customActions.value = chooserActions
             runCurrent()
 
             assertThat(customActions.value)
@@ -123,8 +128,19 @@
             val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)
             val icon = Icon.createWithBitmap(bitmap)
             var actionSent = false
-            val chooserActions = listOf(CustomActionModel("label1", icon) { actionSent = true })
             val activityResultRepository = ActivityResultRepository()
+            val chooserRequestRepository =
+                ChooserRequestRepository(
+                    initialRequest = fakeChooserRequest(),
+                    initialActions =
+                        listOf(
+                            CustomActionModel(
+                                label = "label1",
+                                icon = icon,
+                                performAction = { actionSent = true },
+                            )
+                        ),
+                )
             val underTest =
                 CustomActionsInteractor(
                     activityResultRepo = activityResultRepository,
@@ -132,10 +148,9 @@
                     contentResolver = mock {},
                     eventLog = mock {},
                     packageManager = mock {},
-                    targetIntentRepo =
-                        TargetIntentRepository(
-                            initialIntent = Intent(),
-                            initialActions = chooserActions,
+                    chooserRequestInteractor =
+                        ChooserRequestInteractor(
+                            repository = chooserRequestRepository,
                         ),
                 )
             val customActions: StateFlow<List<ActionModel>> =
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt
index 9317f79..08a667b 100644
--- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt
@@ -121,7 +121,7 @@
 
     @Test
     fun setsInitialPreviews() = runTestWithDeps { deps ->
-        backgroundScope.launch { deps.underTest.launch() }
+        backgroundScope.launch { deps.underTest.activate() }
         runCurrent()
 
         assertThat(deps.previewsRepo.previewsModel.value)
@@ -147,7 +147,7 @@
 
     @Test
     fun lookupCursorFromContentResolver() = runTestWithDeps { deps ->
-        backgroundScope.launch { deps.underTest.launch() }
+        backgroundScope.launch { deps.underTest.activate() }
         deps.cursorResolver.complete()
         runCurrent()
 
@@ -173,7 +173,7 @@
             pageSize = 16,
             maxLoadedPages = 1,
         ) { deps ->
-            backgroundScope.launch { deps.underTest.launch() }
+            backgroundScope.launch { deps.underTest.activate() }
             deps.cursorResolver.complete()
             runCurrent()
 
@@ -205,7 +205,7 @@
             pageSize = 16,
             maxLoadedPages = 2,
         ) { deps ->
-            backgroundScope.launch { deps.underTest.launch() }
+            backgroundScope.launch { deps.underTest.activate() }
             deps.cursorResolver.complete()
             runCurrent()
 
@@ -237,7 +237,7 @@
             pageSize = 16,
             maxLoadedPages = 1,
         ) { deps ->
-            backgroundScope.launch { deps.underTest.launch() }
+            backgroundScope.launch { deps.underTest.activate() }
             deps.cursorResolver.complete()
             runCurrent()
 
@@ -268,7 +268,7 @@
             pageSize = 16,
             maxLoadedPages = 2,
         ) { deps ->
-            backgroundScope.launch { deps.underTest.launch() }
+            backgroundScope.launch { deps.underTest.activate() }
             deps.cursorResolver.complete()
             runCurrent()
 
@@ -299,7 +299,7 @@
             pageSize = 16,
             maxLoadedPages = 2,
         ) { deps ->
-            backgroundScope.launch { deps.underTest.launch() }
+            backgroundScope.launch { deps.underTest.activate() }
             deps.cursorResolver.complete()
             runCurrent()
 
@@ -320,7 +320,7 @@
             pageSize = 16,
             maxLoadedPages = 2,
         ) { deps ->
-            backgroundScope.launch { deps.underTest.launch() }
+            backgroundScope.launch { deps.underTest.activate() }
             deps.cursorResolver.complete()
             runCurrent()
 
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractorTest.kt
index 3dba532..ff22f37 100644
--- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractorTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractorTest.kt
@@ -18,11 +18,13 @@
 
 package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
 
+import android.content.Intent
 import android.net.Uri
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.CursorPreviewsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository
 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository
 import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
-import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
+import com.android.intentresolver.v2.data.model.fakeChooserRequest
+import com.android.intentresolver.v2.data.repository.ChooserRequestRepository
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.first
@@ -34,74 +36,113 @@
 
     @Test
     fun reflectPreviewRepo_initState() = runTest {
-        val repo =
-            CursorPreviewsRepository().apply {
-                previewsModel.value =
-                    PreviewsModel(
-                        previewModels =
-                            setOf(
-                                PreviewModel(
-                                    Uri.fromParts("scheme", "ssp", "fragment"),
-                                    "image/bitmap",
-                                ),
-                                PreviewModel(
-                                    Uri.fromParts("scheme2", "ssp2", "fragment2"),
-                                    "image/bitmap",
-                                ),
-                            ),
-                        startIdx = 0,
-                        loadMoreLeft = null,
-                        loadMoreRight = null,
-                    )
-            }
         val selectionRepo = PreviewSelectionsRepository()
+        val chooserRequestRepo =
+            ChooserRequestRepository(
+                initialRequest = fakeChooserRequest(),
+                initialActions = emptyList(),
+            )
+        val pendingSelectionCallbackRepo = PendingSelectionCallbackRepository()
         val underTest =
             SelectablePreviewInteractor(
                 key = PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), null),
-                selectionRepo = selectionRepo,
+                selectionInteractor =
+                    SelectionInteractor(
+                        selectionsRepo = selectionRepo,
+                        targetIntentModifier = { error("unexpected invocation") },
+                        updateTargetIntentInteractor =
+                            UpdateTargetIntentInteractor(
+                                repository = pendingSelectionCallbackRepo,
+                                chooserRequestInteractor =
+                                    UpdateChooserRequestInteractor(
+                                        repository = chooserRequestRepo,
+                                        pendingIntentSender = { error("unexpected invocation") },
+                                    )
+                            )
+                    ),
             )
-        selectionRepo.setSelection(emptySet())
-        testScheduler.runCurrent()
+        runCurrent()
 
         assertThat(underTest.isSelected.first()).isFalse()
     }
 
     @Test
     fun reflectPreviewRepo_updatedState() = runTest {
-        val repo = CursorPreviewsRepository()
-        val selectionRepository = PreviewSelectionsRepository()
+        val selectionRepo = PreviewSelectionsRepository()
+        val chooserRequestRepo =
+            ChooserRequestRepository(
+                initialRequest = fakeChooserRequest(),
+                initialActions = emptyList(),
+            )
+        val pendingSelectionCallbackRepo = PendingSelectionCallbackRepository()
         val underTest =
             SelectablePreviewInteractor(
                 key = PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), "image/bitmap"),
-                selectionRepo = selectionRepository,
+                selectionInteractor =
+                    SelectionInteractor(
+                        selectionsRepo = selectionRepo,
+                        targetIntentModifier = { error("unexpected invocation") },
+                        updateTargetIntentInteractor =
+                            UpdateTargetIntentInteractor(
+                                repository = pendingSelectionCallbackRepo,
+                                chooserRequestInteractor =
+                                    UpdateChooserRequestInteractor(
+                                        repository = chooserRequestRepo,
+                                        pendingIntentSender = { error("unexpected invocation") },
+                                    )
+                            )
+                    ),
             )
-        selectionRepository.setSelection(emptySet())
 
         assertThat(underTest.isSelected.first()).isFalse()
 
-        repo.previewsModel.value =
-            PreviewsModel(
-                previewModels =
-                    setOf(
-                        PreviewModel(
-                            Uri.fromParts("scheme", "ssp", "fragment"),
-                            "image/bitmap",
-                        ),
-                        PreviewModel(
-                            Uri.fromParts("scheme2", "ssp2", "fragment2"),
-                            "image/bitmap",
-                        ),
-                    ),
-                startIdx = 0,
-                loadMoreLeft = null,
-                loadMoreRight = null,
-            )
-
-        selectionRepository.setSelection(
+        selectionRepo.selections.value =
             setOf(PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), "image/bitmap"))
-        )
         runCurrent()
 
         assertThat(underTest.isSelected.first()).isTrue()
     }
+
+    @Test
+    fun setSelected_updatesChooserRequestRepo() = runTest {
+        val modifiedIntent = Intent()
+        val selectionRepo = PreviewSelectionsRepository()
+        val chooserRequestRepo =
+            ChooserRequestRepository(
+                initialRequest = fakeChooserRequest(),
+                initialActions = emptyList(),
+            )
+        val pendingSelectionCallbackRepo = PendingSelectionCallbackRepository()
+        val underTest =
+            SelectablePreviewInteractor(
+                key = PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), "image/bitmap"),
+                selectionInteractor =
+                    SelectionInteractor(
+                        selectionsRepo = selectionRepo,
+                        targetIntentModifier = { modifiedIntent },
+                        updateTargetIntentInteractor =
+                            UpdateTargetIntentInteractor(
+                                repository = pendingSelectionCallbackRepo,
+                                chooserRequestInteractor =
+                                    UpdateChooserRequestInteractor(
+                                        repository = chooserRequestRepo,
+                                        pendingIntentSender = { error("unexpected invocation") },
+                                    )
+                            )
+                    ),
+            )
+
+        underTest.setSelected(true)
+        runCurrent()
+
+        assertThat(selectionRepo.selections.value)
+            .containsExactly(
+                PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), "image/bitmap")
+            )
+
+        assertThat(chooserRequestRepo.chooserRequest.value.targetIntent)
+            .isSameInstanceAs(modifiedIntent)
+        assertThat(pendingSelectionCallbackRepo.pendingTargetIntent.value)
+            .isSameInstanceAs(modifiedIntent)
+    }
 }
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractorTest.kt
index a5d09f5..3f02c0c 100644
--- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractorTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractorTest.kt
@@ -20,9 +20,12 @@
 
 import android.net.Uri
 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.CursorPreviewsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository
 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository
 import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
 import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
+import com.android.intentresolver.v2.data.model.fakeChooserRequest
+import com.android.intentresolver.v2.data.repository.ChooserRequestRepository
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.first
@@ -57,16 +60,33 @@
             }
         val selectionRepo =
             PreviewSelectionsRepository().apply {
-                setSelection(
+                selections.value =
                     setOf(
                         PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), null),
                     )
-                )
             }
+        val chooserRequestRepo =
+            ChooserRequestRepository(
+                initialRequest = fakeChooserRequest(),
+                initialActions = emptyList(),
+            )
         val underTest =
             SelectablePreviewsInteractor(
                 previewsRepo = repo,
-                selectionRepo = selectionRepo,
+                selectionInteractor =
+                    SelectionInteractor(
+                        selectionRepo,
+                        targetIntentModifier = { error("unexpected invocation") },
+                        updateTargetIntentInteractor =
+                            UpdateTargetIntentInteractor(
+                                repository = PendingSelectionCallbackRepository(),
+                                chooserRequestInteractor =
+                                    UpdateChooserRequestInteractor(
+                                        repository = chooserRequestRepo,
+                                        pendingIntentSender = { error("unexpected invocation") },
+                                    )
+                            )
+                    ),
             )
         val keySet = underTest.previews.stateIn(backgroundScope)
 
@@ -95,9 +115,35 @@
         val previewsRepo = CursorPreviewsRepository()
         val selectionRepo =
             PreviewSelectionsRepository().apply {
-                setSelection(setOf(PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), null)))
+                selections.value =
+                    setOf(
+                        PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), null),
+                    )
             }
-        val underTest = SelectablePreviewsInteractor(previewsRepo, selectionRepo)
+        val chooserRequestRepo =
+            ChooserRequestRepository(
+                initialRequest = fakeChooserRequest(),
+                initialActions = emptyList(),
+            )
+        val underTest =
+            SelectablePreviewsInteractor(
+                previewsRepo = previewsRepo,
+                selectionInteractor =
+                    SelectionInteractor(
+                        selectionRepo,
+                        targetIntentModifier = { error("unexpected invocation") },
+                        updateTargetIntentInteractor =
+                            UpdateTargetIntentInteractor(
+                                repository = PendingSelectionCallbackRepository(),
+                                chooserRequestInteractor =
+                                    UpdateChooserRequestInteractor(
+                                        repository = chooserRequestRepo,
+                                        pendingIntentSender = { error("unexpected invocation") },
+                                    )
+                            )
+                    ),
+            )
+
         val previews = underTest.previews.stateIn(backgroundScope)
         val firstModel =
             underTest.preview(PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), null))
@@ -124,7 +170,7 @@
                 loadMoreLeft = null,
                 loadMoreRight = { loadRequested = true },
             )
-        selectionRepo.setSelection(emptySet())
+        selectionRepo.selections.value = emptySet()
         runCurrent()
 
         assertThat(previews.value).isNotNull()
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractorTest.kt
new file mode 100644
index 0000000..05c7646
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractorTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import android.content.Intent
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.PendingIntentSender
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate
+import com.android.intentresolver.v2.data.model.fakeChooserRequest
+import com.android.intentresolver.v2.data.repository.ChooserRequestRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class UpdateChooserRequestInteractorTest {
+    @Test
+    fun updateTargetIntentWithSelection() = runTest {
+        val pendingIntentSender = PendingIntentSender {}
+        val chooserRequestRepository =
+            ChooserRequestRepository(
+                initialRequest = fakeChooserRequest(),
+                initialActions = emptyList(),
+            )
+        val selectionCallbackResult = ShareouselUpdate(metadataText = ValueUpdate.Value("update"))
+        val pendingSelectionCallbackRepository = PendingSelectionCallbackRepository()
+        val updateTargetIntentInteractor =
+            UpdateTargetIntentInteractor(
+                repository = pendingSelectionCallbackRepository,
+                chooserRequestInteractor =
+                    UpdateChooserRequestInteractor(
+                        repository = chooserRequestRepository,
+                        pendingIntentSender = pendingIntentSender,
+                    )
+            )
+        val processTargetIntentUpdatesInteractor =
+            ProcessTargetIntentUpdatesInteractor(
+                selectionCallback = { selectionCallbackResult },
+                repository = pendingSelectionCallbackRepository,
+                chooserRequestInteractor =
+                    UpdateChooserRequestInteractor(
+                        repository = chooserRequestRepository,
+                        pendingIntentSender = pendingIntentSender,
+                    )
+            )
+
+        backgroundScope.launch { processTargetIntentUpdatesInteractor.activate() }
+
+        updateTargetIntentInteractor.updateTargetIntent(Intent())
+        runCurrent()
+
+        assertThat(pendingSelectionCallbackRepository.pendingTargetIntent.value).isNull()
+        assertThat(chooserRequestRepository.chooserRequest.value.metadataText).isEqualTo("update")
+    }
+}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractorTest.kt
deleted file mode 100644
index 3f437b2..0000000
--- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractorTest.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2024 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.
- */
-
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
-package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
-
-import android.content.Intent
-import android.net.Uri
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ChooserParamsUpdateRepository
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.TargetIntentRepository
-import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate
-import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-
-class UpdateTargetIntentInteractorTest {
-    @Test
-    fun updateTargetIntentWithSelection() = runTest {
-        val initialIntent = Intent()
-        val intentRepository = TargetIntentRepository(initialIntent, emptyList())
-        val selectionRepository = PreviewSelectionsRepository()
-        val chooserParamsUpdateRepository = ChooserParamsUpdateRepository()
-        val selectionCallbackResult = ShareouselUpdate()
-        val underTest =
-            UpdateTargetIntentInteractor(
-                intentRepository = intentRepository,
-                chooserParamsUpdateRepository = chooserParamsUpdateRepository,
-                selectionCallback = { selectionCallbackResult },
-                selectionRepo = selectionRepository,
-                targetIntentModifier = { selection ->
-                    Intent()
-                        .putParcelableArrayListExtra(
-                            "selection",
-                            selection.mapTo(ArrayList()) { it.uri },
-                        )
-                },
-                pendingIntentSender = {},
-            )
-
-        backgroundScope.launch { underTest.launch() }
-        selectionRepository.setSelection(
-            setOf(
-                PreviewModel(Uri.fromParts("scheme0", "ssp0", "fragment0"), null),
-                PreviewModel(Uri.fromParts("scheme1", "ssp1", "fragment1"), null),
-            )
-        )
-        runCurrent()
-
-        // only changes in selection should trigger intent updates
-        assertThat(
-                intentRepository.targetIntent.value.intent.getParcelableArrayListExtra(
-                    "selection",
-                    Uri::class.java,
-                )
-            )
-            .isNull()
-
-        selectionRepository.select(
-            PreviewModel(Uri.fromParts("scheme2", "ssp2", "fragment2"), null),
-        )
-        runCurrent()
-
-        assertThat(
-                intentRepository.targetIntent.value.intent.getParcelableArrayListExtra(
-                    "selection",
-                    Uri::class.java,
-                )
-            )
-            .containsExactly(
-                Uri.fromParts("scheme0", "ssp0", "fragment0"),
-                Uri.fromParts("scheme1", "ssp1", "fragment1"),
-                Uri.fromParts("scheme2", "ssp2", "fragment2"),
-            )
-        assertThat(chooserParamsUpdateRepository.updates.filterNotNull().first())
-            .isEqualTo(selectionCallbackResult)
-    }
-}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt
index 24035c5..5d95df0 100644
--- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt
@@ -30,17 +30,24 @@
 import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel
 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ActivityResultRepository
 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.CursorPreviewsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository
 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.TargetIntentRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.PendingIntentSender
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.TargetIntentModifier
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.ChooserRequestInteractor
 import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.CustomActionsInteractor
 import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.SelectablePreviewsInteractor
 import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.SelectionInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.UpdateChooserRequestInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.UpdateTargetIntentInteractor
 import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
 import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
 import com.android.intentresolver.icon.BitmapIcon
 import com.android.intentresolver.logging.FakeEventLog
 import com.android.intentresolver.mock
 import com.android.intentresolver.util.comparingElementsUsingTransform
+import com.android.intentresolver.v2.data.model.fakeChooserRequest
+import com.android.intentresolver.v2.data.repository.ChooserRequestRepository
 import com.android.internal.logging.InstanceId
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
@@ -54,30 +61,75 @@
 
 class ShareouselViewModelTest {
 
-    class Dependencies {
+    class Dependencies(
+        val pendingIntentSender: PendingIntentSender,
+        val targetIntentModifier: TargetIntentModifier<PreviewModel>,
+    ) {
         val testDispatcher = StandardTestDispatcher()
         val testScope = TestScope(testDispatcher)
         val previewsRepository = CursorPreviewsRepository()
         val selectionRepository =
             PreviewSelectionsRepository().apply {
-                setSelection(setOf(PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), null)))
+                selections.value =
+                    setOf(PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), null))
             }
         val activityResultRepository = ActivityResultRepository()
         val contentResolver = mock<ContentResolver> {}
         val packageManager = mock<PackageManager> {}
         val eventLog = FakeEventLog(instanceId = InstanceId.fakeInstanceId(1))
-        val targetIntentRepo =
-            TargetIntentRepository(
-                initialIntent = Intent(),
-                initialActions = listOf(),
+        val chooserRequestRepo =
+            ChooserRequestRepository(
+                initialRequest = fakeChooserRequest(),
+                initialActions = emptyList(),
             )
+        val pendingSelectionCallbackRepo = PendingSelectionCallbackRepository()
+
+        val actionsInteractor
+            get() =
+                CustomActionsInteractor(
+                    activityResultRepo = activityResultRepository,
+                    bgDispatcher = testDispatcher,
+                    contentResolver = contentResolver,
+                    eventLog = eventLog,
+                    packageManager = packageManager,
+                    chooserRequestInteractor = chooserRequestInteractor,
+                )
+
+        val selectionInteractor
+            get() =
+                SelectionInteractor(
+                    selectionsRepo = selectionRepository,
+                    targetIntentModifier = targetIntentModifier,
+                    updateTargetIntentInteractor = updateTargetIntentInteractor,
+                )
+
+        val updateTargetIntentInteractor
+            get() =
+                UpdateTargetIntentInteractor(
+                    repository = pendingSelectionCallbackRepo,
+                    chooserRequestInteractor = updateChooserRequestInteractor,
+                )
+
+        val updateChooserRequestInteractor
+            get() =
+                UpdateChooserRequestInteractor(
+                    repository = chooserRequestRepo,
+                    pendingIntentSender = pendingIntentSender,
+                )
+
+        val chooserRequestInteractor
+            get() = ChooserRequestInteractor(repository = chooserRequestRepo)
+
+        val previewsInteractor
+            get() =
+                SelectablePreviewsInteractor(
+                    previewsRepo = previewsRepository,
+                    selectionInteractor = selectionInteractor,
+                )
+
         val underTest =
             ShareouselViewModelModule.create(
-                interactor =
-                    SelectablePreviewsInteractor(
-                        previewsRepo = previewsRepository,
-                        selectionRepo = selectionRepository
-                    ),
+                interactor = previewsInteractor,
                 imageLoader =
                     FakeImageLoader(
                         initialBitmaps =
@@ -86,15 +138,7 @@
                                     Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)
                             )
                     ),
-                actionsInteractor =
-                    CustomActionsInteractor(
-                        activityResultRepo = activityResultRepository,
-                        bgDispatcher = testDispatcher,
-                        contentResolver = contentResolver,
-                        eventLog = eventLog,
-                        packageManager = packageManager,
-                        targetIntentRepo = targetIntentRepo,
-                    ),
+                actionsInteractor = actionsInteractor,
                 headlineGenerator =
                     object : HeadlineGenerator {
                         override fun getImagesHeadline(count: Int): String = "IMAGES: $count"
@@ -123,18 +167,19 @@
 
                         override fun getFilesHeadline(count: Int): String = error("not supported")
                     },
-                selectionInteractor =
-                    SelectionInteractor(
-                        selectionRepo = selectionRepository,
-                    ),
+                selectionInteractor = selectionInteractor,
                 scope = testScope.backgroundScope,
             )
     }
 
     private inline fun runTestWithDeps(
+        pendingIntentSender: PendingIntentSender = PendingIntentSender {},
+        targetIntentModifier: TargetIntentModifier<PreviewModel> = TargetIntentModifier {
+            error("unexpected invocation")
+        },
         crossinline block: suspend TestScope.(Dependencies) -> Unit,
     ): Unit =
-        Dependencies().run {
+        Dependencies(pendingIntentSender, targetIntentModifier).run {
             testScope.runTest {
                 runCurrent()
                 block(this@run)
@@ -145,7 +190,7 @@
     fun headline() = runTestWithDeps { deps ->
         with(deps) {
             assertThat(underTest.headline.first()).isEqualTo("IMAGES: 1")
-            selectionRepository.setSelection(
+            selectionRepository.selections.value =
                 setOf(
                     PreviewModel(
                         Uri.fromParts("scheme", "ssp", "fragment"),
@@ -156,88 +201,105 @@
                         null,
                     )
                 )
-            )
             runCurrent()
             assertThat(underTest.headline.first()).isEqualTo("IMAGES: 2")
         }
     }
 
     @Test
-    fun previews() = runTestWithDeps { deps ->
-        with(deps) {
-            previewsRepository.previewsModel.value =
-                PreviewsModel(
-                    previewModels =
-                        setOf(
-                            PreviewModel(
-                                Uri.fromParts("scheme", "ssp", "fragment"),
-                                null,
+    fun previews() =
+        runTestWithDeps(targetIntentModifier = { Intent() }) { deps ->
+            with(deps) {
+                previewsRepository.previewsModel.value =
+                    PreviewsModel(
+                        previewModels =
+                            setOf(
+                                PreviewModel(
+                                    Uri.fromParts("scheme", "ssp", "fragment"),
+                                    null,
+                                ),
+                                PreviewModel(
+                                    Uri.fromParts("scheme1", "ssp1", "fragment1"),
+                                    null,
+                                )
                             ),
-                            PreviewModel(
-                                Uri.fromParts("scheme1", "ssp1", "fragment1"),
-                                null,
-                            )
-                        ),
-                    startIdx = 1,
-                    loadMoreLeft = null,
-                    loadMoreRight = null,
-                )
-            runCurrent()
+                        startIdx = 1,
+                        loadMoreLeft = null,
+                        loadMoreRight = null,
+                    )
+                runCurrent()
 
-            assertWithMessage("previewsKeys is null").that(underTest.previews.first()).isNotNull()
-            assertThat(underTest.previews.first()!!.previewModels)
-                .comparingElementsUsingTransform("has uri of") { it: PreviewModel -> it.uri }
-                .containsExactly(
-                    Uri.fromParts("scheme", "ssp", "fragment"),
-                    Uri.fromParts("scheme1", "ssp1", "fragment1"),
-                )
-                .inOrder()
+                assertWithMessage("previewsKeys is null")
+                    .that(underTest.previews.first())
+                    .isNotNull()
+                assertThat(underTest.previews.first()!!.previewModels)
+                    .comparingElementsUsingTransform("has uri of") { it: PreviewModel -> it.uri }
+                    .containsExactly(
+                        Uri.fromParts("scheme", "ssp", "fragment"),
+                        Uri.fromParts("scheme1", "ssp1", "fragment1"),
+                    )
+                    .inOrder()
 
-            val previewVm =
-                underTest.preview(PreviewModel(Uri.fromParts("scheme1", "ssp1", "fragment1"), null))
+                val previewVm =
+                    underTest.preview(
+                        PreviewModel(Uri.fromParts("scheme1", "ssp1", "fragment1"), null)
+                    )
 
-            assertWithMessage("preview bitmap is null").that(previewVm.bitmap.first()).isNotNull()
-            assertThat(previewVm.isSelected.first()).isFalse()
+                assertWithMessage("preview bitmap is null")
+                    .that(previewVm.bitmap.first())
+                    .isNotNull()
+                assertThat(previewVm.isSelected.first()).isFalse()
 
-            previewVm.setSelected(true)
+                previewVm.setSelected(true)
 
-            assertThat(selectionRepository.selections.first().selection)
-                .comparingElementsUsingTransform("has uri of") { model: PreviewModel -> model.uri }
-                .contains(Uri.fromParts("scheme1", "ssp1", "fragment1"))
+                assertThat(selectionRepository.selections.value)
+                    .comparingElementsUsingTransform("has uri of") { model: PreviewModel ->
+                        model.uri
+                    }
+                    .contains(Uri.fromParts("scheme1", "ssp1", "fragment1"))
+            }
         }
-    }
 
     @Test
-    fun actions() = runTestWithDeps { deps ->
-        with(deps) {
-            assertThat(underTest.actions.first()).isEmpty()
+    fun actions() {
+        runTestWithDeps { deps ->
+            with(deps) {
+                assertThat(underTest.actions.first()).isEmpty()
 
-            val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)
-            val icon = Icon.createWithBitmap(bitmap)
-            var actionSent = false
-            targetIntentRepo.customActions.value =
-                listOf(CustomActionModel("label1", icon) { actionSent = true })
-            runCurrent()
+                val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)
+                val icon = Icon.createWithBitmap(bitmap)
+                var actionSent = false
+                chooserRequestRepo.customActions.value =
+                    listOf(
+                        CustomActionModel(
+                            label = "label1",
+                            icon = icon,
+                            performAction = { actionSent = true },
+                        )
+                    )
+                runCurrent()
 
-            assertThat(underTest.actions.first())
-                .comparingElementsUsingTransform("has a label of") { vm: ActionChipViewModel ->
-                    vm.label
-                }
-                .containsExactly("label1")
-                .inOrder()
-            assertThat(underTest.actions.first())
-                .comparingElementsUsingTransform("has an icon of") { vm: ActionChipViewModel ->
-                    vm.icon
-                }
-                .containsExactly(BitmapIcon(icon.bitmap))
-                .inOrder()
+                assertThat(underTest.actions.first())
+                    .comparingElementsUsingTransform("has a label of") { vm: ActionChipViewModel ->
+                        vm.label
+                    }
+                    .containsExactly("label1")
+                    .inOrder()
+                assertThat(underTest.actions.first())
+                    .comparingElementsUsingTransform("has an icon of") { vm: ActionChipViewModel ->
+                        vm.icon
+                    }
+                    .containsExactly(BitmapIcon(icon.bitmap))
+                    .inOrder()
 
-            underTest.actions.first()[0].onClicked()
+                underTest.actions.first()[0].onClicked()
 
-            assertThat(actionSent).isTrue()
-            assertThat(eventLog.customActionSelected)
-                .isEqualTo(FakeEventLog.CustomActionSelected(0))
-            assertThat(activityResultRepository.activityResult.value).isEqualTo(Activity.RESULT_OK)
+                assertThat(actionSent).isTrue()
+                assertThat(eventLog.customActionSelected)
+                    .isEqualTo(FakeEventLog.CustomActionSelected(0))
+                assertThat(activityResultRepository.activityResult.value)
+                    .isEqualTo(Activity.RESULT_OK)
+            }
         }
     }
 }
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/TargetIntentRecord.kt b/tests/unit/src/com/android/intentresolver/v2/data/model/FakeChooserRequest.kt
similarity index 67%
rename from java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/TargetIntentRecord.kt
rename to tests/unit/src/com/android/intentresolver/v2/data/model/FakeChooserRequest.kt
index 1739302..559e3b7 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/TargetIntentRecord.kt
+++ b/tests/unit/src/com/android/intentresolver/v2/data/model/FakeChooserRequest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2024 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -14,8 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.intentresolver.contentpreview.payloadtoggle.data.model
+package com.android.intentresolver.v2.data.model
 
 import android.content.Intent
+import android.net.Uri
 
-data class TargetIntentRecord(val isInitial: Boolean, val intent: Intent)
+fun fakeChooserRequest(
+    intent: Intent = Intent(),
+    packageName: String = "pkg",
+    referrer: Uri? = null,
+) = ChooserRequest(intent, packageName, null)
diff --git a/tests/unit/src/com/android/intentresolver/v2/domain/interactor/ChooserRequestUpdateInteractorTest.kt b/tests/unit/src/com/android/intentresolver/v2/domain/interactor/ChooserRequestUpdateInteractorTest.kt
deleted file mode 100644
index d05a0a9..0000000
--- a/tests/unit/src/com/android/intentresolver/v2/domain/interactor/ChooserRequestUpdateInteractorTest.kt
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * Copyright 2024 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.v2.domain.interactor
-
-import android.content.Intent
-import android.content.Intent.ACTION_SEND
-import android.content.Intent.ACTION_SEND_MULTIPLE
-import android.content.Intent.EXTRA_STREAM
-import android.content.IntentSender
-import android.net.Uri
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ChooserParamsUpdateRepository
-import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.TargetIntentRepository
-import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate
-import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate
-import com.android.intentresolver.mock
-import com.android.intentresolver.v2.ui.model.ChooserRequest
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-
-class ChooserRequestUpdateInteractorTest {
-    private val targetIntent =
-        Intent(ACTION_SEND_MULTIPLE).apply {
-            putExtra(
-                EXTRA_STREAM,
-                ArrayList<Uri>().apply {
-                    add(createUri(1))
-                    add(createUri(2))
-                }
-            )
-            type = "image/png"
-        }
-    val initialRequest = createSomeChooserRequest(targetIntent)
-    private val targetIntentRepository =
-        TargetIntentRepository(
-            targetIntent,
-            emptyList(),
-        )
-    private val chooserParamsUpdateRepository = ChooserParamsUpdateRepository()
-    private val testScope = TestScope()
-
-    @Test
-    fun testInitialIntentOnly_noUpdates() =
-        testScope.runTest {
-            val requestFlow = MutableStateFlow(initialRequest)
-            val testSubject =
-                ChooserRequestUpdateInteractor(
-                    targetIntentRepository,
-                    chooserParamsUpdateRepository,
-                    requestFlow,
-                )
-            backgroundScope.launch { testSubject.launch() }
-            testScheduler.runCurrent()
-
-            assertWithMessage("No updates expected")
-                .that(requestFlow.value)
-                .isSameInstanceAs(initialRequest)
-        }
-
-    @Test
-    fun testIntentUpdate_newRequestPublished() =
-        testScope.runTest {
-            val requestFlow = MutableStateFlow(initialRequest)
-            val testSubject =
-                ChooserRequestUpdateInteractor(
-                    targetIntentRepository,
-                    chooserParamsUpdateRepository,
-                    requestFlow,
-                )
-            backgroundScope.launch { testSubject.launch() }
-            targetIntentRepository.updateTargetIntent(
-                Intent(targetIntent).apply {
-                    action = ACTION_SEND
-                    putExtra(EXTRA_STREAM, createUri(2))
-                }
-            )
-            testScheduler.runCurrent()
-
-            assertWithMessage("Another chooser request is expected")
-                .that(requestFlow.value)
-                .isNotEqualTo(initialRequest)
-        }
-
-    @Test
-    fun testChooserParamsUpdate_newRequestPublished() =
-        testScope.runTest {
-            val requestFlow = MutableStateFlow(initialRequest)
-            val testSubject =
-                ChooserRequestUpdateInteractor(
-                    targetIntentRepository,
-                    chooserParamsUpdateRepository,
-                    requestFlow,
-                )
-            backgroundScope.launch { testSubject.launch() }
-            val newResultSender = mock<IntentSender>()
-            chooserParamsUpdateRepository.setUpdates(
-                ShareouselUpdate(
-                    resultIntentSender = ValueUpdate.Value(newResultSender),
-                )
-            )
-            testScheduler.runCurrent()
-
-            assertWithMessage("Another chooser request is expected")
-                .that(requestFlow.value)
-                .isNotEqualTo(initialRequest)
-
-            assertWithMessage("Another chooser request is expected")
-                .that(requestFlow.value.chosenComponentSender)
-                .isSameInstanceAs(newResultSender)
-        }
-
-    @Test
-    fun testTargetIntentUpdateDoesNotOverrideOtherParameters() =
-        testScope.runTest {
-            val requestFlow = MutableStateFlow(initialRequest)
-            val testSubject =
-                ChooserRequestUpdateInteractor(
-                    targetIntentRepository,
-                    chooserParamsUpdateRepository,
-                    requestFlow,
-                )
-            backgroundScope.launch { testSubject.launch() }
-
-            val newResultSender = mock<IntentSender>()
-            val newTargetIntent = Intent(Intent.ACTION_VIEW)
-            chooserParamsUpdateRepository.setUpdates(
-                ShareouselUpdate(
-                    resultIntentSender = ValueUpdate.Value(newResultSender),
-                )
-            )
-            testScheduler.runCurrent()
-            targetIntentRepository.updateTargetIntent(newTargetIntent)
-            testScheduler.runCurrent()
-
-            assertThat(requestFlow.value.targetIntent).isSameInstanceAs(newTargetIntent)
-
-            assertThat(requestFlow.value.chosenComponentSender).isSameInstanceAs(newResultSender)
-        }
-
-    @Test
-    fun testUpdateWithNullValues() =
-        testScope.runTest {
-            val initialRequest =
-                ChooserRequest(
-                    targetIntent = targetIntent,
-                    targetAction = targetIntent.action,
-                    isSendActionTarget = true,
-                    targetType = null,
-                    launchedFromPackage = "",
-                    referrer = null,
-                    refinementIntentSender = mock<IntentSender>(),
-                    chosenComponentSender = mock<IntentSender>(),
-                )
-            val requestFlow = MutableStateFlow(initialRequest)
-            val testSubject =
-                ChooserRequestUpdateInteractor(
-                    targetIntentRepository,
-                    chooserParamsUpdateRepository,
-                    requestFlow,
-                )
-            backgroundScope.launch { testSubject.launch() }
-
-            chooserParamsUpdateRepository.setUpdates(
-                ShareouselUpdate(
-                    resultIntentSender = ValueUpdate.Value(null),
-                    refinementIntentSender = ValueUpdate.Value(null),
-                )
-            )
-            testScheduler.runCurrent()
-
-            assertThat(requestFlow.value.chosenComponentSender).isNull()
-            assertThat(requestFlow.value.refinementIntentSender).isNull()
-        }
-}
-
-private fun createSomeChooserRequest(targetIntent: Intent) =
-    ChooserRequest(
-        targetIntent = targetIntent,
-        targetAction = targetIntent.action,
-        isSendActionTarget = true,
-        targetType = null,
-        launchedFromPackage = "",
-        referrer = null,
-    )
-
-private fun createUri(id: Int) = Uri.parse("content://org.pkg.app/image-$id.png")
diff --git a/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt
index d3b9f55..987d55f 100644
--- a/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt
+++ b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt
@@ -31,8 +31,8 @@
 import androidx.core.os.bundleOf
 import com.android.intentresolver.ContentTypeHint
 import com.android.intentresolver.inject.FakeChooserServiceFlags
+import com.android.intentresolver.v2.data.model.ChooserRequest
 import com.android.intentresolver.v2.ui.model.ActivityModel
-import com.android.intentresolver.v2.ui.model.ChooserRequest
 import com.android.intentresolver.v2.validation.Importance
 import com.android.intentresolver.v2.validation.Invalid
 import com.android.intentresolver.v2.validation.NoValue
@@ -126,10 +126,7 @@
     fun payloadIntents_includesTargetThenAdditional() {
         val intent1 = Intent(ACTION_SEND)
         val intent2 = Intent(ACTION_SEND_MULTIPLE)
-        val model = createActivityModel(
-            targetIntent = intent1,
-            additionalIntents = listOf(intent2)
-        )
+        val model = createActivityModel(targetIntent = intent1, additionalIntents = listOf(intent2))
 
         val result = readChooserRequest(model, fakeChooserServiceFlags)
 
@@ -229,7 +226,8 @@
         fakeChooserServiceFlags.setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, true)
         val uri = Uri.parse("content://org.pkg/path")
         val position = 10
-        val model = createActivityModel(targetIntent = Intent(ACTION_VIEW)).apply {
+        val model =
+            createActivityModel(targetIntent = Intent(ACTION_VIEW)).apply {
                 intent.putExtra(EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI, uri)
                 intent.putExtra(EXTRA_CHOOSER_FOCUSED_ITEM_POSITION, position)
             }
@@ -266,7 +264,8 @@
     fun metadataText_whenFlagFalse_isNull() {
         fakeChooserServiceFlags.setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, false)
         val metadataText: CharSequence = "Test metadata text"
-        val model = createActivityModel(targetIntent = Intent()).apply {
+        val model =
+            createActivityModel(targetIntent = Intent()).apply {
                 intent.putExtra(Intent.EXTRA_METADATA_TEXT, metadataText)
             }
 
@@ -283,7 +282,8 @@
         // Arrange
         fakeChooserServiceFlags.setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, true)
         val metadataText: CharSequence = "Test metadata text"
-        val model = createActivityModel(targetIntent = Intent()).apply {
+        val model =
+            createActivityModel(targetIntent = Intent()).apply {
                 intent.putExtra(Intent.EXTRA_METADATA_TEXT, metadataText)
             }
 
diff --git a/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt
index 6f1ed85..f647566 100644
--- a/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt
+++ b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt
@@ -24,7 +24,6 @@
 import com.android.intentresolver.v2.ResolverActivity.PROFILE_WORK
 import com.android.intentresolver.v2.shared.model.Profile.Type.WORK
 import com.android.intentresolver.v2.ui.model.ActivityModel
-import com.android.intentresolver.v2.ui.model.ChooserRequest
 import com.android.intentresolver.v2.ui.model.ResolverRequest
 import com.android.intentresolver.v2.validation.Invalid
 import com.android.intentresolver.v2.validation.UncaughtException