From 38a6a7637ed22bed196ede400ff72fea5407b17b Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Tue, 26 Mar 2024 17:36:06 -0400 Subject: 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 --- .../payloadtoggle/data/model/SelectionRecord.kt | 30 --- .../payloadtoggle/data/model/TargetIntentRecord.kt | 21 --- .../repository/ChooserParamsUpdateRepository.kt | 34 ---- .../PendingSelectionCallbackRepository.kt | 32 ++++ .../data/repository/PreviewSelectionsRepository.kt | 41 +--- .../data/repository/TargetIntentRepository.kt | 45 ----- .../domain/intent/TargetIntentModifier.kt | 4 +- .../domain/interactor/ChooserRequestInteractor.kt | 38 ++++ .../domain/interactor/CustomActionsInteractor.kt | 5 +- .../domain/interactor/FetchPreviewsInteractor.kt | 4 +- .../ProcessTargetIntentUpdatesInteractor.kt | 42 +++++ .../interactor/SelectablePreviewInteractor.kt | 9 +- .../interactor/SelectablePreviewsInteractor.kt | 6 +- .../domain/interactor/SelectionInteractor.kt | 27 ++- .../interactor/UpdateChooserRequestInteractor.kt | 62 ++++++ .../interactor/UpdateTargetIntentInteractor.kt | 62 ++---- .../intentresolver/inject/ActivityModelModule.kt | 12 +- .../android/intentresolver/v2/ChooserActivity.java | 2 +- .../com/android/intentresolver/v2/ChooserHelper.kt | 2 +- .../intentresolver/v2/data/model/ChooserRequest.kt | 209 +++++++++++++++++++++ .../v2/data/repository/ChooserRequestRepository.kt | 39 ++++ .../interactor/ChooserRequestUpdateInteractor.kt | 91 --------- .../intentresolver/v2/ui/model/ActivityModel.kt | 1 + .../intentresolver/v2/ui/model/ChooserRequest.kt | 209 --------------------- .../v2/ui/viewmodel/ChooserRequestReader.kt | 2 +- .../v2/ui/viewmodel/ChooserViewModel.kt | 46 ++--- 26 files changed, 504 insertions(+), 571 deletions(-) delete mode 100644 java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/SelectionRecord.kt delete mode 100644 java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/TargetIntentRecord.kt delete mode 100644 java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/ChooserParamsUpdateRepository.kt create mode 100644 java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PendingSelectionCallbackRepository.kt delete mode 100644 java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/TargetIntentRepository.kt create mode 100644 java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ChooserRequestInteractor.kt create mode 100644 java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ProcessTargetIntentUpdatesInteractor.kt create mode 100644 java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractor.kt create mode 100644 java/src/com/android/intentresolver/v2/data/model/ChooserRequest.kt create mode 100644 java/src/com/android/intentresolver/v2/data/repository/ChooserRequestRepository.kt delete mode 100644 java/src/com/android/intentresolver/v2/domain/interactor/ChooserRequestUpdateInteractor.kt delete mode 100644 java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt (limited to 'java') 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, -) - -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/ChooserParamsUpdateRepository.kt deleted file mode 100644 index 1a4f2b83..00000000 --- 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(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 00000000..1745cd9c --- /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 = 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 = _selections.asStateFlow() - - fun setSelection(selection: Set) { - _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()) } 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 bb43323a..00000000 --- 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, -) { - 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 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 { - fun onSelectionChanged(selection: Collection): Intent + fun intentFromSelection(selection: Collection): Intent } class TargetIntentModifierImpl( @@ -40,7 +40,7 @@ class TargetIntentModifierImpl( private val getUri: Item.() -> Uri, private val getMimeType: Item.() -> String?, ) : TargetIntentModifier { - override fun onSelectionChanged(selection: Collection): Intent { + override fun intentFromSelection(selection: Collection): 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 00000000..61c04ac1 --- /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 + get() = repository.chooserRequest.map { it.targetIntent } + + val customActions: Flow> + 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> 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 = 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 = selectionRepo.selections.map { key in it.selection } + val isSelected: Flow = 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 @@ -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, + private val updateTargetIntentInteractor: UpdateTargetIntentInteractor, ) { + /** Set of selected previews. */ + val selections: StateFlow> + get() = selectionsRepo.selections + /** Amount of selected previews. */ - val amountSelected: Flow = selectionRepo.selections.map { it.selection.size } + val amountSelected: Flow = 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) { + 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, + 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,11 +48,19 @@ object ActivityModelModule { @Provides @ViewModelScoped - fun provideChooserRequest( + fun provideInitialRequest( activityModel: ActivityModel, flags: ChooserServiceFlags, ): ValidationResult = readChooserRequest(activityModel, flags) + @Provides + fun provideChooserRequest( + initialRequest: ValidationResult, + ): ChooserRequest = + requireNotNull((initialRequest as? Valid)?.value) { + "initialRequest is Invalid, no chooser request available" + } + @Provides @TargetIntent fun targetIntent(chooserReq: ValidationResult): Intent = 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/data/model/ChooserRequest.kt b/java/src/com/android/intentresolver/v2/data/model/ChooserRequest.kt new file mode 100644 index 00000000..7c9c8613 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/data/model/ChooserRequest.kt @@ -0,0 +1,209 @@ +/* + * 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.model + +import android.content.ComponentName +import android.content.Intent +import android.content.Intent.ACTION_SEND +import android.content.Intent.ACTION_SEND_MULTIPLE +import android.content.Intent.EXTRA_REFERRER +import android.content.IntentFilter +import android.content.IntentSender +import android.net.Uri +import android.os.Bundle +import android.service.chooser.ChooserAction +import android.service.chooser.ChooserTarget +import androidx.annotation.StringRes +import com.android.intentresolver.ContentTypeHint +import com.android.intentresolver.v2.ext.hasAction + +const val ANDROID_APP_SCHEME = "android-app" + +/** All of the things that are consumed from an incoming share Intent (+Extras). */ +data class ChooserRequest( + /** Required. Represents the content being sent. */ + val targetIntent: Intent, + + /** The action from [targetIntent] as retrieved with [Intent.getAction]. */ + val targetAction: String?, + + /** + * Whether [targetAction] is ACTION_SEND or ACTION_SEND_MULTIPLE. These are considered the + * canonical "Share" actions. When handling other actions, this flag controls behavioral and + * visual changes. + */ + val isSendActionTarget: Boolean, + + /** The top-level content type as retrieved using [Intent.getType]. */ + val targetType: String?, + + /** The package name of the app which started the current activity instance. */ + val launchedFromPackage: String, + + /** A custom tile for the main UI. Ignored when the intent is ACTION_SEND(_MULTIPLE). */ + val title: CharSequence? = null, + + /** A String resource ID to load when [title] is null. */ + @get:StringRes val defaultTitleResource: Int = 0, + + /** + * The referrer value as received by the caller. It may have been supplied via [EXTRA_REFERRER] + * or synthesized from callerPackageName. This value is merged into outgoing intents. + */ + val referrer: Uri?, + + /** + * Choices to exclude from results. + * + * Any resolved intents with a component in this list will be omitted before presentation. + */ + val filteredComponentNames: List = emptyList(), + + /** + * App provided shortcut share intents (aka "direct share targets") + * + * Normally share shortcuts are published and consumed using + * [ShortcutManager][android.content.pm.ShortcutManager]. This is an alternate channel to allow + * apps to directly inject the same information. + * + * Historical note: This option was initially integrated with other results from the + * ChooserTargetService API (since deprecated and removed), hence the name and data format. + * These are more correctly called "Share Shortcuts" now. + */ + val callerChooserTargets: List = emptyList(), + + /** + * Actions the user may perform. These are presented as separate affordances from the main list + * of choices. Selecting a choice is a terminal action which results in finishing. The item + * limit is [MAX_CHOOSER_ACTIONS]. This may be further constrained as appropriate. + */ + val chooserActions: List = emptyList(), + + /** + * An action to start an Activity which for user updating of shared content. Selection is a + * terminal action, closing the current activity and launching the target of the action. + */ + val modifyShareAction: ChooserAction? = null, + + /** + * When false the host activity will be [finished][android.app.Activity.finish] when stopped. + */ + @get:JvmName("shouldRetainInOnStop") val shouldRetainInOnStop: Boolean = false, + + /** + * Intents which contain alternate representations of the content being shared. Any results from + * resolving these _alternate_ intents are included with the results of the primary intent as + * additional choices (e.g. share as image content vs. link to content). + */ + val additionalTargets: List = emptyList(), + + /** + * Alternate [extras][Intent.getExtras] to substitute when launching a selected app. + * + * For a given app (by package name), the Bundle describes what parameters to substitute when + * that app is selected. + * + * // TODO: Map + */ + val replacementExtras: Bundle? = null, + + /** + * App-supplied choices to be presented first in the list. + * + * Custom labels and icons may be supplied using + * [LabeledIntent][android.content.pm.LabeledIntent]. + * + * Limit 2. + */ + val initialIntents: List = emptyList(), + + /** + * Provides for callers to be notified when a component is selected. + * + * The selection is reported in the Intent as [Intent.EXTRA_CHOSEN_COMPONENT] with the + * [ComponentName] of the item. + */ + val chosenComponentSender: IntentSender? = null, + + /** + * Provides a mechanism for callers to post-process a target when a selection is made. + * + * The received intent will contain: + * * **EXTRA_INTENT** The chosen target + * * **EXTRA_ALTERNATE_INTENTS** Additional intents which also match the target + * * **EXTRA_RESULT_RECEIVER** A [ResultReceiver][android.os.ResultReceiver] providing a + * mechanism for the caller to return information. An updated intent to send must be included + * as [Intent.EXTRA_INTENT]. + */ + val refinementIntentSender: IntentSender? = null, + + /** + * Contains the text content to share supplied by the source app. + * + * TODO: Constrain length? + */ + val sharedText: CharSequence? = null, + + /** + * Supplied to + * [ShortcutManager.getShareTargets][android.content.pm.ShortcutManager.getShareTargets] to + * query for matching shortcuts. Specifically, only the [dataTypes][IntentFilter.hasDataType] + * are considered for matching share shortcuts currently. + */ + val shareTargetFilter: IntentFilter? = null, + + /** A URI for additional content */ + val additionalContentUri: Uri? = null, + + /** Focused item index (from target intent's STREAM_EXTRA) */ + val focusedItemPosition: Int = 0, + + /** Value for [Intent.EXTRA_CHOOSER_CONTENT_TYPE_HINT] on the incoming chooser intent. */ + val contentTypeHint: ContentTypeHint = ContentTypeHint.NONE, + + /** + * Metadata to be shown to the user as a part of the sharesheet window. + * + * Specified by the [Intent.EXTRA_METADATA_TEXT] + */ + val metadataText: CharSequence? = null, +) { + val referrerPackage = referrer?.takeIf { it.scheme == ANDROID_APP_SCHEME }?.authority + + fun getReferrerFillInIntent(): Intent { + return Intent().apply { + referrerPackage?.also { pkg -> + putExtra(EXTRA_REFERRER, Uri.parse("$ANDROID_APP_SCHEME://$pkg")) + } + } + } + + val payloadIntents = listOf(targetIntent) + additionalTargets + + /** Constructs an instance from only the required values. */ + constructor( + targetIntent: Intent, + launchedFromPackage: String, + referrer: Uri? + ) : this( + targetIntent = targetIntent, + targetAction = targetIntent.action, + isSendActionTarget = targetIntent.hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE), + targetType = targetIntent.type, + launchedFromPackage = launchedFromPackage, + referrer = referrer + ) +} 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, +) { + /** All information from the sharing application pertaining to the chooser. */ + val chooserRequest: MutableStateFlow = 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> = 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, -) { - - 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 - ): 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/model/ChooserRequest.kt b/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt deleted file mode 100644 index 4f3cf3cd..00000000 --- a/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt +++ /dev/null @@ -1,209 +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.v2.ui.model - -import android.content.ComponentName -import android.content.Intent -import android.content.Intent.ACTION_SEND -import android.content.Intent.ACTION_SEND_MULTIPLE -import android.content.Intent.EXTRA_REFERRER -import android.content.IntentFilter -import android.content.IntentSender -import android.net.Uri -import android.os.Bundle -import android.service.chooser.ChooserAction -import android.service.chooser.ChooserTarget -import androidx.annotation.StringRes -import com.android.intentresolver.ContentTypeHint -import com.android.intentresolver.v2.ext.hasAction - -const val ANDROID_APP_SCHEME = "android-app" - -/** All of the things that are consumed from an incoming share Intent (+Extras). */ -data class ChooserRequest( - /** Required. Represents the content being sent. */ - val targetIntent: Intent, - - /** The action from [targetIntent] as retrieved with [Intent.getAction]. */ - val targetAction: String?, - - /** - * Whether [targetAction] is ACTION_SEND or ACTION_SEND_MULTIPLE. These are considered the - * canonical "Share" actions. When handling other actions, this flag controls behavioral and - * visual changes. - */ - val isSendActionTarget: Boolean, - - /** The top-level content type as retrieved using [Intent.getType]. */ - val targetType: String?, - - /** The package name of the app which started the current activity instance. */ - val launchedFromPackage: String, - - /** A custom tile for the main UI. Ignored when the intent is ACTION_SEND(_MULTIPLE). */ - val title: CharSequence? = null, - - /** A String resource ID to load when [title] is null. */ - @get:StringRes val defaultTitleResource: Int = 0, - - /** - * The referrer value as received by the caller. It may have been supplied via [EXTRA_REFERRER] - * or synthesized from callerPackageName. This value is merged into outgoing intents. - */ - val referrer: Uri?, - - /** - * Choices to exclude from results. - * - * Any resolved intents with a component in this list will be omitted before presentation. - */ - val filteredComponentNames: List = emptyList(), - - /** - * App provided shortcut share intents (aka "direct share targets") - * - * Normally share shortcuts are published and consumed using - * [ShortcutManager][android.content.pm.ShortcutManager]. This is an alternate channel to allow - * apps to directly inject the same information. - * - * Historical note: This option was initially integrated with other results from the - * ChooserTargetService API (since deprecated and removed), hence the name and data format. - * These are more correctly called "Share Shortcuts" now. - */ - val callerChooserTargets: List = emptyList(), - - /** - * Actions the user may perform. These are presented as separate affordances from the main list - * of choices. Selecting a choice is a terminal action which results in finishing. The item - * limit is [MAX_CHOOSER_ACTIONS]. This may be further constrained as appropriate. - */ - val chooserActions: List = emptyList(), - - /** - * An action to start an Activity which for user updating of shared content. Selection is a - * terminal action, closing the current activity and launching the target of the action. - */ - val modifyShareAction: ChooserAction? = null, - - /** - * When false the host activity will be [finished][android.app.Activity.finish] when stopped. - */ - @get:JvmName("shouldRetainInOnStop") val shouldRetainInOnStop: Boolean = false, - - /** - * Intents which contain alternate representations of the content being shared. Any results from - * resolving these _alternate_ intents are included with the results of the primary intent as - * additional choices (e.g. share as image content vs. link to content). - */ - val additionalTargets: List = emptyList(), - - /** - * Alternate [extras][Intent.getExtras] to substitute when launching a selected app. - * - * For a given app (by package name), the Bundle describes what parameters to substitute when - * that app is selected. - * - * // TODO: Map - */ - val replacementExtras: Bundle? = null, - - /** - * App-supplied choices to be presented first in the list. - * - * Custom labels and icons may be supplied using - * [LabeledIntent][android.content.pm.LabeledIntent]. - * - * Limit 2. - */ - val initialIntents: List = emptyList(), - - /** - * Provides for callers to be notified when a component is selected. - * - * The selection is reported in the Intent as [Intent.EXTRA_CHOSEN_COMPONENT] with the - * [ComponentName] of the item. - */ - val chosenComponentSender: IntentSender? = null, - - /** - * Provides a mechanism for callers to post-process a target when a selection is made. - * - * The received intent will contain: - * * **EXTRA_INTENT** The chosen target - * * **EXTRA_ALTERNATE_INTENTS** Additional intents which also match the target - * * **EXTRA_RESULT_RECEIVER** A [ResultReceiver][android.os.ResultReceiver] providing a - * mechanism for the caller to return information. An updated intent to send must be included - * as [Intent.EXTRA_INTENT]. - */ - val refinementIntentSender: IntentSender? = null, - - /** - * Contains the text content to share supplied by the source app. - * - * TODO: Constrain length? - */ - val sharedText: CharSequence? = null, - - /** - * Supplied to - * [ShortcutManager.getShareTargets][android.content.pm.ShortcutManager.getShareTargets] to - * query for matching shortcuts. Specifically, only the [dataTypes][IntentFilter.hasDataType] - * are considered for matching share shortcuts currently. - */ - val shareTargetFilter: IntentFilter? = null, - - /** A URI for additional content */ - val additionalContentUri: Uri? = null, - - /** Focused item index (from target intent's STREAM_EXTRA) */ - val focusedItemPosition: Int = 0, - - /** Value for [Intent.EXTRA_CHOOSER_CONTENT_TYPE_HINT] on the incoming chooser intent. */ - val contentTypeHint: ContentTypeHint = ContentTypeHint.NONE, - - /** - * Metadata to be shown to the user as a part of the sharesheet window. - * - * Specified by the [Intent.EXTRA_METADATA_TEXT] - */ - val metadataText: CharSequence? = null, -) { - val referrerPackage = referrer?.takeIf { it.scheme == ANDROID_APP_SCHEME }?.authority - - fun getReferrerFillInIntent(): Intent { - return Intent().apply { - referrerPackage?.also { pkg -> - putExtra(EXTRA_REFERRER, Uri.parse("$ANDROID_APP_SCHEME://$pkg")) - } - } - } - - val payloadIntents = listOf(targetIntent) + additionalTargets - - /** Constructs an instance from only the required values. */ - constructor( - targetIntent: Intent, - launchedFromPackage: String, - referrer: Uri? - ) : this( - targetIntent = targetIntent, - targetAction = targetIntent.action, - isSendActionTarget = targetIntent.hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE), - targetType = targetIntent.type, - launchedFromPackage = launchedFromPackage, - referrer = referrer - ) -} 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, - private val updateTargetIntentInteractor: Lazy, + private val processUpdatesInteractor: Lazy, private val fetchPreviewsInteractor: Lazy, @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, + private val chooserRequestRepository: Lazy, ) : 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 - /** * A [StateFlow] of [ChooserRequest]. * * Note: Only safe to access after checking if [initialRequest] is [Valid]. */ - lateinit var request: StateFlow - private set + val request: StateFlow + 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") } } } -- cgit v1.2.3-59-g8ed1b