diff options
| author | 2024-03-06 14:37:26 -0500 | |
|---|---|---|
| committer | 2024-03-08 10:39:37 -0500 | |
| commit | 71871fa2621be09aebfe18680cf7d84a66365cf9 (patch) | |
| tree | 19bbe8f38a428243bddf48e7698b165a362dac72 /java/src | |
| parent | 280fd53ff42834beba56a4eff56e5e7802b250ee (diff) | |
PayloadToggle domain layer selection tracking
Bug: 302691505
Flag: ACONFIG android.service.chooser.chooser_payload_toggling DEVELOPMENT
Test: atest IntentResolver-tests-unit
Change-Id: Ibb3c242abc90048304dc4c57b18a3102a0fd8fed
Diffstat (limited to 'java/src')
13 files changed, 471 insertions, 133 deletions
diff --git a/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt b/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt index eda5c4ca..cc82c0a9 100644 --- a/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt +++ b/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt @@ -17,12 +17,11 @@ package com.android.intentresolver.contentpreview import android.content.Intent -import android.content.IntentSender import android.net.Uri import android.service.chooser.ChooserAction -import android.service.chooser.ChooserTarget import android.util.Log import android.util.SparseArray +import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.SelectionChangeCallback.ShareouselUpdate import java.io.Closeable import java.util.LinkedList import java.util.concurrent.atomic.AtomicBoolean @@ -53,7 +52,7 @@ class PayloadToggleInteractor( private val cursorReaderProvider: suspend () -> CursorReader, private val uriMetadataReader: (Uri) -> FileInfo, private val targetIntentModifier: (List<Item>) -> Intent, - private val selectionCallback: (Intent) -> ShareouselUpdate?, + private val selectionCallback: suspend (Intent) -> ShareouselUpdate?, ) { private var cursorDataRef = CompletableDeferred<CursorData?>() private val records = LinkedList<Record>() @@ -279,7 +278,7 @@ class PayloadToggleInteractor( private suspend fun waitForCursorData() = cursorDataRef.await() - private fun notifySelectionChanged(targetIntent: Intent) { + private suspend fun notifySelectionChanged(targetIntent: Intent) { selectionCallback(targetIntent)?.customActions?.let { customActions.tryEmit(it) } } @@ -338,15 +337,6 @@ class PayloadToggleInteractor( val isSelected = MutableStateFlow(false) } - data class ShareouselUpdate( - // for all properties, null value means no change - val customActions: List<ChooserAction>? = null, - val modifyShareAction: ChooserAction? = null, - val alternateIntents: List<Intent>? = null, - val callerTargets: List<ChooserTarget>? = null, - val refinementIntentSender: IntentSender? = null, - ) - private data class CursorData( val reader: CursorReader, val selectionTracker: SelectionTracker<Item>, diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt index 2468bb57..f79f0525 100644 --- a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt @@ -27,6 +27,8 @@ import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.AP import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras import com.android.intentresolver.R +import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.TargetIntentModifierImpl +import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.SelectionChangeCallbackImpl import com.android.intentresolver.inject.Background import java.util.concurrent.Executors import kotlinx.coroutines.CoroutineDispatcher @@ -115,8 +117,13 @@ class PreviewViewModel( ) }, UriMetadataReaderImpl(contentResolver, DefaultMimeTypeClassifier)::getMetadata, - TargetIntentModifier(targetIntent, getUri = { uri }, getMimeType = { mimeType }), - SelectionChangeCallback(contentProviderUri, chooserIntent, contentResolver) + TargetIntentModifierImpl<PayloadToggleInteractor.Item>( + targetIntent, + getUri = { uri }, + getMimeType = { mimeType }, + )::onSelectionChanged, + SelectionChangeCallbackImpl(contentProviderUri, chooserIntent, contentResolver):: + onSelectionChanged, ) } diff --git a/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt b/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt deleted file mode 100644 index 6b33e1cd..00000000 --- a/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt +++ /dev/null @@ -1,97 +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 - -import android.content.ContentInterface -import android.content.Intent -import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION -import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER -import android.content.Intent.EXTRA_CHOOSER_TARGETS -import android.content.Intent.EXTRA_INTENT -import android.content.IntentSender -import android.net.Uri -import android.os.Bundle -import android.service.chooser.AdditionalContentContract.MethodNames.ON_SELECTION_CHANGED -import android.service.chooser.ChooserAction -import android.service.chooser.ChooserTarget -import com.android.intentresolver.contentpreview.PayloadToggleInteractor.ShareouselUpdate -import com.android.intentresolver.v2.ui.viewmodel.readAlternateIntents -import com.android.intentresolver.v2.ui.viewmodel.readChooserActions -import com.android.intentresolver.v2.validation.Invalid -import com.android.intentresolver.v2.validation.Valid -import com.android.intentresolver.v2.validation.ValidationResult -import com.android.intentresolver.v2.validation.log -import com.android.intentresolver.v2.validation.types.array -import com.android.intentresolver.v2.validation.types.value -import com.android.intentresolver.v2.validation.validateFrom - -private const val TAG = "SelectionChangeCallback" - -/** - * Encapsulates payload change callback invocation to the sharing app; handles callback arguments - * and result format mapping. - */ -class SelectionChangeCallback( - private val uri: Uri, - private val chooserIntent: Intent, - private val contentResolver: ContentInterface, -) : (Intent) -> ShareouselUpdate? { - fun onSelectionChanged(targetIntent: Intent): ShareouselUpdate? = - contentResolver - .call( - requireNotNull(uri.authority) { "URI authority can not be null" }, - ON_SELECTION_CHANGED, - uri.toString(), - Bundle().apply { - putParcelable( - EXTRA_INTENT, - Intent(chooserIntent).apply { putExtra(EXTRA_INTENT, targetIntent) } - ) - } - ) - ?.let { bundle -> - return when (val result = readCallbackResponse(bundle)) { - is Valid -> result.value - is Invalid -> { - result.errors.forEach { it.log(TAG) } - null - } - } - } - - override fun invoke(targetIntent: Intent) = onSelectionChanged(targetIntent) - - private fun readCallbackResponse(bundle: Bundle): ValidationResult<ShareouselUpdate> { - return validateFrom(bundle::get) { - val customActions = readChooserActions() - val modifyShareAction = - optional(value<ChooserAction>(EXTRA_CHOOSER_MODIFY_SHARE_ACTION)) - val alternateIntents = readAlternateIntents() - val callerTargets = optional(array<ChooserTarget>(EXTRA_CHOOSER_TARGETS)) - val refinementIntentSender = - optional(value<IntentSender>(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER)) - - ShareouselUpdate( - customActions, - modifyShareAction, - alternateIntents, - callerTargets, - refinementIntentSender, - ) - } - } -} diff --git a/java/src/com/android/intentresolver/contentpreview/TargetIntentModifier.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifier.kt index 58da5bc4..577dc34c 100644 --- a/java/src/com/android/intentresolver/contentpreview/TargetIntentModifier.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifier.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.intentresolver.contentpreview +package com.android.intentresolver.contentpreview.payloadtoggle.domain.intent import android.content.ClipData import android.content.ClipDescription.compareMimeTypes @@ -23,29 +23,38 @@ import android.content.Intent.ACTION_SEND import android.content.Intent.ACTION_SEND_MULTIPLE import android.content.Intent.EXTRA_STREAM import android.net.Uri +import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel +import com.android.intentresolver.inject.TargetIntent +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent /** Modifies target intent based on current payload selection. */ -class TargetIntentModifier<Item>( +fun interface TargetIntentModifier<Item> { + fun onSelectionChanged(selection: Collection<Item>): Intent +} + +class TargetIntentModifierImpl<Item>( private val originalTargetIntent: Intent, private val getUri: Item.() -> Uri, private val getMimeType: Item.() -> String?, -) : (List<Item>) -> Intent { - fun onSelectionChanged(selection: List<Item>): Intent { - val uris = ArrayList<Uri>(selection.size) - var targetMimeType: String? = null - for (item in selection) { - targetMimeType = updateMimeType(item.getMimeType(), targetMimeType) - uris.add(item.getUri()) - } - val action = if (uris.size == 1) ACTION_SEND else ACTION_SEND_MULTIPLE +) : TargetIntentModifier<Item> { + override fun onSelectionChanged(selection: Collection<Item>): Intent { + val uris = selection.mapTo(ArrayList()) { it.getUri() } + val targetMimeType = + selection.fold(null) { target: String?, item: Item -> + updateMimeType(item.getMimeType(), target) + } return Intent(originalTargetIntent).apply { - this.action = action - this.type = targetMimeType - if (action == ACTION_SEND) { - putExtra(EXTRA_STREAM, uris[0]) + if (selection.size == 1) { + action = ACTION_SEND + putExtra(EXTRA_STREAM, selection.first().getUri()) } else { + action = ACTION_SEND_MULTIPLE putParcelableArrayListExtra(EXTRA_STREAM, uris) } + type = targetMimeType if (uris.isNotEmpty()) { clipData = ClipData("", arrayOf(targetMimeType), ClipData.Item(uris[0])).also { @@ -70,6 +79,14 @@ class TargetIntentModifier<Item>( } return "*/*" } +} - override fun invoke(selection: List<Item>): Intent = onSelectionChanged(selection) +@Module +@InstallIn(ViewModelComponent::class) +object TargetIntentModifierModule { + @Provides + fun targetIntentModifier( + @TargetIntent targetIntent: Intent, + ): TargetIntentModifier<PreviewModel> = + TargetIntentModifierImpl(targetIntent, { uri }, { mimeType }) } 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 new file mode 100644 index 00000000..56f781fb --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractor.kt @@ -0,0 +1,67 @@ +/* + * 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.app.Activity +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 +import com.android.intentresolver.logging.EventLog +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +class CustomActionsInteractor +@Inject +constructor( + private val activityResultRepo: ActivityResultRepository, + @Background private val bgDispatcher: CoroutineDispatcher, + private val contentResolver: ContentResolver, + private val eventLog: EventLog, + private val packageManager: PackageManager, + private val targetIntentRepo: TargetIntentRepository, +) { + /** List of [ActionModel] that can be presented in Shareousel. */ + val customActions: Flow<List<ActionModel>> + get() = + targetIntentRepo.customActions + .map { actions -> + actions.map { action -> + ActionModel( + label = action.label, + icon = action.icon.toComposeIcon(packageManager, contentResolver), + performAction = { index -> performAction(action, index) }, + ) + } + } + .flowOn(bgDispatcher) + .conflate() + + private fun performAction(action: CustomActionModel, index: Int) { + action.performAction() + eventLog.logCustomActionSelected(index) + activityResultRepo.activityResult.value = Activity.RESULT_OK + } +} 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 new file mode 100644 index 00000000..d94b1078 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt @@ -0,0 +1,45 @@ +/* + * 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.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 +import kotlinx.coroutines.flow.update + +/** An individual preview in Shareousel. */ +class SelectablePreviewInteractor( + private val key: PreviewModel, + private val selectionRepo: PreviewSelectionsRepository, +) { + val uri: Uri = key.uri + + /** Whether or not this preview is selected by the user. */ + val isSelected: Flow<Boolean> + get() = selectionRepo.selections.map { key in it } + + /** Sets whether this preview is selected by the user. */ + fun setSelected(isSelected: Boolean) { + if (isSelected) { + selectionRepo.selections.update { it + key } + } else { + selectionRepo.selections.update { it - 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 new file mode 100644 index 00000000..78e208f6 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractor.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.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 +import kotlinx.coroutines.flow.Flow + +class SelectablePreviewsInteractor +@Inject +constructor( + private val previewsRepo: CursorPreviewsRepository, + private val selectionRepo: PreviewSelectionsRepository, +) { + /** Keys of previews available for display in Shareousel. */ + val previews: Flow<PreviewsModel?> + get() = previewsRepo.previewsModel + + /** + * 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) +} 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 new file mode 100644 index 00000000..ee9bd689 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.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.domain.interactor + +import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class SelectionInteractor +@Inject +constructor( + private val selectionRepo: PreviewSelectionsRepository, +) { + /** Amount of selected previews. */ + val amountSelected: Flow<Int> + get() = selectionRepo.selections.map { it.size } +} 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 new file mode 100644 index 00000000..e7bdafbc --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor + +import 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.update.SelectionChangeCallback +import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map +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, + @CustomAction private val pendingIntentSender: PendingIntentSender, + private val selectionCallback: SelectionChangeCallback, + private val selectionRepo: PreviewSelectionsRepository, + private val targetIntentModifier: TargetIntentModifier<PreviewModel>, +) { + /** Listen for events and update state. */ + suspend fun launch(): Unit = coroutineScope { + launch { + intentRepository.targetIntent + .mapLatest { targetIntent -> + selectionCallback.onSelectionChanged(targetIntent)?.customActions ?: emptyList() + } + .collect { actions -> + intentRepository.customActions.value = + actions.map { it.toCustomActionModel(pendingIntentSender) } + } + } + launch { + selectionRepo.selections.collectLatest { + intentRepository.targetIntent.value = targetIntentModifier.onSelectionChanged(it) + } + } + } +} diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ActionModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ActionModel.kt new file mode 100644 index 00000000..f69365d7 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ActionModel.kt @@ -0,0 +1,31 @@ +/* + * 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.model + +import com.android.intentresolver.icon.ComposeIcon + +/** An action that the user can take, provided by the sharing application. */ +data class ActionModel( + /** Text shown for this action in the UI. */ + val label: CharSequence, + /** An optional [ComposeIcon] that will be displayed in the UI with this action. */ + val icon: ComposeIcon?, + /** + * Performs the action. The argument indicates the index in the UI that this action is shown. + */ + val performAction: (index: Int) -> Unit, +) diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt new file mode 100644 index 00000000..03295a31 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt @@ -0,0 +1,128 @@ +/* + * 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.update + +import android.content.ContentInterface +import android.content.Intent +import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION +import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER +import android.content.Intent.EXTRA_CHOOSER_TARGETS +import android.content.Intent.EXTRA_INTENT +import android.content.IntentSender +import android.net.Uri +import android.os.Bundle +import android.service.chooser.AdditionalContentContract.MethodNames.ON_SELECTION_CHANGED +import android.service.chooser.ChooserAction +import android.service.chooser.ChooserTarget +import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.SelectionChangeCallback.ShareouselUpdate +import com.android.intentresolver.inject.AdditionalContent +import com.android.intentresolver.inject.ChooserIntent +import com.android.intentresolver.v2.ui.viewmodel.readAlternateIntents +import com.android.intentresolver.v2.ui.viewmodel.readChooserActions +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.log +import com.android.intentresolver.v2.validation.types.array +import com.android.intentresolver.v2.validation.types.value +import com.android.intentresolver.v2.validation.validateFrom +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import javax.inject.Inject +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +private const val TAG = "SelectionChangeCallback" + +/** + * Encapsulates payload change callback invocation to the sharing app; handles callback arguments + * and result format mapping. + */ +fun interface SelectionChangeCallback { + suspend fun onSelectionChanged(targetIntent: Intent): ShareouselUpdate? + + data class ShareouselUpdate( + // for all properties, null value means no change + val customActions: List<ChooserAction>? = null, + val modifyShareAction: ChooserAction? = null, + val alternateIntents: List<Intent>? = null, + val callerTargets: List<ChooserTarget>? = null, + val refinementIntentSender: IntentSender? = null, + ) +} + +class SelectionChangeCallbackImpl +@Inject +constructor( + @AdditionalContent private val uri: Uri, + @ChooserIntent private val chooserIntent: Intent, + private val contentResolver: ContentInterface, +) : SelectionChangeCallback { + private val mutex = Mutex() + + override suspend fun onSelectionChanged(targetIntent: Intent): ShareouselUpdate? = + mutex + .withLock { + contentResolver.call( + requireNotNull(uri.authority) { "URI authority can not be null" }, + ON_SELECTION_CHANGED, + uri.toString(), + Bundle().apply { + putParcelable( + EXTRA_INTENT, + Intent(chooserIntent).apply { putExtra(EXTRA_INTENT, targetIntent) } + ) + } + ) + } + ?.let { bundle -> + return when (val result = readCallbackResponse(bundle)) { + is Valid -> result.value + is Invalid -> { + result.errors.forEach { it.log(TAG) } + null + } + } + } +} + +private fun readCallbackResponse(bundle: Bundle): ValidationResult<ShareouselUpdate> { + return validateFrom(bundle::get) { + val customActions = readChooserActions() + val modifyShareAction = optional(value<ChooserAction>(EXTRA_CHOOSER_MODIFY_SHARE_ACTION)) + val alternateIntents = readAlternateIntents() + val callerTargets = optional(array<ChooserTarget>(EXTRA_CHOOSER_TARGETS)) + val refinementIntentSender = + optional(value<IntentSender>(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER)) + + ShareouselUpdate( + customActions, + modifyShareAction, + alternateIntents, + callerTargets, + refinementIntentSender, + ) + } +} + +@Module +@InstallIn(ViewModelComponent::class) +interface SelectionChangeCallbackModule { + @Binds fun bind(impl: SelectionChangeCallbackImpl): SelectionChangeCallback +} diff --git a/java/src/com/android/intentresolver/logging/EventLogModule.kt b/java/src/com/android/intentresolver/logging/EventLogModule.kt index eba8ecc8..73af7d37 100644 --- a/java/src/com/android/intentresolver/logging/EventLogModule.kt +++ b/java/src/com/android/intentresolver/logging/EventLogModule.kt @@ -24,14 +24,14 @@ import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.hilt.android.scopes.ActivityScoped +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.hilt.android.scopes.ActivityRetainedScoped @Module -@InstallIn(ActivityComponent::class) +@InstallIn(ActivityRetainedComponent::class) interface EventLogModule { - @Binds @ActivityScoped fun eventLog(value: EventLogImpl): EventLog + @Binds @ActivityRetainedScoped fun eventLog(value: EventLogImpl): EventLog companion object { @Provides diff --git a/java/src/com/android/intentresolver/v2/ChooserHelper.kt b/java/src/com/android/intentresolver/v2/ChooserHelper.kt index 1498453b..d34e0b36 100644 --- a/java/src/com/android/intentresolver/v2/ChooserHelper.kt +++ b/java/src/com/android/intentresolver/v2/ChooserHelper.kt @@ -24,6 +24,8 @@ import androidx.activity.viewModels import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +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.domain.interactor.UserInteractor @@ -36,7 +38,9 @@ import com.android.intentresolver.v2.validation.log import dagger.hilt.android.scopes.ActivityScoped import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking private const val TAG: String = "ChooserHelper" @@ -100,6 +104,7 @@ class ChooserHelper constructor( hostActivity: Activity, private val userInteractor: UserInteractor, + private val activityResultRepo: ActivityResultRepository, @Background private val background: CoroutineDispatcher, ) : DefaultLifecycleObserver { // This is guaranteed by Hilt, since only a ComponentActivity is injectable. @@ -140,7 +145,13 @@ constructor( is Valid -> initializeActivity(request) is Invalid -> reportErrorsAndFinish(request) } + + activity.lifecycleScope.launch { + activity.setResult(activityResultRepo.activityResult.filterNotNull().first()) + activity.finish() + } } + override fun onStart(owner: LifecycleOwner) { Log.i(TAG, "START") } |