summaryrefslogtreecommitdiff
path: root/java/src
diff options
context:
space:
mode:
author Steve Elliott <steell@google.com> 2024-03-06 14:37:26 -0500
committer Steve Elliott <steell@google.com> 2024-03-08 10:39:37 -0500
commit71871fa2621be09aebfe18680cf7d84a66365cf9 (patch)
tree19bbe8f38a428243bddf48e7698b165a362dac72 /java/src
parent280fd53ff42834beba56a4eff56e5e7802b250ee (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')
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt16
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt11
-rw-r--r--java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt97
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifier.kt (renamed from java/src/com/android/intentresolver/contentpreview/TargetIntentModifier.kt)51
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractor.kt67
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt45
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractor.kt42
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt32
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt65
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ActionModel.kt31
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt128
-rw-r--r--java/src/com/android/intentresolver/logging/EventLogModule.kt8
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserHelper.kt11
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")
}