diff options
author | 2024-03-26 17:36:06 -0400 | |
---|---|---|
committer | 2024-03-28 15:42:29 -0400 | |
commit | 38a6a7637ed22bed196ede400ff72fea5407b17b (patch) | |
tree | 8799dd7642290f759083546f7c0bd36344b87acd /java | |
parent | 56c54af33d1e4a0e7d9215e90f8d5a6ba75b9ed6 (diff) |
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
Diffstat (limited to 'java')
23 files changed, 248 insertions, 315 deletions
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 c8fcb9d5..00000000 --- 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/model/TargetIntentRecord.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/TargetIntentRecord.kt deleted file mode 100644 index 17393023..00000000 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/TargetIntentRecord.kt +++ /dev/null @@ -1,21 +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 android.content.Intent - -data class TargetIntentRecord(val isInitial: Boolean, val intent: Intent) diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/ChooserParamsUpdateRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PendingSelectionCallbackRepository.kt index 1a4f2b83..1745cd9c 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/ChooserParamsUpdateRepository.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PendingSelectionCallbackRepository.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. @@ -16,19 +16,17 @@ package com.android.intentresolver.contentpreview.payloadtoggle.data.repository -import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate -import dagger.hilt.android.scopes.ViewModelScoped +import android.content.Intent +import dagger.hilt.android.scopes.ActivityRetainedScoped 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) - } +/** 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 b461d10b..9aecc981 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/domain/intent/TargetIntentModifier.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifier.kt index 577dc34c..4a2a6932 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 @@ import dagger.hilt.android.components.ViewModelComponent /** 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 @@ class TargetIntentModifierImpl<Item>( 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/data/repository/TargetIntentRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ChooserRequestInteractor.kt index bb43323a..61c04ac1 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/TargetIntentRepository.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ChooserRequestInteractor.kt @@ -14,32 +14,25 @@ * limitations under the License. */ -package com.android.intentresolver.contentpreview.payloadtoggle.data.repository +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.contentpreview.payloadtoggle.data.model.TargetIntentRecord -import com.android.intentresolver.inject.TargetIntent -import dagger.hilt.android.scopes.ViewModelScoped +import com.android.intentresolver.v2.data.repository.ChooserRequestRepository import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow +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. */ -@ViewModelScoped -class TargetIntentRepository +class ChooserRequestInteractor @Inject constructor( - @TargetIntent initialIntent: Intent, - initialActions: List<CustomActionModel>, + private val repository: ChooserRequestRepository, ) { - val targetIntent = MutableStateFlow(TargetIntentRecord(isInitial = true, initialIntent)) + val targetIntent: Flow<Intent> + get() = repository.chooserRequest.map { it.targetIntent } - // 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) - } + 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 56f781fb..e973e844 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.ContentResolver 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 @@ constructor( 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 a7749c92..9bc7ae63 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 @@ constructor( 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 00000000..04416a3d --- /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 3b5b0ddf..55a995f5 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 @@ import kotlinx.coroutines.flow.map /** 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 78e208f6..a578d0e2 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 @@ class SelectablePreviewsInteractor @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 @@ constructor( * 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 0b8bcdd7..a570f36e 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 00000000..9e48cd28 --- /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 06e28cba..429e34e9 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 c08c7f4c..ff2bb14b 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.net.Uri 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 @@ object ActivityModelModule { @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 ffa0469c..d624c9e4 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.AppPredictionServiceResolverComparator; 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.ProfilePagerResources; 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 f2a2726a..503e46d8 100644 --- a/java/src/com/android/intentresolver/v2/ChooserHelper.kt +++ b/java/src/com/android/intentresolver/v2/ChooserHelper.kt @@ -29,9 +29,9 @@ import androidx.lifecycle.repeatOnLifecycle 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 index 4f3cf3cd..7c9c8613 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 00000000..d23e07ee --- /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 37213403..00000000 --- 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 07b17435..67c2a25e 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.content.Intent 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 7ebf65a9..a25fcbea 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.ContentTypeHint 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 4431a545..e39329b1 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.SavedStateHandle 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 @@ class ChooserViewModel 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 @@ constructor( "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") } } } |